Compare commits
73 Commits
subscripti
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa2a44c734 | ||
|
|
8c2558bfe0 | ||
|
|
67087fade7 | ||
|
|
0eadc7a4ae | ||
|
|
cad4652a08 | ||
|
|
8379f2d073 | ||
|
|
8ed112a4a8 | ||
|
|
14c58c4410 | ||
|
|
66f9c3587f | ||
|
|
87ba9fcbc0 | ||
|
|
ab49f3f3a1 | ||
|
|
df969722d1 | ||
|
|
ef62633df1 | ||
|
|
597dc58eb7 | ||
|
|
5d00d642f3 | ||
|
|
39ab81c3d0 | ||
|
|
0bdd0963a4 | ||
|
|
eae51356c8 | ||
|
|
a36ad3f5e7 | ||
|
|
307ededd19 | ||
|
|
d7ecfdf10f | ||
|
|
a1c36f314d | ||
|
|
86e636cbce | ||
|
|
0d2b36ae82 | ||
|
|
95072da257 | ||
|
|
faa2621e26 | ||
|
|
73eead6b72 | ||
|
|
e449fd0eda | ||
|
|
7d25c967a5 | ||
|
|
dc9bd8bcb1 | ||
|
|
29d83e9c6d | ||
|
|
6393a77c19 | ||
|
|
6bbabf9c45 | ||
|
|
f18d1d2a51 | ||
|
|
5894c66df1 | ||
|
|
6ff8c42041 | ||
|
|
9870a6bc9c | ||
|
|
317ea8c275 | ||
|
|
bc36a93b9b | ||
|
|
847232d64b | ||
|
|
4fbccd4c91 | ||
|
|
cd560bceed | ||
|
|
e08633214e | ||
|
|
a9ad0bf38a | ||
|
|
5f8d6899d2 | ||
|
|
cd6adbadde | ||
|
|
539b43325a | ||
|
|
eeb8e9704b | ||
|
|
0aceb4b590 | ||
|
|
82fa4c1597 | ||
|
|
ee5081f807 | ||
|
|
3cbaa7bf07 | ||
|
|
18e4b9da65 | ||
|
|
d42aa545bb | ||
|
|
69b81c00ca | ||
|
|
068ac4899d | ||
|
|
f8d936a834 | ||
|
|
5c21c34568 | ||
|
|
0eea2e0bbc | ||
|
|
61acc91200 | ||
|
|
a52982d2bf | ||
|
|
1d40b134b6 | ||
|
|
735c8236e5 | ||
|
|
dc5a3236b3 | ||
|
|
60e1eeba8e | ||
|
|
1fc6056c51 | ||
|
|
b9df97e2bc | ||
|
|
b985619d16 | ||
|
|
3e31bbef97 | ||
|
|
3e5ada8f5f | ||
|
|
2b2c74da9c | ||
|
|
450cab428f | ||
|
|
0340fe18fa |
@@ -5,20 +5,20 @@ executors:
|
||||
- image: mcr.microsoft.com/playwright:v1.39.0-focal
|
||||
environment:
|
||||
NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed
|
||||
PERCY_POSTINSTALL_BROWSER: 'true' # Needed to store the percy browser in cache deps
|
||||
PERCY_LOGLEVEL: 'debug' # Enable DEBUG level logging for Percy (Issue: https://github.com/nasa/openmct/issues/5742)
|
||||
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)
|
||||
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!'
|
||||
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. Will use cache if found'
|
||||
description: "All steps used to build and install. Will use cache if found"
|
||||
parameters:
|
||||
node-version:
|
||||
type: string
|
||||
@@ -30,7 +30,7 @@ commands:
|
||||
node-version: << parameters.node-version >>
|
||||
- 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'
|
||||
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
|
||||
@@ -42,7 +42,7 @@ commands:
|
||||
- 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.'
|
||||
description: "Custom command for saving cache."
|
||||
parameters:
|
||||
node-version:
|
||||
type: string
|
||||
@@ -53,7 +53,7 @@ commands:
|
||||
- ~/.npm
|
||||
- node_modules
|
||||
generate_and_store_version_and_filesystem_artifacts:
|
||||
description: 'Track important packages and files'
|
||||
description: "Track important packages and files"
|
||||
steps:
|
||||
- run: |
|
||||
[[ $EUID -ne 0 ]] && (sudo mkdir -p /tmp/artifacts && sudo chmod 777 /tmp/artifacts) || (mkdir -p /tmp/artifacts && chmod 777 /tmp/artifacts)
|
||||
@@ -64,7 +64,7 @@ commands:
|
||||
- store_artifacts:
|
||||
path: /tmp/artifacts/
|
||||
generate_e2e_code_cov_report:
|
||||
description: 'Generate e2e code coverage artifacts and publish to codecov.io. Needed to that we can ignore the exit code status of the npm run test'
|
||||
description: "Generate e2e code coverage artifacts and publish to codecov.io. Needed to that we can ignore the exit code status of the npm run test"
|
||||
parameters:
|
||||
suite:
|
||||
type: string
|
||||
@@ -105,7 +105,11 @@ jobs:
|
||||
node-version: <<parameters.node-version>>
|
||||
- browser-tools/install-chrome:
|
||||
replace-existing: false
|
||||
- run: npm run test
|
||||
- run:
|
||||
command: |
|
||||
mkdir -p dist/reports/tests/
|
||||
TESTFILES=$(circleci tests glob "src/**/*Spec.js")
|
||||
echo "$TESTFILES" | circleci tests run --command="xargs npm run test" --verbose
|
||||
- run: npm run cov:unit:publish
|
||||
- save_cache_cmd:
|
||||
node-version: <<parameters.node-version>>
|
||||
@@ -123,16 +127,20 @@ jobs:
|
||||
suite: #stable or full
|
||||
type: string
|
||||
executor: pw-focal-development
|
||||
parallelism: 6
|
||||
parallelism: 7
|
||||
steps:
|
||||
- build_and_install:
|
||||
node-version: lts/hydrogen
|
||||
- when: #Only install chrome-beta when running the 'full' suite to save $$$
|
||||
condition:
|
||||
equal: ['full', <<parameters.suite>>]
|
||||
equal: ["full", <<parameters.suite>>]
|
||||
steps:
|
||||
- run: npx playwright install chrome-beta
|
||||
- run: SHARD="$((${CIRCLE_NODE_INDEX}+1))"; npm run test:e2e:<<parameters.suite>> -- --shard=${SHARD}/${CIRCLE_NODE_TOTAL}
|
||||
- run:
|
||||
command: |
|
||||
mkdir test-results
|
||||
TESTFILES=$(circleci tests glob "e2e/**/*.spec.js")
|
||||
echo "$TESTFILES" | circleci tests run --command="xargs npm run test:e2e:<<parameters.suite>>" --verbose --split-by=timings
|
||||
- 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
|
||||
@@ -152,6 +160,31 @@ 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:
|
||||
@@ -239,6 +272,7 @@ 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
|
||||
|
||||
workflows:
|
||||
overall-circleci-commit-status: #These jobs run on every commit
|
||||
jobs:
|
||||
@@ -251,10 +285,9 @@ workflows:
|
||||
- e2e-test:
|
||||
name: e2e-stable
|
||||
suite: stable
|
||||
- mem-test
|
||||
- perf-test
|
||||
- e2e-mobile
|
||||
- visual-a11y-tests:
|
||||
name: visual-test-ci
|
||||
name: visual-a11y-test-ci
|
||||
suite: ci
|
||||
|
||||
the-nightly: #These jobs do not run on PRs, but against master at night
|
||||
@@ -270,15 +303,16 @@ workflows:
|
||||
- e2e-test:
|
||||
name: e2e-full-nightly
|
||||
suite: full
|
||||
- mem-test
|
||||
- e2e-mobile
|
||||
- perf-test
|
||||
- mem-test
|
||||
- visual-a11y-tests:
|
||||
name: visual-test-nightly
|
||||
name: visual-a11y-test-nightly
|
||||
suite: full
|
||||
- e2e-couchdb
|
||||
triggers:
|
||||
- schedule:
|
||||
cron: '0 0 * * *'
|
||||
cron: "0 0 * * *"
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
|
||||
@@ -492,9 +492,14 @@
|
||||
"gcov",
|
||||
"WCAG",
|
||||
"stackedplot",
|
||||
"Andale"
|
||||
"Andale",
|
||||
"unnormalized",
|
||||
"checksnapshots",
|
||||
"specced",
|
||||
"composables",
|
||||
"countup"
|
||||
],
|
||||
"dictionaries": ["npm", "softwareTerms", "node", "html", "css", "bash", "en_US"],
|
||||
"dictionaries": ["npm", "softwareTerms", "node", "html", "css", "bash", "en_US", "en-gb", "misc"],
|
||||
"ignorePaths": [
|
||||
"package.json",
|
||||
"dist/**",
|
||||
|
||||
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -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 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.
|
||||
* [ ] 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?
|
||||
|
||||
### Author Checklist
|
||||
|
||||
|
||||
5
.github/release.yml
vendored
5
.github/release.yml
vendored
@@ -1,5 +1,8 @@
|
||||
changelog:
|
||||
categories:
|
||||
- title: 💥 Notable Changes
|
||||
labels:
|
||||
- notable_change
|
||||
- title: 🏕 Features
|
||||
labels:
|
||||
- type:feature
|
||||
@@ -20,4 +23,4 @@ changelog:
|
||||
- dependencies
|
||||
- title: 🐛 Bug Fixes
|
||||
labels:
|
||||
- '*'
|
||||
- "*"
|
||||
|
||||
61
.github/workflows/e2e-flakefinder.yml
vendored
Normal file
61
.github/workflows/e2e-flakefinder.yml
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
name: 'pr:e2e:flakefinder'
|
||||
|
||||
on:
|
||||
# push:
|
||||
# branches: master
|
||||
workflow_dispatch:
|
||||
# pull_request:
|
||||
# types:
|
||||
# - labeled
|
||||
# - opened
|
||||
# schedule:
|
||||
# - cron: '0 0 * * *'
|
||||
|
||||
jobs:
|
||||
e2e-flakefinder:
|
||||
if: contains(github.event.pull_request.labels.*.name, 'pr:e2e:flakefinder') || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || github.event.action == 'opened'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 'lts/hydrogen'
|
||||
|
||||
- name: Cache NPM dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
|
||||
- run: npx playwright@1.39.0 install
|
||||
- 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
|
||||
|
||||
- name: Archive test results
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: test-results
|
||||
|
||||
- name: Remove pr:e2e:flakefinder label (if present)
|
||||
if: always()
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const { owner, repo, number } = context.issue;
|
||||
const labelToRemove = 'pr:e2e:flakefinder';
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: number,
|
||||
name: labelToRemove
|
||||
});
|
||||
} catch (error) {
|
||||
core.warning(`Failed to remove ' + labelToRemove + ' label: ${error.message}`);
|
||||
}
|
||||
58
.github/workflows/e2e-perf.yml
vendored
Normal file
58
.github/workflows/e2e-perf.yml
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
name: 'e2e-perf'
|
||||
on:
|
||||
push:
|
||||
branches: master
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
types:
|
||||
- labeled
|
||||
- opened
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
jobs:
|
||||
e2e-full:
|
||||
if: contains(github.event.pull_request.labels.*.name, 'pr:e2e:perf') || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 'lts/hydrogen'
|
||||
|
||||
- name: Cache NPM dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
|
||||
- run: npx playwright@1.39.0 install
|
||||
- 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
|
||||
- name: Archive test results
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: test-results
|
||||
|
||||
- name: Remove pr:e2e:perf label (if present)
|
||||
if: always()
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const { owner, repo, number } = context.issue;
|
||||
const labelToRemove = 'pr:e2e:perf';
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: number,
|
||||
name: labelToRemove
|
||||
});
|
||||
} catch (error) {
|
||||
core.warning(`Failed to remove ' + labelToRemove + ' label: ${error.message}`);
|
||||
}
|
||||
13
.vscode/extensions.json
vendored
Normal file
13
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
|
||||
// Extension identifier format: ${publisher}.${name}. Example: vscode.csharp
|
||||
|
||||
// List of extensions which should be recommended for users of this workspace.
|
||||
"recommendations": [
|
||||
"Vue.volar",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"rvest.vs-code-prettier-eslint"
|
||||
],
|
||||
// List of extensions recommended by VS Code that should not be recommended for users of this workspace.
|
||||
"unwantedRecommendations": ["octref.vetur"]
|
||||
}
|
||||
30
docs/src/process/release.md
Normal file
30
docs/src/process/release.md
Normal file
@@ -0,0 +1,30 @@
|
||||
|
||||
# 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.
|
||||
@@ -109,7 +109,7 @@ For those interested in the mechanics of snapshot testing with Playwright, you c
|
||||
// from our package.json or circleCI configuration file
|
||||
docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v{X.X.X}-focal /bin/bash
|
||||
npm install
|
||||
npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @snapshot
|
||||
npm run test:e2e:checksnapshots
|
||||
```
|
||||
|
||||
### Updating Snapshots
|
||||
@@ -134,6 +134,12 @@ npm install
|
||||
npm run test:e2e:updatesnapshots
|
||||
```
|
||||
|
||||
Once that's done, you'll need to run the following to verify that the changes do not cause more problems:
|
||||
|
||||
```sh
|
||||
npm run test:e2e:checksnapshots
|
||||
```
|
||||
|
||||
## Automated Accessibility (a11y) Testing
|
||||
|
||||
Open MCT incorporates accessibility testing through two primary methods to ensure its compliance with accessibility standards:
|
||||
@@ -223,7 +229,7 @@ Current list of test tags:
|
||||
|
||||
|Test Tag|Description|
|
||||
|:-:|-|
|
||||
|`@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).|
|
||||
|`@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).|
|
||||
|`@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`.|
|
||||
@@ -232,6 +238,7 @@ 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
|
||||
|
||||
@@ -323,9 +330,15 @@ In terms of operating system testing, we're only limited by what the CI provider
|
||||
|
||||
#### **Mobile**
|
||||
|
||||
We have the Mission-need to support iPad. To run our iPad suite, please see our `playwright-*.config.js` with the 'iPad' project.
|
||||
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.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
#### **Skipping or executing tests based on browser, os, and/os browser version:**
|
||||
|
||||
@@ -435,6 +448,7 @@ 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')`
|
||||
|
||||
@@ -505,15 +505,14 @@ async function setTimeConductorBounds(page, startDate, endDate) {
|
||||
* @param {string} startDate
|
||||
* @param {string} endDate
|
||||
*/
|
||||
async function setIndependentTimeConductorBounds(page, startDate, endDate) {
|
||||
// Activate Independent Time Conductor in Fixed Time Mode
|
||||
await page.getByRole('switch').click();
|
||||
async function setIndependentTimeConductorBounds(page, { start, end }) {
|
||||
// Activate Independent Time Conductor
|
||||
await page.getByLabel('Enable Independent Time Conductor').click();
|
||||
|
||||
// Bring up the time conductor popup
|
||||
await page.click('.c-conductor-holder--compact .c-compact-tc');
|
||||
await page.getByLabel('Independent Time Conductor Settings').click();
|
||||
await expect(page.locator('.itc-popout')).toBeInViewport();
|
||||
|
||||
await setTimeBounds(page, startDate, endDate);
|
||||
await setTimeBounds(page, start, end);
|
||||
|
||||
await page.keyboard.press('Enter');
|
||||
}
|
||||
|
||||
@@ -61,7 +61,6 @@ export async function scanForA11yViolations(page, testCaseName, options = {}) {
|
||||
const builder = new AxeBuilder({ page });
|
||||
builder.withTags(['wcag2aa']);
|
||||
// https://github.com/dequelabs/axe-core/blob/develop/doc/rule-descriptions.md
|
||||
builder.disableRules(['color-contrast']);
|
||||
const accessibilityScanResults = await builder.analyze();
|
||||
|
||||
// Assert that no violations should be present
|
||||
|
||||
@@ -21,10 +21,12 @@
|
||||
*****************************************************************************/
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
import { expect } from '../pluginFixtures.js';
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function navigateToFaultManagementWithExample(page) {
|
||||
export async function navigateToFaultManagementWithExample(page) {
|
||||
await page.addInitScript({
|
||||
path: fileURLToPath(new URL('./addInitExampleFaultProvider.js', import.meta.url))
|
||||
});
|
||||
@@ -35,7 +37,7 @@ async function navigateToFaultManagementWithExample(page) {
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function navigateToFaultManagementWithStaticExample(page) {
|
||||
export async function navigateToFaultManagementWithStaticExample(page) {
|
||||
await page.addInitScript({
|
||||
path: fileURLToPath(new URL('./addInitExampleFaultProviderStatic.js', import.meta.url))
|
||||
});
|
||||
@@ -46,7 +48,7 @@ async function navigateToFaultManagementWithStaticExample(page) {
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function navigateToFaultManagementWithoutExample(page) {
|
||||
export async function navigateToFaultManagementWithoutExample(page) {
|
||||
await page.addInitScript({
|
||||
path: fileURLToPath(new URL('./addInitFaultManagementPlugin.js', import.meta.url))
|
||||
});
|
||||
@@ -57,7 +59,7 @@ async function navigateToFaultManagementWithoutExample(page) {
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function navigateToFaultItemInTree(page) {
|
||||
export async function navigateToFaultItemInTree(page) {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
const faultManagementTreeItem = page
|
||||
@@ -75,88 +77,95 @@ async function navigateToFaultItemInTree(page) {
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function acknowledgeFault(page, rowNumber) {
|
||||
export async function acknowledgeFault(page, rowNumber) {
|
||||
await openFaultRowMenu(page, rowNumber);
|
||||
await page.locator('.c-menu >> text="Acknowledge"').click();
|
||||
// Click [aria-label="Save"]
|
||||
await page.locator('[aria-label="Save"]').click();
|
||||
await page.getByLabel('Acknowledge', { exact: true }).click();
|
||||
await page.getByLabel('Save').click();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function shelveMultipleFaults(page, ...nums) {
|
||||
export async function shelveMultipleFaults(page, ...nums) {
|
||||
const selectRows = nums.map((num) => {
|
||||
return selectFaultItem(page, num);
|
||||
});
|
||||
await Promise.all(selectRows);
|
||||
|
||||
await page.locator('button:has-text("Shelve")').click();
|
||||
await page.locator('[aria-label="Save"]').click();
|
||||
await page.getByLabel('Shelve selected faults').click();
|
||||
await page.getByLabel('Save').click();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function acknowledgeMultipleFaults(page, ...nums) {
|
||||
export 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.locator('[aria-label="Save"]').click();
|
||||
await page.getByLabel('Save').click();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function shelveFault(page, rowNumber) {
|
||||
export async function shelveFault(page, rowNumber) {
|
||||
await openFaultRowMenu(page, rowNumber);
|
||||
await page.locator('.c-menu >> text="Shelve"').click();
|
||||
// Click [aria-label="Save"]
|
||||
await page.locator('[aria-label="Save"]').click();
|
||||
await page.getByLabel('Save').click();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function changeViewTo(page, view) {
|
||||
export async function changeViewTo(page, view) {
|
||||
await page.locator('.c-fault-mgmt__search-row select').first().selectOption(view);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function sortFaultsBy(page, sort) {
|
||||
export async function sortFaultsBy(page, sort) {
|
||||
await page.locator('.c-fault-mgmt__list-header-sortButton select').selectOption(sort);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function enterSearchTerm(page, term) {
|
||||
export async function enterSearchTerm(page, term) {
|
||||
await page.locator('.c-fault-mgmt-search [aria-label="Search Input"]').fill(term);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function clearSearch(page) {
|
||||
export async function clearSearch(page) {
|
||||
await enterSearchTerm(page, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function selectFaultItem(page, rowNumber) {
|
||||
await page.locator(`.c-fault-mgmt-item > input >> nth=${rowNumber - 1}`).check();
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function getHighestSeverity(page) {
|
||||
export async function getHighestSeverity(page) {
|
||||
const criticalCount = await page.locator('[title=CRITICAL]').count();
|
||||
const warningCount = await page.locator('[title=WARNING]').count();
|
||||
|
||||
@@ -172,7 +181,7 @@ async function getHighestSeverity(page) {
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function getLowestSeverity(page) {
|
||||
export async function getLowestSeverity(page) {
|
||||
const warningCount = await page.locator('[title=WARNING]').count();
|
||||
const watchCount = await page.locator('[title=WATCH]').count();
|
||||
|
||||
@@ -188,7 +197,7 @@ async function getLowestSeverity(page) {
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function getFaultResultCount(page) {
|
||||
export async function getFaultResultCount(page) {
|
||||
const count = await page.locator('.c-faults-list-view-item-body > .c-fault-mgmt__list').count();
|
||||
|
||||
return count;
|
||||
@@ -197,7 +206,7 @@ async function getFaultResultCount(page) {
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
function getFault(page, rowNumber) {
|
||||
export function getFault(page, rowNumber) {
|
||||
const fault = page.locator(
|
||||
`.c-faults-list-view-item-body > .c-fault-mgmt__list >> nth=${rowNumber - 1}`
|
||||
);
|
||||
@@ -208,7 +217,7 @@ function getFault(page, rowNumber) {
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
function getFaultByName(page, name) {
|
||||
export function getFaultByName(page, name) {
|
||||
const fault = page.locator(`.c-fault-mgmt__list-faultname:has-text("${name}")`);
|
||||
|
||||
return fault;
|
||||
@@ -217,7 +226,7 @@ function getFaultByName(page, name) {
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function getFaultName(page, rowNumber) {
|
||||
export async function getFaultName(page, rowNumber) {
|
||||
const faultName = await page
|
||||
.locator(`.c-fault-mgmt__list-faultname >> nth=${rowNumber - 1}`)
|
||||
.textContent();
|
||||
@@ -228,7 +237,7 @@ async function getFaultName(page, rowNumber) {
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function getFaultSeverity(page, rowNumber) {
|
||||
export async function getFaultSeverity(page, rowNumber) {
|
||||
const faultSeverity = await page
|
||||
.locator(`.c-faults-list-view-item-body .c-fault-mgmt__list-severity >> nth=${rowNumber - 1}`)
|
||||
.getAttribute('title');
|
||||
@@ -239,7 +248,7 @@ async function getFaultSeverity(page, rowNumber) {
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function getFaultNamespace(page, rowNumber) {
|
||||
export async function getFaultNamespace(page, rowNumber) {
|
||||
const faultNamespace = await page
|
||||
.locator(`.c-fault-mgmt__list-path >> nth=${rowNumber - 1}`)
|
||||
.textContent();
|
||||
@@ -250,7 +259,7 @@ async function getFaultNamespace(page, rowNumber) {
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function getFaultTriggerTime(page, rowNumber) {
|
||||
export async function getFaultTriggerTime(page, rowNumber) {
|
||||
const faultTriggerTime = await page
|
||||
.locator(`.c-fault-mgmt__list-trigTime >> nth=${rowNumber - 1} >> .c-fault-mgmt-item__value`)
|
||||
.textContent();
|
||||
@@ -261,35 +270,10 @@ async function getFaultTriggerTime(page, rowNumber) {
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function openFaultRowMenu(page, rowNumber) {
|
||||
export async function openFaultRowMenu(page, rowNumber) {
|
||||
// select
|
||||
await page
|
||||
.locator(`.c-fault-mgmt-item > .c-fault-mgmt__list-action-button >> nth=${rowNumber - 1}`)
|
||||
.getByLabel('Disposition actions')
|
||||
.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
|
||||
};
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import { createDomainObjectWithDefaults, createPlanFromJSON } from '../appActions.js';
|
||||
import { expect } from '../pluginFixtures.js';
|
||||
|
||||
/**
|
||||
@@ -142,6 +143,18 @@ 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
|
||||
@@ -172,3 +185,55 @@ 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' });
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ 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: {
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
// 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: {
|
||||
@@ -35,7 +33,6 @@ const config = {
|
||||
},
|
||||
{
|
||||
name: 'MMOC',
|
||||
testMatch: '**/*.e2e.spec.js', // only run e2e tests
|
||||
grepInvert: /@snapshot/,
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
@@ -47,8 +44,6 @@ 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'
|
||||
@@ -56,7 +51,6 @@ const config = {
|
||||
},
|
||||
{
|
||||
name: 'firefox',
|
||||
testMatch: '**/*.e2e.spec.js', // only run e2e tests
|
||||
grepInvert: /@snapshot/,
|
||||
use: {
|
||||
browserName: 'firefox'
|
||||
@@ -64,7 +58,6 @@ const config = {
|
||||
},
|
||||
{
|
||||
name: 'canary',
|
||||
testMatch: '**/*.e2e.spec.js', // only run e2e tests
|
||||
grepInvert: /@snapshot/,
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
@@ -73,22 +66,11 @@ 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: [
|
||||
|
||||
69
e2e/playwright-mobile.config.js
Normal file
69
e2e/playwright-mobile.config.js
Normal file
@@ -0,0 +1,69 @@
|
||||
// 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;
|
||||
@@ -1,6 +1,9 @@
|
||||
// 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
|
||||
@@ -28,6 +31,28 @@ 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: [
|
||||
|
||||
26
e2e/test-data/display_layout_with_child_overlay_plot.json
Normal file
26
e2e/test-data/display_layout_with_child_overlay_plot.json
Normal file
File diff suppressed because one or more lines are too long
@@ -6,7 +6,8 @@
|
||||
"end": 1660343797000,
|
||||
"type": "Group 1",
|
||||
"color": "orange",
|
||||
"textColor": "white"
|
||||
"textColor": "white",
|
||||
"id": 1
|
||||
},
|
||||
{
|
||||
"name": "Past event 2",
|
||||
@@ -14,7 +15,8 @@
|
||||
"end": 1660429160000,
|
||||
"type": "Group 1",
|
||||
"color": "orange",
|
||||
"textColor": "white"
|
||||
"textColor": "white",
|
||||
"id": 2
|
||||
},
|
||||
{
|
||||
"name": "Past event 3",
|
||||
@@ -22,7 +24,8 @@
|
||||
"end": 1660503981000,
|
||||
"type": "Group 1",
|
||||
"color": "orange",
|
||||
"textColor": "white"
|
||||
"textColor": "white",
|
||||
"id": 3
|
||||
},
|
||||
{
|
||||
"name": "Past event 4",
|
||||
@@ -30,7 +33,8 @@
|
||||
"end": 1660624108000,
|
||||
"type": "Group 1",
|
||||
"color": "orange",
|
||||
"textColor": "white"
|
||||
"textColor": "white",
|
||||
"id": 4
|
||||
},
|
||||
{
|
||||
"name": "Past event 5",
|
||||
@@ -38,7 +42,8 @@
|
||||
"end": 1660681529000,
|
||||
"type": "Group 1",
|
||||
"color": "orange",
|
||||
"textColor": "white"
|
||||
"textColor": "white",
|
||||
"id": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"end": 1660343797000,
|
||||
"type": "Group 1",
|
||||
"color": "orange",
|
||||
"textColor": "white"
|
||||
"textColor": "white",
|
||||
"id": 1
|
||||
},
|
||||
{
|
||||
"name": "Time until supper",
|
||||
@@ -14,7 +15,8 @@
|
||||
"end": 1650420410000,
|
||||
"type": "Group 2",
|
||||
"color": "blue",
|
||||
"textColor": "white"
|
||||
"textColor": "white",
|
||||
"id": 2
|
||||
}
|
||||
],
|
||||
"Group 2": [
|
||||
@@ -24,7 +26,8 @@
|
||||
"end": 1650320102001,
|
||||
"type": "Group 2",
|
||||
"color": "green",
|
||||
"textColor": "white"
|
||||
"textColor": "white",
|
||||
"id": 3
|
||||
},
|
||||
{
|
||||
"name": "Time since last accident",
|
||||
@@ -32,7 +35,8 @@
|
||||
"end": 1650320102002,
|
||||
"type": "Group 1",
|
||||
"color": "yellow",
|
||||
"textColor": "white"
|
||||
"textColor": "white",
|
||||
"id": 4
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -174,6 +174,6 @@ test.describe('AppActions', () => {
|
||||
type: 'Folder'
|
||||
});
|
||||
await openObjectTreeContextMenu(page, folder.url);
|
||||
await expect(page.getByLabel('Menu')).toBeVisible();
|
||||
await expect(page.getByLabel(`${folder.name} Context Menu`)).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33,13 +33,18 @@
|
||||
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
import { createDomainObjectWithDefaults, createExampleTelemetryObject } from '../../appActions.js';
|
||||
import {
|
||||
createDomainObjectWithDefaults,
|
||||
createExampleTelemetryObject,
|
||||
setIndependentTimeConductorBounds,
|
||||
setTimeConductorBounds
|
||||
} 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', () => {
|
||||
test.describe('Generate Visual Test Data @localStorage @generatedata @clock', () => {
|
||||
test.use({
|
||||
clockOptions: {
|
||||
now: MISSION_TIME,
|
||||
@@ -89,6 +94,53 @@ test.describe('Generate Visual Test Data @localStorage @generatedata', () => {
|
||||
});
|
||||
});
|
||||
|
||||
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, {
|
||||
|
||||
127
e2e/tests/functional/missionStatus.e2e.spec.js
Normal file
127
e2e/tests/functional/missionStatus.e2e.spec.js
Normal file
@@ -0,0 +1,127 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
This test suite is dedicated to tests which verify persistability checks
|
||||
*/
|
||||
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
import { expect, test } from '../../baseFixtures.js';
|
||||
|
||||
test.describe('Mission Status @addInit', () => {
|
||||
const NO_GO = '0';
|
||||
const GO = '1';
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// FIXME: determine if plugins will be added to index.html or need to be injected
|
||||
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();
|
||||
// Description should be empty https://github.com/nasa/openmct/issues/6978
|
||||
await expect(page.getByLabel('Dialog message')).toBeHidden();
|
||||
// set role
|
||||
await page.getByRole('button', { name: 'Select', exact: true }).click();
|
||||
// dismiss role confirmation popup
|
||||
await page.getByRole('button', { name: 'Dismiss' }).click();
|
||||
});
|
||||
|
||||
test('Basic functionality', async ({ page }) => {
|
||||
const imageryStatusSelect = page.getByRole('combobox', { name: 'Imagery' });
|
||||
const commandingStatusSelect = page.getByRole('combobox', { name: 'Commanding' });
|
||||
const drivingStatusSelect = page.getByRole('combobox', { name: 'Driving' });
|
||||
const missionStatusPanel = page.getByRole('dialog', { name: 'User Control Panel' });
|
||||
|
||||
await test.step('Mission status panel shows/hides when toggled', async () => {
|
||||
// Ensure that clicking the button toggles the dialog
|
||||
await page.getByLabel('Toggle Mission Status Panel').click();
|
||||
await expect(missionStatusPanel).toBeVisible();
|
||||
await page.getByLabel('Toggle Mission Status Panel').click();
|
||||
await expect(missionStatusPanel).toBeHidden();
|
||||
await page.getByLabel('Toggle Mission Status Panel').click();
|
||||
await expect(missionStatusPanel).toBeVisible();
|
||||
|
||||
// Ensure that clicking the close button closes the dialog
|
||||
await page.getByLabel('Close Mission Status Panel').click();
|
||||
await expect(missionStatusPanel).toBeHidden();
|
||||
await page.getByLabel('Toggle Mission Status Panel').click();
|
||||
await expect(missionStatusPanel).toBeVisible();
|
||||
|
||||
// Ensure clicking off the dialog also closes it
|
||||
await page.getByLabel('My Items Grid View').click();
|
||||
await expect(missionStatusPanel).toBeHidden();
|
||||
await page.getByLabel('Toggle Mission Status Panel').click();
|
||||
await expect(missionStatusPanel).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Mission action statuses have correct defaults and can be set', async () => {
|
||||
await expect(imageryStatusSelect).toHaveValue(NO_GO);
|
||||
await expect(commandingStatusSelect).toHaveValue(NO_GO);
|
||||
await expect(drivingStatusSelect).toHaveValue(NO_GO);
|
||||
|
||||
await setMissionStatus(page, 'Imagery', GO);
|
||||
await expect(imageryStatusSelect).toHaveValue(GO);
|
||||
await expect(commandingStatusSelect).toHaveValue(NO_GO);
|
||||
await expect(drivingStatusSelect).toHaveValue(NO_GO);
|
||||
|
||||
await setMissionStatus(page, 'Commanding', GO);
|
||||
await expect(imageryStatusSelect).toHaveValue(GO);
|
||||
await expect(commandingStatusSelect).toHaveValue(GO);
|
||||
await expect(drivingStatusSelect).toHaveValue(NO_GO);
|
||||
|
||||
await setMissionStatus(page, 'Driving', GO);
|
||||
await expect(imageryStatusSelect).toHaveValue(GO);
|
||||
await expect(commandingStatusSelect).toHaveValue(GO);
|
||||
await expect(drivingStatusSelect).toHaveValue(GO);
|
||||
|
||||
await setMissionStatus(page, 'Imagery', NO_GO);
|
||||
await expect(imageryStatusSelect).toHaveValue(NO_GO);
|
||||
await expect(commandingStatusSelect).toHaveValue(GO);
|
||||
await expect(drivingStatusSelect).toHaveValue(GO);
|
||||
|
||||
await setMissionStatus(page, 'Commanding', NO_GO);
|
||||
await expect(imageryStatusSelect).toHaveValue(NO_GO);
|
||||
await expect(commandingStatusSelect).toHaveValue(NO_GO);
|
||||
await expect(drivingStatusSelect).toHaveValue(GO);
|
||||
|
||||
await setMissionStatus(page, 'Driving', NO_GO);
|
||||
await expect(imageryStatusSelect).toHaveValue(NO_GO);
|
||||
await expect(commandingStatusSelect).toHaveValue(NO_GO);
|
||||
await expect(drivingStatusSelect).toHaveValue(NO_GO);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {'Commanding'|'Imagery'|'Driving'} action
|
||||
* @param {'0'|'1'} status
|
||||
*/
|
||||
async function setMissionStatus(page, action, status) {
|
||||
await page.getByRole('combobox', { name: action }).selectOption(status);
|
||||
await expect(
|
||||
page.getByRole('alert').filter({ hasText: 'Successfully set mission status' })
|
||||
).toBeVisible();
|
||||
await page.getByLabel('Dismiss').click();
|
||||
}
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
assertPlanActivities,
|
||||
assertPlanOrderedSwimLanes
|
||||
} from '../../../helper/planningUtils.js';
|
||||
import { test } from '../../../pluginFixtures.js';
|
||||
import { expect, test } from '../../../pluginFixtures.js';
|
||||
|
||||
const testPlan1 = JSON.parse(
|
||||
fs.readFileSync(
|
||||
@@ -63,4 +63,47 @@ test.describe('Plan', () => {
|
||||
});
|
||||
await assertPlanOrderedSwimLanes(page, testPlanWithOrderedLanes, planWithSwimLanes.url);
|
||||
});
|
||||
|
||||
test('Allows setting the state of an activity when selected.', async ({ page }) => {
|
||||
const groups = Object.keys(testPlan1);
|
||||
const firstGroupKey = groups[0];
|
||||
const firstGroupItems = testPlan1[firstGroupKey];
|
||||
const firstActivity = firstGroupItems[0];
|
||||
const lastActivity = firstGroupItems[firstGroupItems.length - 1];
|
||||
const startBound = firstActivity.start;
|
||||
// Set the endBound to the end time of the current activity
|
||||
let endBound = lastActivity.end;
|
||||
// eslint-disable-next-line playwright/no-conditional-in-test
|
||||
if (endBound === startBound) {
|
||||
// Prevent oddities with setting start and end bound equal
|
||||
// via URL params
|
||||
endBound += 1;
|
||||
}
|
||||
|
||||
// Switch to fixed time mode with all plan events within the bounds
|
||||
await page.goto(
|
||||
`${plan.url}?tc.mode=fixed&tc.startBound=${startBound}&tc.endBound=${endBound}&tc.timeSystem=utc&view=plan.view`
|
||||
);
|
||||
|
||||
// select the first activity in the list
|
||||
await page.getByText('Past event 1').click();
|
||||
|
||||
// Find the activity state section in the inspector
|
||||
await page.getByRole('tab', { name: 'Activity' }).click();
|
||||
|
||||
// Check that activity state dropdown selection shows the `set status` option by default
|
||||
await expect(page.getByLabel('Activity Status').locator("[aria-selected='true']")).toHaveText(
|
||||
'Not started'
|
||||
);
|
||||
|
||||
// Change the selection of the activity status
|
||||
await page.getByRole('combobox').selectOption({ label: 'Aborted' });
|
||||
// select a different activity and back to the previous one
|
||||
await page.getByText('Past event 2').click();
|
||||
await page.getByText('Past event 1').click();
|
||||
// Check that activity state dropdown selection shows the previously selected option by default
|
||||
await expect(page.getByLabel('Activity Status').locator("[aria-selected='true']")).toHaveText(
|
||||
'Aborted'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,71 +22,15 @@
|
||||
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(
|
||||
const examplePlanSmall1 = JSON.parse(
|
||||
fs.readFileSync(
|
||||
new URL('../../../test-data/examplePlans/ExamplePlan_Small3.json', import.meta.url)
|
||||
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 = 4;
|
||||
|
||||
const testPlan = {
|
||||
TEST_GROUP: [
|
||||
{
|
||||
name: 'Past event 1',
|
||||
start: 1660320408000,
|
||||
end: 1660343797000,
|
||||
type: 'TEST-GROUP',
|
||||
color: 'orange',
|
||||
textColor: 'white'
|
||||
},
|
||||
{
|
||||
name: 'Past event 2',
|
||||
start: 1660406808000,
|
||||
end: 1660429160000,
|
||||
type: 'TEST-GROUP',
|
||||
color: 'orange',
|
||||
textColor: 'white'
|
||||
},
|
||||
{
|
||||
name: 'Past event 3',
|
||||
start: 1660493208000,
|
||||
end: 1660503981000,
|
||||
type: 'TEST-GROUP',
|
||||
color: 'orange',
|
||||
textColor: 'white'
|
||||
},
|
||||
{
|
||||
name: 'Past event 4',
|
||||
start: 1660579608000,
|
||||
end: 1660624108000,
|
||||
type: 'TEST-GROUP',
|
||||
color: 'orange',
|
||||
textColor: 'white'
|
||||
},
|
||||
{
|
||||
name: 'Past event 5',
|
||||
start: 1660666008000,
|
||||
end: 1660681529000,
|
||||
type: 'TEST-GROUP',
|
||||
color: 'orange',
|
||||
textColor: 'white'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
test.describe('Time List', () => {
|
||||
test('Create a Time List, add a single Plan to it and verify all the activities are displayed with no milliseconds', async ({
|
||||
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
|
||||
}) => {
|
||||
// Goto baseURL
|
||||
@@ -103,12 +47,16 @@ test.describe('Time List', () => {
|
||||
await test.step('Create a Plan and add it to the timelist', async () => {
|
||||
await createPlanFromJSON(page, {
|
||||
name: 'Test Plan',
|
||||
json: testPlan,
|
||||
json: examplePlanSmall1,
|
||||
parent: timelist.uuid
|
||||
});
|
||||
|
||||
const startBound = testPlan.TEST_GROUP[0].start;
|
||||
const endBound = testPlan.TEST_GROUP[testPlan.TEST_GROUP.length - 1].end;
|
||||
const groups = Object.keys(examplePlanSmall1);
|
||||
const firstGroupKey = groups[0];
|
||||
const firstGroupItems = examplePlanSmall1[firstGroupKey];
|
||||
const firstActivity = firstGroupItems[0];
|
||||
const lastActivity = firstGroupItems[firstGroupItems.length - 1];
|
||||
const startBound = firstActivity.start;
|
||||
const endBound = lastActivity.end;
|
||||
|
||||
// Switch to fixed time mode with all plan events within the bounds
|
||||
await page.goto(
|
||||
@@ -118,7 +66,7 @@ test.describe('Time List', () => {
|
||||
// Verify all events are displayed
|
||||
const eventCount = await page.getByRole('row').count();
|
||||
// subtracting one for the header
|
||||
await expect(eventCount - 1).toEqual(testPlan.TEST_GROUP.length);
|
||||
await expect(eventCount - 1).toEqual(firstGroupItems.length);
|
||||
});
|
||||
|
||||
await test.step('Does not show milliseconds in times', async () => {
|
||||
@@ -131,164 +79,86 @@ test.describe('Time List', () => {
|
||||
await expect(row.locator('.--end')).not.toContainText('.');
|
||||
await expect(row.locator('.--duration')).not.toContainText('.');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 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})/;
|
||||
await test.step('Shows activity properties when a row is selected', async () => {
|
||||
await page.getByRole('row').nth(2).click();
|
||||
|
||||
/**
|
||||
* @typedef {Object} CountdownObject
|
||||
* @property {string} sign - The sign of the countdown ('-' if the countdown is negative, otherwise undefined).
|
||||
* @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`
|
||||
// Find the activity state section in the inspector
|
||||
await page.getByRole('tab', { name: 'Activity' }).click();
|
||||
// Check that activity state label is displayed in the inspector.
|
||||
await expect(page.getByLabel('Activity Status').locator("[aria-selected='true']")).toHaveText(
|
||||
'Not started'
|
||||
);
|
||||
});
|
||||
|
||||
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 getAndAssertCountdownObject(page, i + 3);
|
||||
// Wait until it changes
|
||||
await expect(countdownCell).not.toHaveText(beforeCountdown.toString());
|
||||
// Get the new countdown timestamp object
|
||||
const afterCountdown = await getAndAssertCountdownObject(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 countdownCell = countUpCells[i];
|
||||
// Get the initial count-up timestamp object
|
||||
const beforeCountdown = await getAndAssertCountdownObject(page, i + 1);
|
||||
// Wait until it changes
|
||||
await expect(countdownCell).not.toHaveText(beforeCountdown.toString());
|
||||
// Get the new count-up timestamp object
|
||||
const afterCountdown = await getAndAssertCountdownObject(page, i + 1);
|
||||
// Verify that the new count-up timestamp object is greater than the old one
|
||||
expect(Number(afterCountdown.seconds)).toBeGreaterThan(Number(beforeCountdown.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);
|
||||
}
|
||||
test("View a timelist in expanded view, verify all the activities are displayed and selecting an activity shows it's properties", async ({
|
||||
page
|
||||
}) => {
|
||||
// Goto baseURL
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
const timelist = await test.step('Create a Time List', async () => {
|
||||
const createdTimeList = await createDomainObjectWithDefaults(page, { type: 'Time List' });
|
||||
const objectName = await page.locator('.l-browse-bar__object-name').innerText();
|
||||
expect(objectName).toBe(createdTimeList.name);
|
||||
|
||||
/**
|
||||
* Get the text from the countdown cell in the given row, assert that it matches the countdown
|
||||
* regex, and return an object representing the countdown.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {number} rowIndex the row index
|
||||
* @returns {Promise<CountdownObject>} countdownObject
|
||||
*/
|
||||
async function getAndAssertCountdownObject(page, rowIndex) {
|
||||
const timeToFrom = await getCellTextByIndex(page, HEADER_ROW + rowIndex, TIME_TO_FROM_COLUMN);
|
||||
return createdTimeList;
|
||||
});
|
||||
|
||||
expect(timeToFrom).toMatch(COUNTDOWN_REGEXP);
|
||||
const match = timeToFrom.match(COUNTDOWN_REGEXP);
|
||||
await test.step('Create a Plan and add it to the timelist', async () => {
|
||||
await createPlanFromJSON(page, {
|
||||
name: 'Test Plan',
|
||||
json: examplePlanSmall1,
|
||||
parent: timelist.uuid
|
||||
});
|
||||
|
||||
return {
|
||||
sign: match[COUNTDOWN.SIGN],
|
||||
days: match[COUNTDOWN.DAYS],
|
||||
hours: match[COUNTDOWN.HOURS],
|
||||
minutes: match[COUNTDOWN.MINUTES],
|
||||
seconds: match[COUNTDOWN.SECONDS],
|
||||
toString: () => timeToFrom
|
||||
};
|
||||
}
|
||||
// Ensure that all activities are shown in the expanded view
|
||||
const groups = Object.keys(examplePlanSmall1);
|
||||
const firstGroupKey = groups[0];
|
||||
const firstGroupItems = examplePlanSmall1[firstGroupKey];
|
||||
const firstActivity = firstGroupItems[0];
|
||||
const lastActivity = firstGroupItems[firstGroupItems.length - 1];
|
||||
const startBound = firstActivity.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();
|
||||
|
||||
// Verify all events are displayed
|
||||
const eventCount = await page.getByRole('row').count();
|
||||
await expect(eventCount).toEqual(firstGroupItems.length);
|
||||
});
|
||||
|
||||
await test.step('Shows activity properties when a row is selected in the expanded view', async () => {
|
||||
await page.getByRole('row').nth(2).click();
|
||||
|
||||
// Find the activity state section in the inspector
|
||||
await page.getByRole('tab', { name: 'Activity' }).click();
|
||||
// Check that activity state label is displayed in the inspector.
|
||||
await expect(page.getByLabel('Activity Status').locator("[aria-selected='true']")).toHaveText(
|
||||
'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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
/*****************************************************************************
|
||||
* 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
|
||||
};
|
||||
}
|
||||
@@ -131,7 +131,10 @@ test.describe('Time Strip', () => {
|
||||
const startBoundString = new Date(startBound).toISOString().replace('T', ' ');
|
||||
const endBoundString = new Date(endBound).toISOString().replace('T', ' ');
|
||||
|
||||
await setIndependentTimeConductorBounds(page, startBoundString, endBoundString);
|
||||
await setIndependentTimeConductorBounds(page, {
|
||||
start: startBoundString,
|
||||
end: endBoundString
|
||||
});
|
||||
expect(await activityBounds.count()).toEqual(1);
|
||||
});
|
||||
|
||||
@@ -160,7 +163,10 @@ test.describe('Time Strip', () => {
|
||||
const startBoundString = new Date(startBound).toISOString().replace('T', ' ');
|
||||
const endBoundString = new Date(endBound).toISOString().replace('T', ' ');
|
||||
|
||||
await setIndependentTimeConductorBounds(page, startBoundString, endBoundString);
|
||||
await setIndependentTimeConductorBounds(page, {
|
||||
start: startBoundString,
|
||||
end: endBoundString
|
||||
});
|
||||
|
||||
// Verify that two events are displayed
|
||||
expect(await activityBounds.count()).toEqual(2);
|
||||
|
||||
@@ -35,7 +35,7 @@ import { expect, test } from '../../../../pluginFixtures.js';
|
||||
|
||||
let conditionSetUrl;
|
||||
|
||||
test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
|
||||
test.describe.serial('Condition Set CRUD Operations on @localStorage @2p', () => {
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
//TODO: This needs to be refactored
|
||||
const context = await browser.newContext();
|
||||
@@ -68,30 +68,35 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
|
||||
});
|
||||
|
||||
//Begin suite of tests again localStorage
|
||||
test('Condition set object properties persist in main view and inspector @localStorage', async ({
|
||||
page
|
||||
}) => {
|
||||
//Navigate to baseURL with injected localStorage
|
||||
await page.goto(conditionSetUrl, { waitUntil: 'networkidle' });
|
||||
test.fixme(
|
||||
'Condition set object properties persist in main view and inspector @localStorage',
|
||||
async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/7421'
|
||||
});
|
||||
//Navigate to baseURL with injected localStorage
|
||||
await page.goto(conditionSetUrl, { waitUntil: 'networkidle' });
|
||||
|
||||
//Assertions on loaded Condition Set in main view. This is a stateful transition step after page.goto()
|
||||
await expect
|
||||
.soft(page.locator('.l-browse-bar__object-name'))
|
||||
.toContainText('Unnamed Condition Set');
|
||||
//Assertions on loaded Condition Set in main view. This is a stateful transition step after page.goto()
|
||||
await expect
|
||||
.soft(page.locator('.l-browse-bar__object-name'))
|
||||
.toContainText('Unnamed Condition Set');
|
||||
|
||||
//Assertions on loaded Condition Set in Inspector
|
||||
expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy();
|
||||
//Assertions on loaded Condition Set in Inspector
|
||||
expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy();
|
||||
|
||||
//Reload Page
|
||||
await Promise.all([page.reload(), page.waitForLoadState('networkidle')]);
|
||||
//Reload Page
|
||||
await Promise.all([page.reload(), page.waitForLoadState('networkidle')]);
|
||||
|
||||
//Re-verify after reload
|
||||
await expect
|
||||
.soft(page.locator('.l-browse-bar__object-name'))
|
||||
.toContainText('Unnamed Condition Set');
|
||||
//Assertions on loaded Condition Set in Inspector
|
||||
expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy();
|
||||
});
|
||||
//Re-verify after reload
|
||||
await expect
|
||||
.soft(page.locator('.l-browse-bar__object-name'))
|
||||
.toContainText('Unnamed Condition Set');
|
||||
//Assertions on loaded Condition Set in Inspector
|
||||
expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy();
|
||||
}
|
||||
);
|
||||
test('condition set object can be modified on @localStorage', async ({ page, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
|
||||
@@ -281,7 +286,7 @@ 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.click('button[title="Change the current view"]');
|
||||
await page.getByLabel('Open the View Switcher Menu').click();
|
||||
|
||||
await expect(page.getByRole('menuitem', { name: /Lad Table/ })).toBeHidden();
|
||||
await expect(page.getByRole('menuitem', { name: /Conditions View/ })).toBeVisible();
|
||||
@@ -293,7 +298,7 @@ test.describe('Basic Condition Set Use', () => {
|
||||
}) => {
|
||||
const exampleTelemetry = await createExampleTelemetryObject(page);
|
||||
|
||||
await page.getByTitle('Show selected item in tree').click();
|
||||
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();
|
||||
@@ -373,4 +378,83 @@ 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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,6 +23,7 @@ import { fileURLToPath } from 'url';
|
||||
|
||||
import {
|
||||
createDomainObjectWithDefaults,
|
||||
navigateToObjectWithFixedTimeBounds,
|
||||
setFixedTimeMode,
|
||||
setIndependentTimeConductorBounds,
|
||||
setRealTimeMode,
|
||||
@@ -30,12 +31,120 @@ import {
|
||||
} from '../../../../appActions.js';
|
||||
import { expect, test } from '../../../../pluginFixtures.js';
|
||||
|
||||
const LOCALSTORAGE_PATH = fileURLToPath(
|
||||
const CHILD_LAYOUT_STORAGE_STATE_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';
|
||||
@@ -50,7 +159,7 @@ test.describe('Display Layout Toolbar Actions @localStorage', () => {
|
||||
await page.getByLabel('Edit Object').click();
|
||||
});
|
||||
test.use({
|
||||
storageState: LOCALSTORAGE_PATH
|
||||
storageState: CHILD_LAYOUT_STORAGE_STATE_PATH
|
||||
});
|
||||
|
||||
test('can add/remove Text element to a single layout', async ({ page }) => {
|
||||
@@ -161,6 +270,13 @@ test.describe('Display Layout', () => {
|
||||
const trimmedDisplayValue = displayLayoutValue.trim();
|
||||
|
||||
expect(trimmedDisplayValue).toBe(formattedTelemetryValue);
|
||||
|
||||
// ensure we can right click on the alpha-numeric widget and view historical data
|
||||
await page.getByLabel('Sine', { exact: true }).click({
|
||||
button: 'right'
|
||||
});
|
||||
await page.getByLabel('View Historical Data').click();
|
||||
await expect(page.getByLabel('Plot Container Style Target')).toBeVisible();
|
||||
});
|
||||
test('alpha-numeric widget telemetry value exactly matches latest telemetry value received in fixed time', async ({
|
||||
page
|
||||
@@ -329,7 +445,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, startDate, endDate);
|
||||
await setIndependentTimeConductorBounds(page, { start: startDate, end: endDate });
|
||||
|
||||
// check image date
|
||||
await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible();
|
||||
|
||||
@@ -20,25 +20,46 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import * as utils from '../../../../helper/faultUtils.js';
|
||||
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 { expect, test } from '../../../../pluginFixtures.js';
|
||||
|
||||
test.describe('The Fault Management Plugin using example faults', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await utils.navigateToFaultManagementWithExample(page);
|
||||
await navigateToFaultManagementWithExample(page);
|
||||
});
|
||||
|
||||
test('Shows a criticality icon for every fault @unstable', async ({ page }) => {
|
||||
test('Shows a criticality icon for every fault', async ({ page }) => {
|
||||
const faultCount = await page.locator('c-fault-mgmt__list').count();
|
||||
const criticalityIconCount = await page.locator('c-fault-mgmt__list-severity').count();
|
||||
|
||||
expect.soft(faultCount).toEqual(criticalityIconCount);
|
||||
expect(faultCount).toEqual(criticalityIconCount);
|
||||
});
|
||||
|
||||
test('When selecting a fault, it has an "is-selected" class and it\'s information shows in the inspector @unstable', async ({
|
||||
test('When selecting a fault, it has an "is-selected" class and it\'s information shows in the inspector', async ({
|
||||
page
|
||||
}) => {
|
||||
await utils.selectFaultItem(page, 1);
|
||||
await selectFaultItem(page, 1);
|
||||
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
const selectedFaultName = await page
|
||||
@@ -48,22 +69,22 @@ test.describe('The Fault Management Plugin using example faults', () => {
|
||||
.locator(`.c-inspector__properties >> :text("${selectedFaultName}")`)
|
||||
.count();
|
||||
|
||||
await expect
|
||||
.soft(page.locator('.c-faults-list-view-item-body > .c-fault-mgmt__list').first())
|
||||
.toHaveClass(/is-selected/);
|
||||
expect.soft(inspectorFaultNameCount).toEqual(1);
|
||||
await expect(
|
||||
page.locator('.c-faults-list-view-item-body > .c-fault-mgmt__list').first()
|
||||
).toHaveClass(/is-selected/);
|
||||
expect(inspectorFaultNameCount).toEqual(1);
|
||||
});
|
||||
|
||||
test('When selecting multiple faults, no specific fault information is shown in the inspector @unstable', async ({
|
||||
test('When selecting multiple faults, no specific fault information is shown in the inspector', async ({
|
||||
page
|
||||
}) => {
|
||||
await utils.selectFaultItem(page, 1);
|
||||
await utils.selectFaultItem(page, 2);
|
||||
await selectFaultItem(page, 1);
|
||||
await selectFaultItem(page, 2);
|
||||
|
||||
const selectedRows = page.locator(
|
||||
'.c-fault-mgmt__list.is-selected .c-fault-mgmt__list-faultname'
|
||||
);
|
||||
expect.soft(await selectedRows.count()).toEqual(2);
|
||||
expect(await selectedRows.count()).toEqual(2);
|
||||
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
const firstSelectedFaultName = await selectedRows.nth(0).textContent();
|
||||
@@ -75,180 +96,180 @@ test.describe('The Fault Management Plugin using example faults', () => {
|
||||
.locator(`.c-inspector__properties >> :text("${secondSelectedFaultName}")`)
|
||||
.count();
|
||||
|
||||
expect.soft(firstNameInInspectorCount).toEqual(0);
|
||||
expect.soft(secondNameInInspectorCount).toEqual(0);
|
||||
expect(firstNameInInspectorCount).toEqual(0);
|
||||
expect(secondNameInInspectorCount).toEqual(0);
|
||||
});
|
||||
|
||||
test('Allows you to shelve a fault @unstable', async ({ page }) => {
|
||||
const shelvedFaultName = await utils.getFaultName(page, 2);
|
||||
const beforeShelvedFault = utils.getFaultByName(page, shelvedFaultName);
|
||||
test('Allows you to shelve a fault', async ({ page }) => {
|
||||
const shelvedFaultName = await getFaultName(page, 2);
|
||||
const beforeShelvedFault = getFaultByName(page, shelvedFaultName);
|
||||
|
||||
expect.soft(await beforeShelvedFault.count()).toBe(1);
|
||||
await expect(beforeShelvedFault).toHaveCount(1);
|
||||
|
||||
await utils.shelveFault(page, 2);
|
||||
await shelveFault(page, 2);
|
||||
|
||||
// check it is removed from standard view
|
||||
const afterShelvedFault = utils.getFaultByName(page, shelvedFaultName);
|
||||
expect.soft(await afterShelvedFault.count()).toBe(0);
|
||||
const afterShelvedFault = getFaultByName(page, shelvedFaultName);
|
||||
expect(await afterShelvedFault.count()).toBe(0);
|
||||
|
||||
await utils.changeViewTo(page, 'shelved');
|
||||
await changeViewTo(page, 'shelved');
|
||||
|
||||
const shelvedViewFault = utils.getFaultByName(page, shelvedFaultName);
|
||||
const shelvedViewFault = getFaultByName(page, shelvedFaultName);
|
||||
|
||||
expect.soft(await shelvedViewFault.count()).toBe(1);
|
||||
expect(await shelvedViewFault.count()).toBe(1);
|
||||
});
|
||||
|
||||
test('Allows you to acknowledge a fault @unstable', async ({ page }) => {
|
||||
const acknowledgedFaultName = await utils.getFaultName(page, 3);
|
||||
test('Allows you to acknowledge a fault', async ({ page }) => {
|
||||
const acknowledgedFaultName = await getFaultName(page, 3);
|
||||
|
||||
await utils.acknowledgeFault(page, 3);
|
||||
await acknowledgeFault(page, 3);
|
||||
|
||||
const fault = utils.getFault(page, 3);
|
||||
await expect.soft(fault).toHaveClass(/is-acknowledged/);
|
||||
const fault = getFault(page, 3);
|
||||
await expect(fault).toHaveClass(/is-acknowledged/);
|
||||
|
||||
await utils.changeViewTo(page, 'acknowledged');
|
||||
await changeViewTo(page, 'acknowledged');
|
||||
|
||||
const acknowledgedViewFaultName = await utils.getFaultName(page, 1);
|
||||
expect.soft(acknowledgedFaultName).toEqual(acknowledgedViewFaultName);
|
||||
const acknowledgedViewFaultName = await getFaultName(page, 1);
|
||||
expect(acknowledgedFaultName).toEqual(acknowledgedViewFaultName);
|
||||
});
|
||||
|
||||
test('Allows you to shelve multiple faults @unstable', async ({ page }) => {
|
||||
const shelvedFaultNameOne = await utils.getFaultName(page, 1);
|
||||
const shelvedFaultNameFour = await utils.getFaultName(page, 4);
|
||||
test('Allows you to shelve multiple faults', async ({ page }) => {
|
||||
const shelvedFaultNameOne = await getFaultName(page, 1);
|
||||
const shelvedFaultNameFour = await getFaultName(page, 4);
|
||||
|
||||
const beforeShelvedFaultOne = utils.getFaultByName(page, shelvedFaultNameOne);
|
||||
const beforeShelvedFaultFour = utils.getFaultByName(page, shelvedFaultNameFour);
|
||||
const beforeShelvedFaultOne = getFaultByName(page, shelvedFaultNameOne);
|
||||
const beforeShelvedFaultFour = getFaultByName(page, shelvedFaultNameFour);
|
||||
|
||||
expect.soft(await beforeShelvedFaultOne.count()).toBe(1);
|
||||
expect.soft(await beforeShelvedFaultFour.count()).toBe(1);
|
||||
await expect(beforeShelvedFaultOne).toHaveCount(1);
|
||||
await expect(beforeShelvedFaultFour).toHaveCount(1);
|
||||
|
||||
await utils.shelveMultipleFaults(page, 1, 4);
|
||||
await shelveMultipleFaults(page, 1, 4);
|
||||
|
||||
// check it is removed from standard view
|
||||
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);
|
||||
const afterShelvedFaultOne = getFaultByName(page, shelvedFaultNameOne);
|
||||
const afterShelvedFaultFour = getFaultByName(page, shelvedFaultNameFour);
|
||||
await expect(afterShelvedFaultOne).toHaveCount(0);
|
||||
await expect(afterShelvedFaultFour).toHaveCount(0);
|
||||
|
||||
await utils.changeViewTo(page, 'shelved');
|
||||
await changeViewTo(page, 'shelved');
|
||||
|
||||
const shelvedViewFaultOne = utils.getFaultByName(page, shelvedFaultNameOne);
|
||||
const shelvedViewFaultFour = utils.getFaultByName(page, shelvedFaultNameFour);
|
||||
const shelvedViewFaultOne = getFaultByName(page, shelvedFaultNameOne);
|
||||
const shelvedViewFaultFour = getFaultByName(page, shelvedFaultNameFour);
|
||||
|
||||
expect.soft(await shelvedViewFaultOne.count()).toBe(1);
|
||||
expect.soft(await shelvedViewFaultFour.count()).toBe(1);
|
||||
await expect(shelvedViewFaultOne).toHaveCount(1);
|
||||
await expect(shelvedViewFaultFour).toHaveCount(1);
|
||||
});
|
||||
|
||||
test('Allows you to acknowledge multiple faults @unstable', async ({ page }) => {
|
||||
const acknowledgedFaultNameTwo = await utils.getFaultName(page, 2);
|
||||
const acknowledgedFaultNameFive = await utils.getFaultName(page, 5);
|
||||
test('Allows you to acknowledge multiple faults', async ({ page }) => {
|
||||
const acknowledgedFaultNameTwo = await getFaultName(page, 2);
|
||||
const acknowledgedFaultNameFive = await getFaultName(page, 5);
|
||||
|
||||
await utils.acknowledgeMultipleFaults(page, 2, 5);
|
||||
await acknowledgeMultipleFaults(page, 2, 5);
|
||||
|
||||
const faultTwo = utils.getFault(page, 2);
|
||||
const faultFive = utils.getFault(page, 5);
|
||||
const faultTwo = getFault(page, 2);
|
||||
const faultFive = getFault(page, 5);
|
||||
|
||||
// check they have been acknowledged
|
||||
await expect.soft(faultTwo).toHaveClass(/is-acknowledged/);
|
||||
await expect.soft(faultFive).toHaveClass(/is-acknowledged/);
|
||||
await expect(faultTwo).toHaveClass(/is-acknowledged/);
|
||||
await expect(faultFive).toHaveClass(/is-acknowledged/);
|
||||
|
||||
await utils.changeViewTo(page, 'acknowledged');
|
||||
await changeViewTo(page, 'acknowledged');
|
||||
|
||||
const acknowledgedViewFaultTwo = utils.getFaultByName(page, acknowledgedFaultNameTwo);
|
||||
const acknowledgedViewFaultFive = utils.getFaultByName(page, acknowledgedFaultNameFive);
|
||||
const acknowledgedViewFaultTwo = getFaultByName(page, acknowledgedFaultNameTwo);
|
||||
const acknowledgedViewFaultFive = getFaultByName(page, acknowledgedFaultNameFive);
|
||||
|
||||
expect.soft(await acknowledgedViewFaultTwo.count()).toBe(1);
|
||||
expect.soft(await acknowledgedViewFaultFive.count()).toBe(1);
|
||||
await expect(acknowledgedViewFaultTwo).toHaveCount(1);
|
||||
await expect(acknowledgedViewFaultFive).toHaveCount(1);
|
||||
});
|
||||
|
||||
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);
|
||||
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);
|
||||
|
||||
// should be all faults (5)
|
||||
let faultResultCount = await utils.getFaultResultCount(page);
|
||||
expect.soft(faultResultCount).toEqual(5);
|
||||
let faultResultCount = await getFaultResultCount(page);
|
||||
expect(faultResultCount).toEqual(5);
|
||||
|
||||
// search namespace
|
||||
await utils.enterSearchTerm(page, faultThreeNamespace);
|
||||
await enterSearchTerm(page, faultThreeNamespace);
|
||||
|
||||
faultResultCount = await utils.getFaultResultCount(page);
|
||||
expect.soft(faultResultCount).toEqual(1);
|
||||
expect.soft(await utils.getFaultNamespace(page, 1)).toEqual(faultThreeNamespace);
|
||||
faultResultCount = await getFaultResultCount(page);
|
||||
expect(faultResultCount).toEqual(1);
|
||||
expect(await getFaultNamespace(page, 1)).toEqual(faultThreeNamespace);
|
||||
|
||||
// all faults
|
||||
await utils.clearSearch(page);
|
||||
faultResultCount = await utils.getFaultResultCount(page);
|
||||
expect.soft(faultResultCount).toEqual(5);
|
||||
await clearSearch(page);
|
||||
faultResultCount = await getFaultResultCount(page);
|
||||
expect(faultResultCount).toEqual(5);
|
||||
|
||||
// search name
|
||||
await utils.enterSearchTerm(page, faultTwoName);
|
||||
await enterSearchTerm(page, faultTwoName);
|
||||
|
||||
faultResultCount = await utils.getFaultResultCount(page);
|
||||
expect.soft(faultResultCount).toEqual(1);
|
||||
expect.soft(await utils.getFaultName(page, 1)).toEqual(faultTwoName);
|
||||
faultResultCount = await getFaultResultCount(page);
|
||||
expect(faultResultCount).toEqual(1);
|
||||
expect(await getFaultName(page, 1)).toEqual(faultTwoName);
|
||||
|
||||
// all faults
|
||||
await utils.clearSearch(page);
|
||||
faultResultCount = await utils.getFaultResultCount(page);
|
||||
expect.soft(faultResultCount).toEqual(5);
|
||||
await clearSearch(page);
|
||||
faultResultCount = await getFaultResultCount(page);
|
||||
expect(faultResultCount).toEqual(5);
|
||||
|
||||
// search triggerTime
|
||||
await utils.enterSearchTerm(page, faultFiveTriggerTime);
|
||||
await enterSearchTerm(page, faultFiveTriggerTime);
|
||||
|
||||
faultResultCount = await utils.getFaultResultCount(page);
|
||||
expect.soft(faultResultCount).toEqual(1);
|
||||
expect.soft(await utils.getFaultTriggerTime(page, 1)).toEqual(faultFiveTriggerTime);
|
||||
faultResultCount = await getFaultResultCount(page);
|
||||
expect(faultResultCount).toEqual(1);
|
||||
expect(await getFaultTriggerTime(page, 1)).toEqual(faultFiveTriggerTime);
|
||||
});
|
||||
|
||||
test('Allows you to sort faults @unstable', async ({ page }) => {
|
||||
const highestSeverity = await utils.getHighestSeverity(page);
|
||||
const lowestSeverity = await utils.getLowestSeverity(page);
|
||||
test('Allows you to sort faults', async ({ page }) => {
|
||||
const highestSeverity = await getHighestSeverity(page);
|
||||
const lowestSeverity = await getLowestSeverity(page);
|
||||
const faultOneName = 'Example Fault 1';
|
||||
const faultFiveName = 'Example Fault 5';
|
||||
let firstFaultName = await utils.getFaultName(page, 1);
|
||||
let firstFaultName = await getFaultName(page, 1);
|
||||
|
||||
expect.soft(firstFaultName).toEqual(faultOneName);
|
||||
expect(firstFaultName).toEqual(faultOneName);
|
||||
|
||||
await utils.sortFaultsBy(page, 'oldest-first');
|
||||
await sortFaultsBy(page, 'oldest-first');
|
||||
|
||||
firstFaultName = await utils.getFaultName(page, 1);
|
||||
expect.soft(firstFaultName).toEqual(faultFiveName);
|
||||
firstFaultName = await getFaultName(page, 1);
|
||||
expect(firstFaultName).toEqual(faultFiveName);
|
||||
|
||||
await utils.sortFaultsBy(page, 'severity');
|
||||
await sortFaultsBy(page, 'severity');
|
||||
|
||||
const sortedHighestSeverity = await utils.getFaultSeverity(page, 1);
|
||||
const sortedLowestSeverity = await utils.getFaultSeverity(page, 5);
|
||||
expect.soft(sortedHighestSeverity).toEqual(highestSeverity);
|
||||
expect.soft(sortedLowestSeverity).toEqual(lowestSeverity);
|
||||
const sortedHighestSeverity = await getFaultSeverity(page, 1);
|
||||
const sortedLowestSeverity = await getFaultSeverity(page, 5);
|
||||
expect(sortedHighestSeverity).toEqual(highestSeverity);
|
||||
expect(sortedLowestSeverity).toEqual(lowestSeverity);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('The Fault Management Plugin without using example faults', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await utils.navigateToFaultManagementWithoutExample(page);
|
||||
await navigateToFaultManagementWithoutExample(page);
|
||||
});
|
||||
|
||||
test('Shows no faults when no faults are provided @unstable', async ({ page }) => {
|
||||
test('Shows no faults when no faults are provided', async ({ page }) => {
|
||||
const faultCount = await page.locator('c-fault-mgmt__list').count();
|
||||
|
||||
expect.soft(faultCount).toEqual(0);
|
||||
expect(faultCount).toEqual(0);
|
||||
|
||||
await utils.changeViewTo(page, 'acknowledged');
|
||||
await changeViewTo(page, 'acknowledged');
|
||||
const acknowledgedCount = await page.locator('c-fault-mgmt__list').count();
|
||||
expect.soft(acknowledgedCount).toEqual(0);
|
||||
expect(acknowledgedCount).toEqual(0);
|
||||
|
||||
await utils.changeViewTo(page, 'shelved');
|
||||
await changeViewTo(page, 'shelved');
|
||||
const shelvedCount = await page.locator('c-fault-mgmt__list').count();
|
||||
expect.soft(shelvedCount).toEqual(0);
|
||||
expect(shelvedCount).toEqual(0);
|
||||
});
|
||||
|
||||
test('Will return no faults when searching @unstable', async ({ page }) => {
|
||||
await utils.enterSearchTerm(page, 'fault');
|
||||
test('Will return no faults when searching', async ({ page }) => {
|
||||
await enterSearchTerm(page, 'fault');
|
||||
|
||||
const faultCount = await page.locator('c-fault-mgmt__list').count();
|
||||
|
||||
expect.soft(faultCount).toEqual(0);
|
||||
expect(faultCount).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -248,11 +248,10 @@ test.describe('Flexible Layout', () => {
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
// flip on independent time conductor
|
||||
await setIndependentTimeConductorBounds(
|
||||
page,
|
||||
'2021-12-30 01:01:00.000Z',
|
||||
'2021-12-30 01:11:00.000Z'
|
||||
);
|
||||
await setIndependentTimeConductorBounds(page, {
|
||||
start: '2021-12-30 01:01:00.000Z',
|
||||
end: '2021-12-30 01:11:00.000Z'
|
||||
});
|
||||
|
||||
// check image date
|
||||
await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible();
|
||||
|
||||
@@ -136,7 +136,11 @@ test.describe('Gauge', () => {
|
||||
// TODO: Verify changes in the UI
|
||||
});
|
||||
|
||||
test('Gauge does not display NaN when data not available', async ({ page }) => {
|
||||
test.fixme('Gauge does not display NaN when data not available', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/7421'
|
||||
});
|
||||
// Create a Gauge
|
||||
const gauge = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Gauge'
|
||||
@@ -171,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();
|
||||
});
|
||||
|
||||
@@ -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.locator('[aria-label="Close"]').click();
|
||||
await page.getByRole('button', { name: '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.locator('[aria-label="Close"]').click();
|
||||
await page.getByRole('button', { name: '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.getByLabel('Close').click();
|
||||
await page.getByRole('button', { name: 'Close' }).click();
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
|
||||
@@ -37,6 +37,8 @@ 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'
|
||||
});
|
||||
@@ -78,5 +80,9 @@ 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());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -308,7 +308,7 @@ test.describe('Notebook entry tests', () => {
|
||||
await page.goto(notebookObject.url);
|
||||
|
||||
// Reveal the notebook in the tree
|
||||
await page.getByTitle('Show selected item in tree').click();
|
||||
await page.getByLabel('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.getByTitle('Show selected item in tree').click();
|
||||
await page.getByLabel('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.getByTitle('Show selected item in tree').click();
|
||||
await page.getByLabel('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.getByTitle('Show selected item in tree').click();
|
||||
await page.getByLabel('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.getByTitle('Show selected item in tree').click();
|
||||
await page.getByLabel('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.getByTitle('Show selected item in tree').click();
|
||||
await page.getByLabel('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.getByTitle('Show selected item in tree').click();
|
||||
await page.getByLabel('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.getByTitle('Show selected item in tree').click();
|
||||
await page.getByLabel('Show selected item in tree').click();
|
||||
|
||||
await nbUtils.enterTextEntry(
|
||||
page,
|
||||
|
||||
@@ -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.getByLabel('Close').click();
|
||||
await page.getByRole('button', { name: 'Close' }).click();
|
||||
|
||||
// drop another image onto the entry
|
||||
await page.dispatchEvent('.c-snapshots', 'drop', { dataTransfer: dropTransfer });
|
||||
|
||||
@@ -83,7 +83,7 @@ test.describe('Snapshot Container tests', () => {
|
||||
// name: "Dropped Overlay Plot"
|
||||
// });
|
||||
|
||||
await page.getByLabel('Take a Notebook Snapshot').click();
|
||||
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();
|
||||
});
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 30 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 39 KiB |
@@ -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
|
||||
@@ -63,6 +63,63 @@ test.describe('Overlay Plot', () => {
|
||||
await expect(seriesColorSwatch).toHaveCSS('background-color', 'rgb(255, 166, 61)');
|
||||
});
|
||||
|
||||
test('Plot legend expands by default', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/7403'
|
||||
});
|
||||
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
parent: overlayPlot.uuid
|
||||
});
|
||||
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
parent: overlayPlot.uuid
|
||||
});
|
||||
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
parent: overlayPlot.uuid
|
||||
});
|
||||
|
||||
await page.goto(overlayPlot.url);
|
||||
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
|
||||
// Assert that the legend is collapsed by default
|
||||
await expect(page.getByLabel('Plot Legend Collapsed')).toBeVisible();
|
||||
await expect(page.getByLabel('Plot Legend Expanded')).toBeHidden();
|
||||
await expect(page.getByLabel('Expand by Default')).toHaveText('No');
|
||||
|
||||
expect(await page.getByLabel('Plot Legend Item').count()).toBe(3);
|
||||
|
||||
// Change the legend to expand by default
|
||||
await page.getByLabel('Edit Object').click();
|
||||
await page.getByLabel('Expand By Default').check();
|
||||
await page.getByLabel('Save').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
// Assert that the legend is now open
|
||||
await expect(page.getByLabel('Plot Legend Collapsed')).toBeHidden();
|
||||
await expect(page.getByLabel('Plot Legend Expanded')).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'Name' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'Value' })).toBeVisible();
|
||||
await expect(page.getByLabel('Expand by Default')).toHaveText('Yes');
|
||||
await expect(page.getByLabel('Plot Legend Item')).toHaveCount(3);
|
||||
|
||||
// Assert that the legend is expanded on page load
|
||||
await page.reload();
|
||||
await expect(page.getByLabel('Plot Legend Collapsed')).toBeHidden();
|
||||
await expect(page.getByLabel('Plot Legend Expanded')).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'Name' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'Value' })).toBeVisible();
|
||||
await expect(page.getByLabel('Expand by Default')).toHaveText('Yes');
|
||||
await expect(page.getByLabel('Plot Legend Item')).toHaveCount(3);
|
||||
});
|
||||
|
||||
test('Limit lines persist when series is moved to another Y Axis and on refresh', async ({
|
||||
page
|
||||
}) => {
|
||||
@@ -70,10 +127,6 @@ 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',
|
||||
@@ -136,13 +189,60 @@ test.describe('Overlay Plot', () => {
|
||||
await assertLimitLinesExistAndAreVisible(page);
|
||||
});
|
||||
|
||||
test('The elements pool supports dragging series into multiple y-axis buckets', async ({
|
||||
page
|
||||
}) => {
|
||||
const overlayPlot = await createDomainObjectWithDefaults(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 swgA = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
parent: overlayPlot.uuid
|
||||
@@ -224,17 +324,42 @@ test.describe('Overlay Plot', () => {
|
||||
expect(yAxis3Group.getByRole('listitem').nth(0).getByText(swgB.name)).toBeTruthy();
|
||||
});
|
||||
|
||||
test('Clicking on an item in the elements pool brings up the plot preview with data points', async ({
|
||||
page
|
||||
}) => {
|
||||
const overlayPlot = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Overlay Plot'
|
||||
});
|
||||
test.fixme(
|
||||
'Clicking on an item in the elements pool brings up the plot preview with data points',
|
||||
async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/7421'
|
||||
});
|
||||
|
||||
const swgA = 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();
|
||||
|
||||
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
|
||||
@@ -243,11 +368,29 @@ test.describe('Overlay Plot', () => {
|
||||
|
||||
await page.getByRole('tab', { name: 'Elements' }).click();
|
||||
|
||||
await page.locator(`#inspector-elements-tree >> text=${swgA.name}`).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();
|
||||
|
||||
const plotPixels = await getCanvasPixels(page, '.js-overlay canvas');
|
||||
const plotPixelSize = plotPixels.length;
|
||||
expect(plotPixelSize).toBeGreaterThan(0);
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -266,4 +409,7 @@ 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;
|
||||
}
|
||||
|
||||
@@ -52,7 +52,11 @@ test.describe('Plot Rendering', () => {
|
||||
expect(createMineFolderRequests.length).toEqual(0);
|
||||
});
|
||||
|
||||
test('Plot is rendered when infinity values exist', async ({ page }) => {
|
||||
test.fixme('Plot is rendered when infinity values exist', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/7421'
|
||||
});
|
||||
// Edit Plot
|
||||
await editSineWaveToUseInfinityOption(page, sineWaveGeneratorObject);
|
||||
|
||||
|
||||
88
e2e/tests/functional/plugins/plot/previews.e2e.spec.js
Normal file
88
e2e/tests/functional/plugins/plot/previews.e2e.spec.js
Normal file
@@ -0,0 +1,88 @@
|
||||
/*****************************************************************************
|
||||
* 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 '../../../../pluginFixtures.js';
|
||||
|
||||
test.describe('Plots work in Previews', () => {
|
||||
test('We can preview plot in display layouts', async ({ page, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
// Create a Sinewave Generator
|
||||
const sineWaveObject = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator'
|
||||
});
|
||||
// Create a Display Layout
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Display Layout',
|
||||
name: 'Test Display Layout'
|
||||
});
|
||||
// Edit Display Layout
|
||||
await page.getByLabel('Edit Object').click();
|
||||
|
||||
// Expand the 'My Items' folder in the left tree
|
||||
await page.getByLabel(`Expand ${myItemsFolderName} folder`).click();
|
||||
// Add the Sine Wave Generator to the Display Layout and save changes
|
||||
const treePane = page.getByRole('tree', {
|
||||
name: 'Main Tree'
|
||||
});
|
||||
const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
|
||||
name: new RegExp(sineWaveObject.name)
|
||||
});
|
||||
const layoutGridHolder = page.getByLabel('Test Display Layout Layout Grid');
|
||||
await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder);
|
||||
await page.getByLabel('Save').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
// right click on the plot and select view large
|
||||
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('Expand Test Display Layout layout').click();
|
||||
|
||||
// change to a plot and ensure embiggen works
|
||||
await page.getByLabel('Edit Object').click();
|
||||
await page.getByLabel('Move Sub-object Frame').click();
|
||||
await page.getByText('View type').click();
|
||||
await page.getByText('Overlay Plot').click();
|
||||
await page.getByLabel('Save').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
await expect(
|
||||
page.getByLabel('Test Display Layout Layout', { exact: true }).getByLabel('Plot Canvas')
|
||||
).toBeVisible();
|
||||
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();
|
||||
|
||||
// get last sinewave tree item (in the display layout)
|
||||
await page
|
||||
.getByRole('treeitem', { name: /Sine Wave Generator/ })
|
||||
.locator('a')
|
||||
.last()
|
||||
.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();
|
||||
});
|
||||
});
|
||||
@@ -257,6 +257,56 @@ test.describe('Stacked Plot', () => {
|
||||
|
||||
await assertAggregateLegendIsVisible(page);
|
||||
});
|
||||
|
||||
test('can toggle between aggregate and per child legends', async ({ page }) => {
|
||||
// make some an overlay plot
|
||||
const overlayPlot = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Overlay Plot',
|
||||
parent: stackedPlot.uuid
|
||||
});
|
||||
|
||||
// make some SWGs for the overlay plot
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
parent: overlayPlot.uuid
|
||||
});
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
parent: overlayPlot.uuid
|
||||
});
|
||||
|
||||
await page.goto(stackedPlot.url);
|
||||
await page.getByLabel('Edit Object').click();
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
await page.getByLabel('Inspector Views').getByRole('checkbox').uncheck();
|
||||
await page.getByLabel('Expand By Default').check();
|
||||
await page.getByLabel('Save').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
await expect(page.getByLabel('Plot Legend Expanded')).toHaveCount(1);
|
||||
await expect(page.getByLabel('Plot Legend Item')).toHaveCount(5);
|
||||
|
||||
// reload and ensure the legend is still expanded
|
||||
await page.reload();
|
||||
await expect(page.getByLabel('Plot Legend Expanded')).toHaveCount(1);
|
||||
await expect(page.getByLabel('Plot Legend Item')).toHaveCount(5);
|
||||
|
||||
// change to collapsed by default
|
||||
await page.getByLabel('Edit Object').click();
|
||||
await page.getByLabel('Expand By Default').uncheck();
|
||||
await page.getByLabel('Save').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
await expect(page.getByLabel('Plot Legend Collapsed')).toHaveCount(1);
|
||||
await expect(page.getByLabel('Plot Legend Item')).toHaveCount(5);
|
||||
|
||||
// change it to individual legends
|
||||
await page.getByLabel('Edit Object').click();
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
await page.getByLabel('Show Legends For Children').check();
|
||||
await page.getByLabel('Save').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
await expect(page.getByLabel('Plot Legend Collapsed')).toHaveCount(4);
|
||||
await expect(page.getByLabel('Plot Legend Item')).toHaveCount(5);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2023, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import { createDomainObjectWithDefaults, expandEntireTree } from '../../../../appActions.js';
|
||||
import { expect, test } from '../../../../pluginFixtures.js';
|
||||
|
||||
test.describe('Reload action', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
const displayLayout = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Display Layout'
|
||||
});
|
||||
|
||||
const alphaTable = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Telemetry Table',
|
||||
name: 'Alpha Table'
|
||||
});
|
||||
|
||||
const betaTable = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Telemetry Table',
|
||||
name: 'Beta Table'
|
||||
});
|
||||
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
parent: alphaTable.uuid,
|
||||
customParameters: {
|
||||
'[aria-label="Data Rate (hz)"]': '0.001'
|
||||
}
|
||||
});
|
||||
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
parent: betaTable.uuid,
|
||||
customParameters: {
|
||||
'[aria-label="Data Rate (hz)"]': '0.001'
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto(displayLayout.url);
|
||||
|
||||
// Expand all folders
|
||||
await expandEntireTree(page);
|
||||
|
||||
await page.getByLabel('Edit Object', { exact: true }).click();
|
||||
|
||||
await page.dragAndDrop(`text='Alpha Table'`, '.l-layout__grid-holder', {
|
||||
targetPosition: { x: 0, y: 0 }
|
||||
});
|
||||
|
||||
await page.dragAndDrop(`text='Beta Table'`, '.l-layout__grid-holder', {
|
||||
targetPosition: { x: 0, y: 250 }
|
||||
});
|
||||
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
});
|
||||
|
||||
test('can reload display layout and its children', async ({ page }) => {
|
||||
const beforeReloadAlphaTelemetryValue = await page
|
||||
.getByLabel('Alpha Table table content')
|
||||
.getByLabel('wavelengths table cell')
|
||||
.first()
|
||||
.getAttribute('title');
|
||||
const beforeReloadBetaTelemetryValue = await page
|
||||
.getByLabel('Beta Table table content')
|
||||
.getByLabel('wavelengths table cell')
|
||||
.first()
|
||||
.getAttribute('title');
|
||||
// reload alpha
|
||||
await page.getByTitle('View menu items').first().click();
|
||||
await page.getByRole('menuitem', { name: /Reload/ }).click();
|
||||
|
||||
const afterReloadAlphaTelemetryValue = await page
|
||||
.getByLabel('Alpha Table table content')
|
||||
.getByLabel('wavelengths table cell')
|
||||
.first()
|
||||
.getAttribute('title');
|
||||
const afterReloadBetaTelemetryValue = await page
|
||||
.getByLabel('Beta Table table content')
|
||||
.getByLabel('wavelengths table cell')
|
||||
.first()
|
||||
.getAttribute('title');
|
||||
|
||||
expect(beforeReloadAlphaTelemetryValue).not.toEqual(afterReloadAlphaTelemetryValue);
|
||||
expect(beforeReloadBetaTelemetryValue).toEqual(afterReloadBetaTelemetryValue);
|
||||
|
||||
// now reload parent
|
||||
await page.getByTitle('More actions').click();
|
||||
await page.getByRole('menuitem', { name: /Reload/ }).click();
|
||||
|
||||
const fullReloadAlphaTelemetryValue = await page
|
||||
.getByLabel('Alpha Table table content')
|
||||
.getByLabel('wavelengths table cell')
|
||||
.first()
|
||||
.getAttribute('title');
|
||||
const fullReloadBetaTelemetryValue = await page
|
||||
.getByLabel('Beta Table table content')
|
||||
.getByLabel('wavelengths table cell')
|
||||
.first()
|
||||
.getAttribute('title');
|
||||
|
||||
expect(fullReloadAlphaTelemetryValue).not.toEqual(afterReloadAlphaTelemetryValue);
|
||||
expect(fullReloadBetaTelemetryValue).not.toEqual(afterReloadBetaTelemetryValue);
|
||||
});
|
||||
});
|
||||
@@ -32,10 +32,10 @@ const setBorderColor = '#ff00ff';
|
||||
const setBackgroundColor = '#5b0f00';
|
||||
const setTextColor = '#e6b8af';
|
||||
const defaultFrameBorderColor = '#e6b8af'; //default border color
|
||||
const defaultBorderTargetColor = '#aaaaaa';
|
||||
const defaultTextColor = '#aaaaaa'; // default text color
|
||||
const inheritedColor = '#aaaaaa'; // inherited from the body style
|
||||
const pukeGreen = '#6aa84f'; //Ugliest green known to man
|
||||
const defaultBorderTargetColor = '#acacac';
|
||||
const defaultTextColor = '#acacac'; // default text color
|
||||
const inheritedColor = '#acacac'; // inherited from the body style
|
||||
const pukeGreen = '#6aa84f'; //Ugliest green known to man 🤮
|
||||
const NO_STYLE_RGBA = 'rgba(0, 0, 0, 0)'; //default background color value
|
||||
|
||||
test.describe('Flexible Layout styling', () => {
|
||||
@@ -114,7 +114,9 @@ test.describe('Flexible Layout styling', () => {
|
||||
hexToRGB(defaultFrameBorderColor),
|
||||
NO_STYLE_RGBA,
|
||||
hexToRGB(setTextColor),
|
||||
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
|
||||
page
|
||||
.getByRole('group', { name: 'StackedPlot1 Frame' })
|
||||
.getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
|
||||
// Check styles on StackedPlot2. Note: https://github.com/nasa/openmct/issues/7337
|
||||
@@ -122,7 +124,9 @@ test.describe('Flexible Layout styling', () => {
|
||||
hexToRGB(defaultFrameBorderColor),
|
||||
NO_STYLE_RGBA,
|
||||
hexToRGB(setTextColor),
|
||||
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target')
|
||||
page
|
||||
.getByRole('group', { name: 'StackedPlot2 Frame' })
|
||||
.getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
});
|
||||
|
||||
@@ -143,7 +147,9 @@ test.describe('Flexible Layout styling', () => {
|
||||
hexToRGB(defaultBorderTargetColor),
|
||||
NO_STYLE_RGBA,
|
||||
hexToRGB(defaultTextColor),
|
||||
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
|
||||
page
|
||||
.getByRole('group', { name: 'StackedPlot1 Frame' })
|
||||
.getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
|
||||
// Check styles on StackedPlot2
|
||||
@@ -151,7 +157,9 @@ test.describe('Flexible Layout styling', () => {
|
||||
hexToRGB(defaultBorderTargetColor),
|
||||
NO_STYLE_RGBA,
|
||||
hexToRGB(defaultTextColor),
|
||||
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target')
|
||||
page
|
||||
.getByRole('group', { name: 'StackedPlot2 Frame' })
|
||||
.getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
|
||||
// Set styles using setStyles function on StackedPlot1 but not StackedPlot2
|
||||
@@ -160,7 +168,7 @@ test.describe('Flexible Layout styling', () => {
|
||||
setBorderColor,
|
||||
setBackgroundColor,
|
||||
setTextColor,
|
||||
page.getByLabel('StackedPlot1 Frame')
|
||||
page.getByRole('group', { name: 'StackedPlot1 Frame' })
|
||||
);
|
||||
|
||||
// Check styles on StackedPlot1
|
||||
@@ -168,7 +176,9 @@ test.describe('Flexible Layout styling', () => {
|
||||
hexToRGB(setBorderColor),
|
||||
hexToRGB(setBackgroundColor),
|
||||
hexToRGB(setTextColor),
|
||||
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
|
||||
page
|
||||
.getByRole('group', { name: 'StackedPlot1 Frame' })
|
||||
.getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
|
||||
// Check styles on StackedPlot2
|
||||
@@ -176,7 +186,9 @@ test.describe('Flexible Layout styling', () => {
|
||||
hexToRGB(defaultBorderTargetColor),
|
||||
NO_STYLE_RGBA,
|
||||
hexToRGB(defaultTextColor),
|
||||
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target')
|
||||
page
|
||||
.getByRole('group', { name: 'StackedPlot2 Frame' })
|
||||
.getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
|
||||
// Save Flexible Layout
|
||||
@@ -191,7 +203,9 @@ test.describe('Flexible Layout styling', () => {
|
||||
hexToRGB(setBorderColor),
|
||||
hexToRGB(setBackgroundColor),
|
||||
hexToRGB(setTextColor),
|
||||
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
|
||||
page
|
||||
.getByRole('group', { name: 'StackedPlot1 Frame' })
|
||||
.getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
|
||||
// Check styles on StackedPlot2
|
||||
@@ -199,7 +213,9 @@ test.describe('Flexible Layout styling', () => {
|
||||
hexToRGB(defaultBorderTargetColor),
|
||||
NO_STYLE_RGBA,
|
||||
hexToRGB(defaultTextColor),
|
||||
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target')
|
||||
page
|
||||
.getByRole('group', { name: 'StackedPlot2 Frame' })
|
||||
.getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
});
|
||||
|
||||
@@ -241,7 +257,9 @@ test.describe('Flexible Layout styling', () => {
|
||||
hexToRGB(setBorderColor),
|
||||
hexToRGB(setBackgroundColor),
|
||||
hexToRGB(setTextColor),
|
||||
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
|
||||
page
|
||||
.getByRole('group', { name: 'StackedPlot1 Frame' })
|
||||
.getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
|
||||
// Check styles on StackedPlot2 to verify they are the default
|
||||
@@ -249,7 +267,9 @@ test.describe('Flexible Layout styling', () => {
|
||||
hexToRGB(defaultBorderTargetColor),
|
||||
NO_STYLE_RGBA,
|
||||
hexToRGB(defaultTextColor),
|
||||
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target')
|
||||
page
|
||||
.getByRole('group', { name: 'StackedPlot2 Frame' })
|
||||
.getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
|
||||
// Set styles using setStyles function on StackedPlot2
|
||||
@@ -258,7 +278,7 @@ test.describe('Flexible Layout styling', () => {
|
||||
setBorderColor,
|
||||
setBackgroundColor,
|
||||
setTextColor,
|
||||
page.getByLabel('StackedPlot2 Frame')
|
||||
page.getByRole('group', { name: 'StackedPlot2 Frame' })
|
||||
);
|
||||
|
||||
// Check styles on StackedPlot2
|
||||
@@ -266,7 +286,9 @@ test.describe('Flexible Layout styling', () => {
|
||||
hexToRGB(setBorderColor),
|
||||
hexToRGB(setBackgroundColor),
|
||||
hexToRGB(setTextColor),
|
||||
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target')
|
||||
page
|
||||
.getByRole('group', { name: 'StackedPlot2 Frame' })
|
||||
.getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
|
||||
// Save Flexible Layout
|
||||
@@ -281,7 +303,9 @@ test.describe('Flexible Layout styling', () => {
|
||||
hexToRGB(setBorderColor),
|
||||
hexToRGB(setBackgroundColor),
|
||||
hexToRGB(setTextColor),
|
||||
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
|
||||
page
|
||||
.getByRole('group', { name: 'StackedPlot1 Frame' })
|
||||
.getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
|
||||
// Check styles on StackedPlot2
|
||||
@@ -289,7 +313,9 @@ test.describe('Flexible Layout styling', () => {
|
||||
hexToRGB(setBorderColor),
|
||||
hexToRGB(setBackgroundColor),
|
||||
hexToRGB(setTextColor),
|
||||
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target')
|
||||
page
|
||||
.getByRole('group', { name: 'StackedPlot2 Frame' })
|
||||
.getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
|
||||
// Directly navigate to the flexible layout
|
||||
@@ -326,7 +352,9 @@ test.describe('Flexible Layout styling', () => {
|
||||
hexToRGB(setBorderColor),
|
||||
hexToRGB(setBackgroundColor),
|
||||
hexToRGB(setTextColor),
|
||||
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
|
||||
page
|
||||
.getByRole('group', { name: 'StackedPlot1 Frame' })
|
||||
.getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
|
||||
// Check styles on StackedPlot2 matches previous set colors
|
||||
@@ -334,7 +362,9 @@ test.describe('Flexible Layout styling', () => {
|
||||
hexToRGB(setBorderColor),
|
||||
hexToRGB(setBackgroundColor),
|
||||
hexToRGB(setTextColor),
|
||||
page.getByLabel('StackedPlot2 Frame').getByLabel('Stacked Plot Style Target')
|
||||
page
|
||||
.getByRole('group', { name: 'StackedPlot2 Frame' })
|
||||
.getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
});
|
||||
|
||||
@@ -356,7 +386,7 @@ test.describe('Flexible Layout styling', () => {
|
||||
setBorderColor,
|
||||
setBackgroundColor,
|
||||
setTextColor,
|
||||
page.getByLabel('StackedPlot1 Frame')
|
||||
page.getByRole('group', { name: 'StackedPlot1 Frame' })
|
||||
);
|
||||
|
||||
// Check styles using checkStyles function
|
||||
@@ -364,7 +394,9 @@ test.describe('Flexible Layout styling', () => {
|
||||
hexToRGB(setBorderColor),
|
||||
hexToRGB(setBackgroundColor),
|
||||
hexToRGB(setTextColor),
|
||||
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
|
||||
page
|
||||
.getByRole('group', { name: 'StackedPlot1 Frame' })
|
||||
.getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
|
||||
// Save Flexible Layout
|
||||
@@ -386,7 +418,7 @@ test.describe('Flexible Layout styling', () => {
|
||||
'No Style',
|
||||
'No Style',
|
||||
'No Style',
|
||||
page.getByLabel('StackedPlot1 Frame')
|
||||
page.getByRole('group', { name: 'StackedPlot1 Frame' })
|
||||
);
|
||||
|
||||
// Check styles using checkStyles function
|
||||
@@ -394,11 +426,13 @@ test.describe('Flexible Layout styling', () => {
|
||||
hexToRGB(defaultBorderTargetColor),
|
||||
NO_STYLE_RGBA,
|
||||
hexToRGB(inheritedColor),
|
||||
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
|
||||
page
|
||||
.getByRole('group', { name: 'StackedPlot1 Frame' })
|
||||
.getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
// Save Flexible Layout
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
// Reload page and verify that styles persist
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
@@ -408,7 +442,44 @@ test.describe('Flexible Layout styling', () => {
|
||||
hexToRGB(defaultBorderTargetColor),
|
||||
NO_STYLE_RGBA,
|
||||
hexToRGB(inheritedColor),
|
||||
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
|
||||
page
|
||||
.getByRole('group', { name: 'StackedPlot1 Frame' })
|
||||
.getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
});
|
||||
|
||||
test('Styling, and then canceling reverts to previous style', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/7233'
|
||||
});
|
||||
|
||||
await page.goto(flexibleLayout.url);
|
||||
|
||||
await page.getByLabel('Edit Object').click();
|
||||
await page.getByRole('tab', { name: 'Styles' }).click();
|
||||
await setStyles(
|
||||
page,
|
||||
setBorderColor,
|
||||
setBackgroundColor,
|
||||
setTextColor,
|
||||
page.getByLabel('Flexible Layout Column')
|
||||
);
|
||||
await page.getByLabel('Cancel Editing').click();
|
||||
await page.getByRole('button', { name: 'OK', exact: true }).click();
|
||||
await checkStyles(
|
||||
hexToRGB(defaultBorderTargetColor),
|
||||
NO_STYLE_RGBA,
|
||||
hexToRGB(inheritedColor),
|
||||
page.getByLabel('Flexible Layout Column')
|
||||
);
|
||||
|
||||
await page.reload();
|
||||
await checkStyles(
|
||||
hexToRGB(defaultBorderTargetColor),
|
||||
NO_STYLE_RGBA,
|
||||
hexToRGB(inheritedColor),
|
||||
page.getByLabel('Flexible Layout Column')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -36,9 +36,9 @@ import { test } from '../../../../pluginFixtures.js';
|
||||
const setBorderColor = '#ff00ff';
|
||||
const setBackgroundColor = '#5b0f00';
|
||||
const setTextColor = '#e6b8af';
|
||||
const defaultTextColor = '#aaaaaa'; // default text color
|
||||
const defaultTextColor = '#acacac'; // default text color
|
||||
const NO_STYLE_RGBA = 'rgba(0, 0, 0, 0)'; //default background color value
|
||||
const DEFAULT_PLOT_VIEW_BORDER_COLOR = '#AAAAAA';
|
||||
const DEFAULT_PLOT_VIEW_BORDER_COLOR = '#acacac';
|
||||
const setFontSize = '72px';
|
||||
const setFontWeight = '700'; //bold for monospace bold
|
||||
const setFontFamily = '"Andale Mono", sans-serif';
|
||||
|
||||
@@ -67,7 +67,7 @@ test.describe('Style Inspector Options', () => {
|
||||
await expect(page.getByRole('tab', { name: 'Styles' })).toBeVisible();
|
||||
|
||||
// Select Stacked Layout Column
|
||||
await page.getByLabel('Stacked Plot Frame').click();
|
||||
await page.getByRole('group', { name: 'Stacked Plot Frame' }).click();
|
||||
|
||||
// The overall Flex Layout or Stacked Plot itself MUST be style-able.
|
||||
await expect(page.getByRole('tab', { name: 'Styles' })).toBeVisible();
|
||||
|
||||
@@ -24,13 +24,18 @@ import { createDomainObjectWithDefaults } from '../../../../appActions.js';
|
||||
import { expect, test } from '../../../../pluginFixtures.js';
|
||||
|
||||
test.describe('Tabs View', () => {
|
||||
test('Renders tabbed elements', async ({ page }) => {
|
||||
let tabsView;
|
||||
let table;
|
||||
let notebook;
|
||||
let sineWaveGenerator;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
const tabsView = await createDomainObjectWithDefaults(page, {
|
||||
tabsView = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Tabs View'
|
||||
});
|
||||
const table = await createDomainObjectWithDefaults(page, {
|
||||
table = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Telemetry Table',
|
||||
parent: tabsView.uuid
|
||||
});
|
||||
@@ -38,19 +43,21 @@ test.describe('Tabs View', () => {
|
||||
type: 'Event Message Generator',
|
||||
parent: table.uuid
|
||||
});
|
||||
const notebook = await createDomainObjectWithDefaults(page, {
|
||||
notebook = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Notebook',
|
||||
parent: tabsView.uuid
|
||||
});
|
||||
const sineWaveGenerator = await createDomainObjectWithDefaults(page, {
|
||||
sineWaveGenerator = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
parent: tabsView.uuid
|
||||
});
|
||||
});
|
||||
|
||||
page.goto(tabsView.url);
|
||||
test('Renders tabbed elements', async ({ page }) => {
|
||||
await page.goto(tabsView.url);
|
||||
|
||||
// select first tab
|
||||
await page.getByLabel(`${table.name} tab`).click();
|
||||
await page.getByLabel(`${table.name} tab`, { exact: true }).click();
|
||||
// ensure table header visible
|
||||
await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible();
|
||||
|
||||
@@ -58,7 +65,7 @@ test.describe('Tabs View', () => {
|
||||
await expect(page.locator('canvas[id=webglContext]')).toBeHidden();
|
||||
|
||||
// select second tab
|
||||
await page.getByLabel(`${notebook.name} tab`).click();
|
||||
await page.getByLabel(`${notebook.name} tab`, { exact: true }).click();
|
||||
|
||||
// ensure notebook visible
|
||||
await expect(page.locator('.c-notebook__drag-area')).toBeVisible();
|
||||
@@ -67,7 +74,7 @@ test.describe('Tabs View', () => {
|
||||
await expect(page.locator('canvas[id=webglContext]')).toBeHidden();
|
||||
|
||||
// select third tab
|
||||
await page.getByLabel(`${sineWaveGenerator.name} tab`).click();
|
||||
await page.getByLabel(`${sineWaveGenerator.name} tab`, { exact: true }).click();
|
||||
|
||||
// expect sine wave generator visible
|
||||
await expect(page.locator('.c-plot')).toBeVisible();
|
||||
@@ -78,7 +85,7 @@ test.describe('Tabs View', () => {
|
||||
await expect(page.locator('canvas').nth(1)).toBeVisible();
|
||||
|
||||
// now try to select the first tab again
|
||||
await page.getByLabel(`${table.name} tab`).click();
|
||||
await page.getByLabel(`${table.name} tab`, { exact: true }).click();
|
||||
// ensure table header visible
|
||||
await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible();
|
||||
|
||||
@@ -86,3 +93,29 @@ test.describe('Tabs View', () => {
|
||||
await expect(page.locator('canvas[id=webglContext]')).toBeHidden();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Tabs View CRUD', () => {
|
||||
let tabsView;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
tabsView = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Tabs View'
|
||||
});
|
||||
});
|
||||
|
||||
test('Eager Load Tabs is the default and then can be toggled off', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/7198'
|
||||
});
|
||||
await page.goto(tabsView.url);
|
||||
|
||||
await page.getByLabel('Edit Object').click();
|
||||
await page.getByLabel('More actions').click();
|
||||
await page.getByLabel('Edit Properties...').click();
|
||||
await expect(await page.getByLabel('Eager Load Tabs')).not.toBeChecked();
|
||||
await page.getByLabel('Eager Load Tabs').setChecked(true);
|
||||
await expect(await page.getByLabel('Eager Load Tabs')).toBeChecked();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
/*
|
||||
* This test suite is dedicated to testing the preview plugin.
|
||||
*/
|
||||
|
||||
import { createDomainObjectWithDefaults, expandEntireTree } from '../../../../appActions.js';
|
||||
import { expect, test } from '../../../../pluginFixtures.js';
|
||||
|
||||
test.describe('Preview mode', () => {
|
||||
test('all context menu items are available for a telemetry table', async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
// Create a Display Layout
|
||||
const displayLayout = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Display Layout'
|
||||
});
|
||||
// Create a Telemetry Table
|
||||
const telemetryTable = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Telemetry Table',
|
||||
parent: displayLayout.uuid
|
||||
});
|
||||
// Create a Sinewave Generator
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
parent: telemetryTable.uuid
|
||||
});
|
||||
|
||||
await page.goto(displayLayout.url);
|
||||
await page.getByLabel('View menu items').click();
|
||||
await expect(page.getByLabel('Export Marked Rows')).toBeVisible();
|
||||
|
||||
await page.getByRole('menuitem', { name: 'Large View' }).click();
|
||||
await page.getByLabel('Overlay').getByLabel('More actions').click();
|
||||
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 expandEntireTree(page);
|
||||
|
||||
await page.getByLabel('Edit Object').click();
|
||||
|
||||
const treePane = page.getByRole('tree', {
|
||||
name: 'Main Tree'
|
||||
});
|
||||
const telemetryTableTreeItem = treePane.getByRole('treeitem', {
|
||||
name: new RegExp(telemetryTable.name)
|
||||
});
|
||||
await telemetryTableTreeItem.locator('a').click();
|
||||
await page.getByLabel('Overlay').getByLabel('More actions').click();
|
||||
await expect(page.getByLabel('Export Table Data')).toBeVisible();
|
||||
await expect(page.getByLabel('Export Marked Rows')).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -64,10 +64,9 @@ test.describe('Telemetry Table', () => {
|
||||
|
||||
// Get the most recent telemetry date
|
||||
const latestTelemetryDate = await page
|
||||
.locator('table.c-telemetry-table__body > tbody > tr')
|
||||
.getByLabel('table content')
|
||||
.getByLabel('utc table cell')
|
||||
.last()
|
||||
.locator('td')
|
||||
.nth(1)
|
||||
.getAttribute('title');
|
||||
|
||||
// Verify that it is <= our new end bound
|
||||
@@ -91,7 +90,7 @@ test.describe('Telemetry Table', () => {
|
||||
await page.getByRole('searchbox', { name: 'message filter input' }).click();
|
||||
await page.getByRole('searchbox', { name: 'message filter input' }).fill('Roger');
|
||||
|
||||
let cells = await page.getByRole('cell', { name: /Roger/ }).all();
|
||||
let cells = await page.getByRole('cell').getByText(/Roger/).all();
|
||||
// ensure we've got more than one cell
|
||||
expect(cells.length).toBeGreaterThan(1);
|
||||
// ensure the text content of each cell contains the search term
|
||||
@@ -103,7 +102,10 @@ test.describe('Telemetry Table', () => {
|
||||
await page.getByRole('searchbox', { name: 'message filter input' }).click();
|
||||
await page.getByRole('searchbox', { name: 'message filter input' }).fill('Dodger');
|
||||
|
||||
cells = await page.getByRole('cell', { name: /Dodger/ }).all();
|
||||
cells = await page
|
||||
.getByRole('cell')
|
||||
.getByText(/Dodger/)
|
||||
.all();
|
||||
// ensure we've got more than one cell
|
||||
expect(cells.length).toBe(0);
|
||||
// ensure the text content of each cell contains the search term
|
||||
@@ -135,7 +137,7 @@ test.describe('Telemetry Table', () => {
|
||||
await page.getByRole('searchbox', { name: 'message filter input' }).click();
|
||||
await page.getByRole('searchbox', { name: 'message filter input' }).fill('/[Rr]oger/');
|
||||
|
||||
let cells = await page.getByRole('cell', { name: /Roger/ }).all();
|
||||
let cells = await page.getByRole('cell').getByText(/Roger/).all();
|
||||
// ensure we've got more than one cell
|
||||
expect(cells.length).toBeGreaterThan(1);
|
||||
// ensure the text content of each cell contains the search term
|
||||
@@ -147,7 +149,10 @@ test.describe('Telemetry Table', () => {
|
||||
await page.getByRole('searchbox', { name: 'message filter input' }).click();
|
||||
await page.getByRole('searchbox', { name: 'message filter input' }).fill('/[Dd]oger/');
|
||||
|
||||
cells = await page.getByRole('cell', { name: /Dodger/ }).all();
|
||||
cells = await page
|
||||
.getByRole('cell')
|
||||
.getByText(/Dodger/)
|
||||
.all();
|
||||
// ensure we've got more than one cell
|
||||
expect(cells.length).toBe(0);
|
||||
// ensure the text content of each cell contains the search term
|
||||
|
||||
@@ -66,7 +66,7 @@ test.describe('Timer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Timer with target date', () => {
|
||||
test.describe('Timer with target date @clock', () => {
|
||||
let timer;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
|
||||
@@ -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.getByTitle('Show selected item in tree').click();
|
||||
await page.getByLabel('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('Ensure clear recent objects button is active or inactive', async ({ page }) => {
|
||||
test('Verify functionality of "clear" and "collapse pane" buttons', 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,6 +331,24 @@ 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() {
|
||||
|
||||
@@ -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 @ipad', async ({ page, openmctConfig }) => {
|
||||
test('Verify that My Items Tree appears', async ({ page, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
//Go to baseURL
|
||||
await page.goto('./');
|
||||
|
||||
@@ -359,7 +359,11 @@ test.describe('Verify tooltips', () => {
|
||||
expect(tooltipText).toBe(sineWaveObject3.path);
|
||||
});
|
||||
|
||||
test('display tooltip path for telemetry table names', async ({ page }) => {
|
||||
test.fixme('display tooltip path for telemetry table names', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/7421'
|
||||
});
|
||||
// set endBound to 10 seconds after start bound
|
||||
const url = await page.url();
|
||||
const parsedUrl = new URL(url.replace('#', '!'));
|
||||
|
||||
@@ -40,7 +40,7 @@ test.describe('Main Tree', () => {
|
||||
type: 'Folder'
|
||||
});
|
||||
|
||||
await page.getByTitle('Show selected item in tree').click();
|
||||
await page.getByLabel('Show selected item in tree').click();
|
||||
|
||||
const clock = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Clock',
|
||||
|
||||
95
e2e/tests/mobile/smoke.e2e.spec.js
Normal file
95
e2e/tests/mobile/smoke.e2e.spec.js
Normal file
@@ -0,0 +1,95 @@
|
||||
/*****************************************************************************
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
@@ -178,7 +178,7 @@ test.describe('Performance tests', () => {
|
||||
console.log('jpgResourceTiming ' + JSON.stringify(jpgResourceTiming));
|
||||
|
||||
// Click Close Icon
|
||||
await page.locator('[aria-label="Close"]').click();
|
||||
await page.getByRole('button', { name: 'Close' }).click();
|
||||
await page.evaluate(() => window.performance.mark('view-large-close-button'));
|
||||
|
||||
//await client.send('HeapProfiler.enable');
|
||||
|
||||
@@ -24,7 +24,7 @@ import { createDomainObjectWithDefaults, waitForPlotsToRender } from '../../appA
|
||||
import { expect, test } from '../../pluginFixtures.js';
|
||||
|
||||
test.describe('Tabs View', () => {
|
||||
test('Renders tabbed elements nicely', async ({ page }) => {
|
||||
test('Renders tabbed elements only when visible', async ({ page }) => {
|
||||
// Code to hook into the requestAnimationFrame function and log each call
|
||||
let animationCalls = [];
|
||||
await page.exposeFunction('logCall', (callCount) => {
|
||||
@@ -64,24 +64,24 @@ test.describe('Tabs View', () => {
|
||||
page.goto(tabsView.url);
|
||||
|
||||
// select first tab
|
||||
await page.getByLabel(`${table.name} tab`).click();
|
||||
await page.getByLabel(`${table.name} tab`, { exact: true }).click();
|
||||
// ensure table header visible
|
||||
await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible();
|
||||
|
||||
// select second tab
|
||||
await page.getByLabel(`${notebook.name} tab`).click();
|
||||
await page.getByLabel(`${notebook.name} tab`, { exact: true }).click();
|
||||
|
||||
// expect notebook visible
|
||||
await expect(page.locator('.c-notebook__drag-area')).toBeVisible();
|
||||
|
||||
// select third tab
|
||||
await page.getByLabel(`${sineWaveGenerator.name} tab`).click();
|
||||
await page.getByLabel(`${sineWaveGenerator.name} tab`, { exact: true }).click();
|
||||
|
||||
// ensure sine wave generator visible
|
||||
expect(await page.locator('.c-plot').isVisible()).toBe(true);
|
||||
|
||||
// now select notebook and clear animation calls
|
||||
await page.getByLabel(`${notebook.name} tab`).click();
|
||||
await page.getByLabel(`${notebook.name} tab`, { exact: true }).click();
|
||||
animationCalls = [];
|
||||
// expect notebook visible
|
||||
await expect(page.locator('.c-notebook__drag-area')).toBeVisible();
|
||||
@@ -89,7 +89,7 @@ test.describe('Tabs View', () => {
|
||||
|
||||
// select sine wave generator and clear animation calls
|
||||
animationCalls = [];
|
||||
await page.getByLabel(`${sineWaveGenerator.name} tab`).click();
|
||||
await page.getByLabel(`${sineWaveGenerator.name} tab`, { exact: true }).click();
|
||||
|
||||
// ensure sine wave generator visible
|
||||
await waitForPlotsToRender(page);
|
||||
@@ -20,14 +20,16 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import { scanForA11yViolations, test } from '../../avpFixtures.js';
|
||||
import { test } from '../../avpFixtures.js';
|
||||
import { VISUAL_URL } from '../../constants.js';
|
||||
|
||||
test.describe('a11y - Default @a11y', () => {
|
||||
test.describe('a11y - Default', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
|
||||
});
|
||||
test('main view @a11y', async ({ page }, testInfo) => {
|
||||
await scanForA11yViolations(page, testInfo.title);
|
||||
test('main view', async ({ page }, testInfo) => {
|
||||
await page.goto('./');
|
||||
//Skipping for https://github.com/nasa/openmct/issues/7421
|
||||
//await scanForA11yViolations(page, testInfo.title);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,11 +25,12 @@ Tests the branding associated with the default deployment. At least the about mo
|
||||
*/
|
||||
|
||||
import percySnapshot from '@percy/playwright';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
import { expect, scanForA11yViolations, test } from '../../../avpFixtures.js';
|
||||
import { expect, test } from '../../../avpFixtures.js';
|
||||
import { VISUAL_URL } from '../../../constants.js';
|
||||
|
||||
//Declare the scope of the visual test
|
||||
//Declare the component scope of the visual test for Percy
|
||||
const header = '.l-shell__head';
|
||||
|
||||
test.describe('Visual - Header @a11y', () => {
|
||||
@@ -68,16 +69,37 @@ test.describe('Visual - Header @a11y', () => {
|
||||
});
|
||||
|
||||
test('show snapshot button', async ({ page, theme }) => {
|
||||
await page.getByLabel('Take a Notebook Snapshot').click();
|
||||
await page.getByLabel('Open the Notebook Snapshot Menu').click();
|
||||
|
||||
await page.getByRole('menuitem', { name: 'Save to Notebook Snapshots' }).click();
|
||||
|
||||
await percySnapshot(page, `Notebook Snapshot Show button (theme: '${theme}')`, {
|
||||
scope: header
|
||||
});
|
||||
await expect(await page.getByLabel('Show Snapshots')).toBeVisible();
|
||||
await expect(page.getByLabel('Show Snapshots')).toBeVisible();
|
||||
});
|
||||
});
|
||||
test.afterEach(async ({ page }, testInfo) => {
|
||||
await scanForA11yViolations(page, testInfo.title);
|
||||
|
||||
//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
|
||||
});
|
||||
});
|
||||
});
|
||||
// Skipping for https://github.com/nasa/openmct/issues/7421
|
||||
// test.afterEach(async ({ page }, testInfo) => {
|
||||
// await scanForA11yViolations(page, testInfo.title);
|
||||
// });
|
||||
|
||||
@@ -22,13 +22,13 @@
|
||||
|
||||
import percySnapshot from '@percy/playwright';
|
||||
|
||||
import { scanForA11yViolations, test } from '../../../avpFixtures.js';
|
||||
import { test } from '../../../avpFixtures.js';
|
||||
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', () => {
|
||||
test.describe('Visual - Inspector @ally @clock', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
|
||||
});
|
||||
@@ -55,6 +55,7 @@ test.describe('Visual - Inspector @ally', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
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);
|
||||
// });
|
||||
|
||||
@@ -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', () => {
|
||||
test.describe('Visual - Controlled Clock @clock', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
|
||||
});
|
||||
|
||||
@@ -93,4 +93,14 @@ 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}')`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,18 +20,26 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import percySnapshot from '@percy/playwright';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
import * as utils from '../../helper/faultUtils.js';
|
||||
import {
|
||||
acknowledgeFault,
|
||||
changeViewTo,
|
||||
navigateToFaultManagementWithoutExample,
|
||||
navigateToFaultManagementWithStaticExample,
|
||||
openFaultRowMenu,
|
||||
selectFaultItem,
|
||||
shelveFault
|
||||
} from '../../helper/faultUtils.js';
|
||||
import { expect, test } from '../../pluginFixtures.js';
|
||||
|
||||
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.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('fault management icon appears in tree', async ({ page, theme }) => {
|
||||
// Wait for status bar to load
|
||||
await expect(
|
||||
page.getByRole('status', {
|
||||
@@ -51,14 +59,20 @@ test.describe('Fault Management Visual Tests', () => {
|
||||
|
||||
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 utils.acknowledgeFault(page, 1);
|
||||
await utils.changeViewTo(page, 'acknowledged');
|
||||
await acknowledgeFault(page, 1);
|
||||
await changeViewTo(page, 'acknowledged');
|
||||
|
||||
await percySnapshot(
|
||||
page,
|
||||
@@ -67,14 +81,12 @@ test.describe('Fault Management Visual Tests', () => {
|
||||
});
|
||||
|
||||
test('shelved faults', async ({ page, theme }) => {
|
||||
await utils.navigateToFaultManagementWithStaticExample(page);
|
||||
|
||||
await utils.shelveFault(page, 1);
|
||||
await utils.changeViewTo(page, 'shelved');
|
||||
await shelveFault(page, 1);
|
||||
await changeViewTo(page, 'shelved');
|
||||
|
||||
await percySnapshot(page, `Shelved faults appear in the shelved view (theme: '${theme}')`);
|
||||
|
||||
await utils.openFaultRowMenu(page, 1);
|
||||
await openFaultRowMenu(page, 1);
|
||||
|
||||
await percySnapshot(
|
||||
page,
|
||||
@@ -83,9 +95,7 @@ test.describe('Fault Management Visual Tests', () => {
|
||||
});
|
||||
|
||||
test('3-dot menu for fault', async ({ page, theme }) => {
|
||||
await utils.navigateToFaultManagementWithStaticExample(page);
|
||||
|
||||
await utils.openFaultRowMenu(page, 1);
|
||||
await openFaultRowMenu(page, 1);
|
||||
|
||||
await percySnapshot(
|
||||
page,
|
||||
@@ -94,9 +104,7 @@ test.describe('Fault Management Visual Tests', () => {
|
||||
});
|
||||
|
||||
test('ability to acknowledge or shelve', async ({ page, theme }) => {
|
||||
await utils.navigateToFaultManagementWithStaticExample(page);
|
||||
|
||||
await utils.selectFaultItem(page, 1);
|
||||
await selectFaultItem(page, 1);
|
||||
|
||||
await percySnapshot(
|
||||
page,
|
||||
|
||||
93
e2e/tests/visual-a11y/imagery.visual.spec.js
Normal file
93
e2e/tests/visual-a11y/imagery.visual.spec.js
Normal file
@@ -0,0 +1,93 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import percySnapshot from '@percy/playwright';
|
||||
|
||||
import { createDomainObjectWithDefaults, setRealTimeMode } from '../../appActions.js';
|
||||
import { VISUAL_URL } from '../../constants.js';
|
||||
import { expect, test } from '../../pluginFixtures.js';
|
||||
|
||||
test.describe('Visual - Example Imagery', () => {
|
||||
let exampleImagery;
|
||||
let parentLayout;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
parentLayout = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Display Layout',
|
||||
name: 'Parent Layout'
|
||||
});
|
||||
|
||||
exampleImagery = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Example Imagery',
|
||||
name: 'Example Imagery Test',
|
||||
parent: parentLayout.uuid
|
||||
});
|
||||
|
||||
// Modify Example Imagery to create a really stable Example Imagery
|
||||
await page.goto(exampleImagery.url, { waitUntil: 'domcontentloaded' });
|
||||
await page.getByRole('button', { name: 'More actions' }).click();
|
||||
await page.getByRole('menuitem', { name: 'Edit Properties...' }).click();
|
||||
await page
|
||||
.locator('#imageLocation-textarea')
|
||||
.fill(
|
||||
'https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg,https://www.nasa.gov/wp-content/uploads/static/history/alsj/a16/AS16-117-18731.jpg'
|
||||
);
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
await page.getByTitle('Collapse Browse Pane').click();
|
||||
await page.getByTitle('Collapse Inspect Pane').click();
|
||||
});
|
||||
|
||||
test('Example Imagery in Fixed Time', async ({ page, theme }) => {
|
||||
await page.goto(exampleImagery.url, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
await expect(page.getByLabel('Image Wrapper')).toBeVisible();
|
||||
|
||||
await percySnapshot(page, `Example Imagery in Fixed Time (theme: ${theme})`);
|
||||
|
||||
await page.getByLabel('Image Wrapper').hover();
|
||||
|
||||
await percySnapshot(page, `Example Imagery Hover in Fixed Time (theme: ${theme})`);
|
||||
});
|
||||
|
||||
test('Example Imagery in Real Time', async ({ page, theme }) => {
|
||||
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 percySnapshot(page, `Example Imagery in Real Time (theme: ${theme})`);
|
||||
});
|
||||
|
||||
test('Example Imagery in Display Layout', async ({ page, theme }) => {
|
||||
await page.goto(parentLayout.url, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
await expect(page.getByLabel('Image Wrapper')).toBeVisible();
|
||||
|
||||
await percySnapshot(page, `Example Imagery in Display Layout (theme: ${theme})`);
|
||||
});
|
||||
});
|
||||
57
e2e/tests/visual-a11y/missionStatus.visual.spec.js
Normal file
57
e2e/tests/visual-a11y/missionStatus.visual.spec.js
Normal file
@@ -0,0 +1,57 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import percySnapshot from '@percy/playwright';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
import { expect, scanForA11yViolations, test } from '../../avpFixtures.js';
|
||||
|
||||
test.describe('Mission Status Visual Tests @a11y', () => {
|
||||
const GO = '1';
|
||||
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();
|
||||
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();
|
||||
await expect(page.getByRole('dialog', { name: 'User Control Panel' })).toBeVisible();
|
||||
await percySnapshot(page, `Mission status panel w/ default statuses (theme: '${theme}')`);
|
||||
await page.getByRole('combobox', { name: 'Commanding' }).selectOption(GO);
|
||||
await expect(
|
||||
page.getByRole('alert').filter({ hasText: 'Successfully set mission status' })
|
||||
).toBeVisible();
|
||||
await page.getByLabel('Dismiss').click();
|
||||
await percySnapshot(page, `Mission status panel w/ non-default status (theme: '${theme}')`);
|
||||
});
|
||||
|
||||
test.afterEach(async ({ page }, testInfo) => {
|
||||
await scanForA11yViolations(page, testInfo.title);
|
||||
});
|
||||
});
|
||||
@@ -23,11 +23,11 @@
|
||||
import percySnapshot from '@percy/playwright';
|
||||
|
||||
import { createDomainObjectWithDefaults, expandTreePaneItemByName } from '../../appActions.js';
|
||||
import { scanForA11yViolations, test } from '../../avpFixtures.js';
|
||||
import { test } from '../../avpFixtures.js';
|
||||
import { VISUAL_URL } from '../../constants.js';
|
||||
import { enterTextEntry, startAndAddRestrictedNotebookObject } from '../../helper/notebookUtils.js';
|
||||
|
||||
test.describe('Visual - Restricted Notebook', () => {
|
||||
test.describe('Visual - Restricted Notebook @a11y', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
const restrictedNotebook = await startAndAddRestrictedNotebookObject(page);
|
||||
await page.goto(restrictedNotebook.url + '?hideTree=true&hideInspector=true');
|
||||
@@ -39,7 +39,7 @@ test.describe('Visual - Restricted Notebook', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Visual - Notebook', () => {
|
||||
test.describe('Visual - Notebook @a11y', () => {
|
||||
let notebook;
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
|
||||
@@ -125,7 +125,8 @@ test.describe('Visual - Notebook', () => {
|
||||
// Take a snapshot
|
||||
await percySnapshot(page, `Notebook Selected Entry Text Area Active (theme: '${theme}')`);
|
||||
});
|
||||
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);
|
||||
// });
|
||||
});
|
||||
|
||||
@@ -24,17 +24,45 @@ import percySnapshot from '@percy/playwright';
|
||||
import fs from 'fs';
|
||||
|
||||
import { createDomainObjectWithDefaults, createPlanFromJSON } from '../../appActions.js';
|
||||
import { scanForA11yViolations, test } from '../../avpFixtures.js';
|
||||
import { test } from '../../avpFixtures.js';
|
||||
import { VISUAL_URL } from '../../constants.js';
|
||||
import { setBoundsToSpanAllActivities, setDraftStatusForPlan } from '../../helper/planningUtils.js';
|
||||
import {
|
||||
createTimelistWithPlanAndSetActivityInProgress,
|
||||
getFirstActivity,
|
||||
setBoundsToSpanAllActivities,
|
||||
setDraftStatusForPlan
|
||||
} from '../../helper/planningUtils.js';
|
||||
|
||||
const examplePlanSmall = JSON.parse(
|
||||
const examplePlanSmall1 = JSON.parse(
|
||||
fs.readFileSync(new URL('../../test-data/examplePlans/ExamplePlan_Small1.json', import.meta.url))
|
||||
);
|
||||
|
||||
const examplePlanSmall2 = JSON.parse(
|
||||
fs.readFileSync(new URL('../../test-data/examplePlans/ExamplePlan_Small2.json', import.meta.url))
|
||||
);
|
||||
|
||||
const snapshotScope = '.l-shell__pane-main .l-pane__contents';
|
||||
test.describe('Visual - Timelist progress bar @clock', () => {
|
||||
const firstActivity = getFirstActivity(examplePlanSmall1);
|
||||
|
||||
test.describe('Visual - Planning @a11y', () => {
|
||||
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})`);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Visual - Planning', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
|
||||
});
|
||||
@@ -42,42 +70,57 @@ test.describe('Visual - Planning @a11y', () => {
|
||||
test('Plan View', async ({ page, theme }) => {
|
||||
const plan = await createPlanFromJSON(page, {
|
||||
name: 'Plan Visual Test',
|
||||
json: examplePlanSmall
|
||||
json: examplePlanSmall2
|
||||
});
|
||||
|
||||
await setBoundsToSpanAllActivities(page, examplePlanSmall, plan.url);
|
||||
await percySnapshot(page, `Plan View (theme: ${theme})`, {
|
||||
scope: snapshotScope
|
||||
});
|
||||
await setBoundsToSpanAllActivities(page, examplePlanSmall2, plan.url);
|
||||
await percySnapshot(page, `Plan View (theme: ${theme})`);
|
||||
});
|
||||
|
||||
test('Plan View w/ draft status', async ({ page, theme }) => {
|
||||
const plan = await createPlanFromJSON(page, {
|
||||
name: 'Plan Visual Test (Draft)',
|
||||
json: examplePlanSmall
|
||||
json: examplePlanSmall2
|
||||
});
|
||||
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
|
||||
await setDraftStatusForPlan(page, plan);
|
||||
|
||||
await setBoundsToSpanAllActivities(page, examplePlanSmall, plan.url);
|
||||
await percySnapshot(page, `Plan View w/ draft status (theme: ${theme})`, {
|
||||
scope: snapshotScope
|
||||
});
|
||||
await setBoundsToSpanAllActivities(page, examplePlanSmall2, plan.url);
|
||||
await percySnapshot(page, `Plan View w/ draft status (theme: ${theme})`);
|
||||
});
|
||||
});
|
||||
|
||||
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: examplePlanSmall,
|
||||
json: examplePlanSmall2,
|
||||
parent: ganttChart.uuid
|
||||
});
|
||||
await setBoundsToSpanAllActivities(page, examplePlanSmall, ganttChart.url);
|
||||
await percySnapshot(page, `Gantt Chart View (theme: ${theme})`, {
|
||||
scope: snapshotScope
|
||||
});
|
||||
await setBoundsToSpanAllActivities(page, examplePlanSmall2, ganttChart.url);
|
||||
await percySnapshot(page, `Gantt Chart View (theme: ${theme}) - Clipped Activity Names`);
|
||||
|
||||
// Expand the inspect pane and uncheck the 'Clip Activity Names' option
|
||||
await page.getByRole('button', { name: 'Expand Inspect Pane' }).click();
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
await page.getByLabel('Edit Object').click();
|
||||
await page.getByLabel('Clip Activity Names').click();
|
||||
|
||||
// Close the inspect pane and save the changes
|
||||
await page.getByRole('button', { name: 'Collapse Inspect Pane' }).click();
|
||||
await page.getByLabel('Save').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
// Dismiss the notification
|
||||
await page.getByLabel('Dismiss').click();
|
||||
|
||||
await percySnapshot(page, `Gantt Chart View (theme: ${theme}) - Unclipped Activity Names`);
|
||||
});
|
||||
|
||||
test('Gantt Chart View w/ draft status', async ({ page, theme }) => {
|
||||
@@ -86,7 +129,7 @@ test.describe('Visual - Planning @a11y', () => {
|
||||
name: 'Gantt Chart Visual Test (Draft)'
|
||||
});
|
||||
const plan = await createPlanFromJSON(page, {
|
||||
json: examplePlanSmall,
|
||||
json: examplePlanSmall2,
|
||||
parent: ganttChart.uuid
|
||||
});
|
||||
|
||||
@@ -94,12 +137,31 @@ test.describe('Visual - Planning @a11y', () => {
|
||||
|
||||
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
await setBoundsToSpanAllActivities(page, examplePlanSmall, ganttChart.url);
|
||||
await percySnapshot(page, `Gantt Chart View w/ draft status (theme: ${theme})`, {
|
||||
scope: snapshotScope
|
||||
});
|
||||
});
|
||||
test.afterEach(async ({ page }, testInfo) => {
|
||||
await scanForA11yViolations(page, testInfo.title);
|
||||
await setBoundsToSpanAllActivities(page, examplePlanSmall2, ganttChart.url);
|
||||
await percySnapshot(page, `Gantt Chart View w/ draft status (theme: ${theme})`);
|
||||
|
||||
// Expand the inspect pane and uncheck the 'Clip Activity Names' option
|
||||
await page.getByRole('button', { name: 'Expand Inspect Pane' }).click();
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
await page.getByLabel('Edit Object').click();
|
||||
await page.getByLabel('Clip Activity Names').click();
|
||||
|
||||
// Close the inspect pane and save the changes
|
||||
await page.getByRole('button', { name: 'Collapse Inspect Pane' }).click();
|
||||
await page.getByLabel('Save').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
// Dismiss the notification
|
||||
await page.getByLabel('Dismiss').click();
|
||||
|
||||
await percySnapshot(
|
||||
page,
|
||||
`Gantt Chart View w/ draft status (theme: ${theme}) - Unclipped Activity Names`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// Skipping for https://github.com/nasa/openmct/issues/7421
|
||||
// test.afterEach(async ({ page }, testInfo) => {
|
||||
// await scanForA11yViolations(page, testInfo.title);
|
||||
// });
|
||||
|
||||
@@ -91,7 +91,7 @@ test.describe('Flexible Layout styling @a11y', () => {
|
||||
setBorderColor,
|
||||
setBackgroundColor,
|
||||
setTextColor,
|
||||
page.getByLabel('StackedPlot1 Frame')
|
||||
page.getByRole('group', { name: 'StackedPlot1 Frame' })
|
||||
);
|
||||
|
||||
await percySnapshot(
|
||||
|
||||
@@ -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.getByRole('button', { name: 'Plot' }).click();
|
||||
await page.getByLabel('Open the View Switcher Menu').click();
|
||||
await page.getByLabel('Telemetry Table').click();
|
||||
|
||||
//Get Table View in place
|
||||
expect(await page.getByLabel('Expand Columns')).toBeInViewport();
|
||||
await expect(page.getByLabel('Expand Columns')).toBeInViewport();
|
||||
|
||||
await percySnapshot(page, `Default Telemetry Table View (theme: ${theme})`);
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const ONE_HOUR = 60 * 60 * 1000;
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject'],
|
||||
data() {
|
||||
@@ -77,6 +78,10 @@ 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,
|
||||
@@ -88,6 +93,9 @@ export default {
|
||||
icon: item.type.cssClass
|
||||
},
|
||||
dataRanges: [
|
||||
{
|
||||
bounds: otherBounds
|
||||
},
|
||||
{
|
||||
bounds
|
||||
}
|
||||
|
||||
@@ -60,10 +60,22 @@ const STATUSES = [
|
||||
statusFgColor: '#fff'
|
||||
}
|
||||
];
|
||||
|
||||
const MISSION_STATUSES = [
|
||||
{
|
||||
key: 0,
|
||||
label: 'NO GO'
|
||||
},
|
||||
{
|
||||
key: 1,
|
||||
label: 'GO'
|
||||
}
|
||||
];
|
||||
/**
|
||||
* @implements {StatusUserProvider}
|
||||
*/
|
||||
export default class ExampleUserProvider extends EventEmitter {
|
||||
#actionToStatusMap;
|
||||
constructor(
|
||||
openmct,
|
||||
{ statusRoles } = {
|
||||
@@ -73,6 +85,11 @@ export default class ExampleUserProvider extends EventEmitter {
|
||||
super();
|
||||
|
||||
this.openmct = openmct;
|
||||
this.#actionToStatusMap = {
|
||||
Imagery: MISSION_STATUSES[0],
|
||||
Commanding: MISSION_STATUSES[0],
|
||||
Driving: MISSION_STATUSES[0]
|
||||
};
|
||||
this.user = undefined;
|
||||
this.loggedIn = false;
|
||||
this.autoLoginUser = undefined;
|
||||
@@ -110,6 +127,11 @@ export default class ExampleUserProvider extends EventEmitter {
|
||||
canSetPollQuestion() {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
canSetMissionStatus() {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
hasRole(roleId) {
|
||||
if (!this.loggedIn) {
|
||||
Promise.resolve(undefined);
|
||||
@@ -122,6 +144,28 @@ export default class ExampleUserProvider extends EventEmitter {
|
||||
return this.user.getRoles();
|
||||
}
|
||||
|
||||
getPossibleMissionActions() {
|
||||
return Promise.resolve(Object.keys(this.#actionToStatusMap));
|
||||
}
|
||||
|
||||
getPossibleMissionActionStatuses() {
|
||||
return Promise.resolve(MISSION_STATUSES);
|
||||
}
|
||||
|
||||
getStatusForMissionAction(action) {
|
||||
return Promise.resolve(this.#actionToStatusMap[action]);
|
||||
}
|
||||
|
||||
setStatusForMissionAction(action, status) {
|
||||
this.#actionToStatusMap[action] = status;
|
||||
this.emit('missionStatusChange', {
|
||||
action,
|
||||
status
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
getAllStatusRoles() {
|
||||
return Promise.resolve(this.statusRoles);
|
||||
}
|
||||
|
||||
@@ -20,13 +20,13 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import utils from './utils.js';
|
||||
import { acknowledgeFault, randomFaults, shelveFault } from './utils.js';
|
||||
|
||||
export default function (staticFaults = false) {
|
||||
return function install(openmct) {
|
||||
openmct.install(openmct.plugins.FaultManagement());
|
||||
|
||||
const faultsData = utils.randomFaults(staticFaults);
|
||||
const faultsData = randomFaults(staticFaults);
|
||||
|
||||
openmct.faults.addProvider({
|
||||
request(domainObject, options) {
|
||||
@@ -44,14 +44,14 @@ export default function (staticFaults = false) {
|
||||
return domainObject.type === 'faultManagement';
|
||||
},
|
||||
acknowledgeFault(fault, { comment = '' }) {
|
||||
utils.acknowledgeFault(fault);
|
||||
acknowledgeFault(fault);
|
||||
|
||||
return Promise.resolve({
|
||||
success: true
|
||||
});
|
||||
},
|
||||
shelveFault(fault, duration) {
|
||||
utils.shelveFault(fault, duration);
|
||||
shelveFault(fault, duration);
|
||||
|
||||
return Promise.resolve({
|
||||
success: true
|
||||
|
||||
@@ -43,7 +43,7 @@ const getRandom = {
|
||||
}
|
||||
};
|
||||
|
||||
function shelveFault(
|
||||
export function shelveFault(
|
||||
fault,
|
||||
opts = {
|
||||
shelved: true,
|
||||
@@ -58,11 +58,11 @@ function shelveFault(
|
||||
}, opts.shelveDuration);
|
||||
}
|
||||
|
||||
function acknowledgeFault(fault) {
|
||||
export function acknowledgeFault(fault) {
|
||||
fault.acknowledged = true;
|
||||
}
|
||||
|
||||
function randomFaults(staticFaults, count = 5) {
|
||||
export function randomFaults(staticFaults, count = 5) {
|
||||
let faults = [];
|
||||
|
||||
for (let x = 1, y = count + 1; x < y; x++) {
|
||||
@@ -71,9 +71,3 @@ function randomFaults(staticFaults, count = 5) {
|
||||
|
||||
return faults;
|
||||
}
|
||||
|
||||
export default {
|
||||
randomFaults,
|
||||
shelveFault,
|
||||
acknowledgeFault
|
||||
};
|
||||
|
||||
@@ -92,6 +92,8 @@ GeneratorProvider.prototype.request = function (domainObject, request) {
|
||||
var workerRequest = this.makeWorkerRequest(domainObject, request);
|
||||
workerRequest.start = request.start;
|
||||
workerRequest.end = request.end;
|
||||
workerRequest.size = request.size;
|
||||
workerRequest.strategy = request.strategy;
|
||||
|
||||
return this.workerInterface.request(workerRequest);
|
||||
};
|
||||
|
||||
@@ -130,48 +130,37 @@
|
||||
var now = Date.now();
|
||||
var start = request.start;
|
||||
var end = request.end > now ? now : request.end;
|
||||
var amplitude = request.amplitude;
|
||||
var period = request.period;
|
||||
var offset = request.offset;
|
||||
var dataRateInHz = request.dataRateInHz;
|
||||
var phase = request.phase;
|
||||
var randomness = request.randomness;
|
||||
var loadDelay = Math.max(request.loadDelay, 0);
|
||||
var infinityValues = request.infinityValues;
|
||||
var exceedFloat32 = request.exceedFloat32;
|
||||
|
||||
var size = request.size;
|
||||
var duration = end - start;
|
||||
var step = 1000 / dataRateInHz;
|
||||
var maxPoints = Math.floor(duration / step);
|
||||
var nextStep = start - (start % step) + step;
|
||||
|
||||
var data = [];
|
||||
|
||||
for (; nextStep < end && data.length < 5000; nextStep += step) {
|
||||
data.push({
|
||||
utc: nextStep,
|
||||
yesterday: nextStep - 60 * 60 * 24 * 1000,
|
||||
sin: sin(
|
||||
nextStep,
|
||||
period,
|
||||
amplitude,
|
||||
offset,
|
||||
phase,
|
||||
randomness,
|
||||
infinityValues,
|
||||
exceedFloat32
|
||||
),
|
||||
wavelengths: wavelengths(),
|
||||
intensities: intensities(),
|
||||
cos: cos(
|
||||
nextStep,
|
||||
period,
|
||||
amplitude,
|
||||
offset,
|
||||
phase,
|
||||
randomness,
|
||||
infinityValues,
|
||||
exceedFloat32
|
||||
)
|
||||
});
|
||||
if (request.strategy === 'minmax' && size) {
|
||||
// Calculate the number of cycles to include based on size (2 points per cycle)
|
||||
var totalCycles = Math.min(Math.floor(size / 2), Math.floor(duration / period));
|
||||
|
||||
for (let cycle = 0; cycle < totalCycles; cycle++) {
|
||||
// Distribute cycles evenly across the time range
|
||||
let cycleStart = start + (duration / totalCycles) * cycle;
|
||||
let minPointTime = cycleStart; // Assuming min at the start of the cycle
|
||||
let maxPointTime = cycleStart + period / 2; // Assuming max at the halfway of the cycle
|
||||
|
||||
data.push(createDataPoint(minPointTime, request), createDataPoint(maxPointTime, request));
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < maxPoints && nextStep < end; i++, nextStep += step) {
|
||||
data.push(createDataPoint(nextStep, request));
|
||||
}
|
||||
}
|
||||
|
||||
if (request.strategy !== 'minmax' && size) {
|
||||
data = data.slice(-size);
|
||||
}
|
||||
|
||||
if (loadDelay === 0) {
|
||||
@@ -181,6 +170,35 @@
|
||||
}
|
||||
}
|
||||
|
||||
function createDataPoint(time, request) {
|
||||
return {
|
||||
utc: time,
|
||||
yesterday: time - 60 * 60 * 24 * 1000,
|
||||
sin: sin(
|
||||
time,
|
||||
request.period,
|
||||
request.amplitude,
|
||||
request.offset,
|
||||
request.phase,
|
||||
request.randomness,
|
||||
request.infinityValues,
|
||||
request.exceedFloat32
|
||||
),
|
||||
wavelengths: wavelengths(),
|
||||
intensities: intensities(),
|
||||
cos: cos(
|
||||
time,
|
||||
request.period,
|
||||
request.amplitude,
|
||||
request.offset,
|
||||
request.phase,
|
||||
request.randomness,
|
||||
request.infinityValues,
|
||||
request.exceedFloat32
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
function postOnRequest(message, request, data) {
|
||||
self.postMessage({
|
||||
id: message.id,
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -47,7 +47,7 @@ if (document.currentScript) {
|
||||
* @property {*} inspectorViews
|
||||
* @property {*} propertyEditors
|
||||
* @property {*} toolbars
|
||||
* @property {*} types
|
||||
* @property {import('./src/api/types/TypeRegistry').default} 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,6 +67,7 @@ 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
|
||||
|
||||
51
package.json
51
package.json
@@ -1,17 +1,18 @@
|
||||
{
|
||||
"name": "openmct",
|
||||
"version": "3.3.0-next",
|
||||
"version": "4.0.0-next",
|
||||
"description": "The Open MCT core platform",
|
||||
"type": "module",
|
||||
"main": "dist/openmct.js",
|
||||
"devDependencies": {
|
||||
"@axe-core/playwright": "4.8.2",
|
||||
"@axe-core/playwright": "4.8.5",
|
||||
"@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",
|
||||
@@ -22,28 +23,29 @@
|
||||
"babel-plugin-istanbul": "6.1.1",
|
||||
"codecov": "3.8.3",
|
||||
"comma-separated-values": "3.6.4",
|
||||
"copy-webpack-plugin": "11.0.0",
|
||||
"copy-webpack-plugin": "12.0.2",
|
||||
"cspell": "7.3.8",
|
||||
"css-loader": "6.8.1",
|
||||
"css-loader": "6.10.0",
|
||||
"d3-axis": "3.0.0",
|
||||
"d3-shape": "3.0.0",
|
||||
"d3-scale": "4.0.2",
|
||||
"d3-selection": "3.0.0",
|
||||
"eslint": "8.54.0",
|
||||
"eslint-config-prettier": "9.0.0",
|
||||
"eslint": "8.56.0",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"eslint-plugin-compat": "4.2.0",
|
||||
"eslint-plugin-no-unsanitized": "4.0.2",
|
||||
"eslint-plugin-playwright": "0.12.0",
|
||||
"eslint-plugin-prettier": "4.2.1",
|
||||
"eslint-plugin-prettier": "5.1.3",
|
||||
"eslint-plugin-simple-import-sort": "10.0.0",
|
||||
"eslint-plugin-unicorn": "49.0.0",
|
||||
"eslint-plugin-vue": "9.18.1",
|
||||
"eslint-plugin-vue": "9.22.0",
|
||||
"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": "4.0.1",
|
||||
"imports-loader": "5.0.0",
|
||||
"jasmine-core": "5.1.1",
|
||||
"karma": "6.4.2",
|
||||
"karma-chrome-launcher": "3.2.0",
|
||||
@@ -54,37 +56,38 @@
|
||||
"karma-junit-reporter": "2.0.1",
|
||||
"karma-sourcemap-loader": "0.4.0",
|
||||
"karma-spec-reporter": "0.0.36",
|
||||
"karma-webpack": "5.0.0",
|
||||
"karma-webpack": "5.0.1",
|
||||
"location-bar": "3.0.1",
|
||||
"lodash": "4.17.21",
|
||||
"marked": "11.1.0",
|
||||
"mini-css-extract-plugin": "2.7.6",
|
||||
"moment": "2.29.4",
|
||||
"marked": "12.0.0",
|
||||
"mini-css-extract-plugin": "2.8.1",
|
||||
"moment": "2.30.1",
|
||||
"moment-duration-format": "2.3.2",
|
||||
"moment-timezone": "0.5.41",
|
||||
"npm-run-all2": "6.1.1",
|
||||
"npm-run-all2": "6.1.2",
|
||||
"nyc": "15.1.0",
|
||||
"painterro": "1.2.87",
|
||||
"plotly.js-basic-dist-min": "2.20.0",
|
||||
"plotly.js-basic-dist-min": "2.29.1",
|
||||
"plotly.js-gl2d-dist-min": "2.20.0",
|
||||
"prettier": "2.8.7",
|
||||
"prettier": "3.2.5",
|
||||
"prettier-eslint": "16.3.0",
|
||||
"printj": "1.3.1",
|
||||
"resolve-url-loader": "5.0.0",
|
||||
"sanitize-html": "2.11.0",
|
||||
"sass": "1.68.0",
|
||||
"sass-loader": "13.3.2",
|
||||
"sanitize-html": "2.12.1",
|
||||
"sass": "1.71.1",
|
||||
"sass-loader": "14.1.1",
|
||||
"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.3.8",
|
||||
"vue-eslint-parser": "9.3.2",
|
||||
"vue": "3.4.19",
|
||||
"vue-eslint-parser": "9.4.2",
|
||||
"vue-loader": "16.8.3",
|
||||
"webpack": "5.89.0",
|
||||
"webpack": "5.90.3",
|
||||
"webpack-cli": "5.1.1",
|
||||
"webpack-dev-server": "4.15.1",
|
||||
"webpack-dev-server": "5.0.2",
|
||||
"webpack-merge": "5.10.0"
|
||||
},
|
||||
"scripts": {
|
||||
@@ -106,11 +109,13 @@
|
||||
"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",
|
||||
"test:e2e:local": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome",
|
||||
"test:e2e:generatedata": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @generatedata",
|
||||
"test:e2e:checksnapshots": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @snapshot --retries=0",
|
||||
"test:e2e:updatesnapshots": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @snapshot --update-snapshots",
|
||||
"test:e2e:visual:ci": "percy exec --config ./e2e/.percy.ci.yml --partial -- npx playwright test --config=e2e/playwright-visual-a11y.config.js --project=chrome --grep-invert @unstable",
|
||||
"test:e2e:visual:full": "percy exec --config ./e2e/.percy.nightly.yml -- npx playwright test --config=e2e/playwright-visual-a11y.config.js --grep-invert @unstable",
|
||||
|
||||
@@ -251,6 +251,7 @@ export class MCT extends EventEmitter {
|
||||
this.install(this.plugins.FlexibleLayout());
|
||||
this.install(this.plugins.GoToOriginalAction());
|
||||
this.install(this.plugins.OpenInNewTabAction());
|
||||
this.install(this.plugins.ReloadAction());
|
||||
this.install(this.plugins.WebPage());
|
||||
this.install(this.plugins.Condition());
|
||||
this.install(this.plugins.ConditionWidget());
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
*****************************************************************************/
|
||||
import _ from 'lodash';
|
||||
|
||||
import objectUtils from '../objects/object-utils.js';
|
||||
import { makeKeyString, parseKeyString } 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 = objectUtils.makeKeyString(oldDomainObject.identifier);
|
||||
const id = makeKeyString(oldDomainObject.identifier);
|
||||
const listeners = this.#listeningTo[id];
|
||||
|
||||
if (!listeners) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldComposition = oldDomainObject.composition.map(objectUtils.makeKeyString);
|
||||
const newComposition = newDomainObject.composition.map(objectUtils.makeKeyString);
|
||||
const oldComposition = oldDomainObject.composition.map(makeKeyString);
|
||||
const newComposition = newDomainObject.composition.map(makeKeyString);
|
||||
|
||||
const added = _.difference(newComposition, oldComposition).map(objectUtils.parseKeyString);
|
||||
const removed = _.difference(oldComposition, newComposition).map(objectUtils.parseKeyString);
|
||||
const added = _.difference(newComposition, oldComposition).map(parseKeyString);
|
||||
const removed = _.difference(oldComposition, newComposition).map(parseKeyString);
|
||||
|
||||
function notify(value) {
|
||||
return function (listener) {
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
*****************************************************************************/
|
||||
import { toRaw } from 'vue';
|
||||
|
||||
import objectUtils from '../objects/object-utils.js';
|
||||
import { makeKeyString } 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 = objectUtils.makeKeyString(domainObject.identifier);
|
||||
const keyString = 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 = objectUtils.makeKeyString(domainObject.identifier);
|
||||
const keyString = 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 = objectUtils.makeKeyString(domainObject.identifier);
|
||||
let id = makeKeyString(domainObject.identifier);
|
||||
const listeners = this.listeningTo[id];
|
||||
|
||||
if (!listeners) {
|
||||
|
||||
@@ -28,10 +28,14 @@
|
||||
v-for="action in actionGroups"
|
||||
:key="action.name"
|
||||
role="menuitem"
|
||||
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
|
||||
:aria-disabled="action.isDisabled"
|
||||
:aria-label="action.name"
|
||||
aria-describedby="item-description"
|
||||
:class="action.cssClass"
|
||||
:title="action.description"
|
||||
@click="action.onItemClicked"
|
||||
@mouseover="toggleItem(action)"
|
||||
@mouseleave="toggleItem()"
|
||||
>
|
||||
{{ action.name }}
|
||||
</li>
|
||||
@@ -51,15 +55,23 @@
|
||||
v-for="action in options.actions"
|
||||
:key="action.name"
|
||||
role="menuitem"
|
||||
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
|
||||
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>
|
||||
|
||||
@@ -68,11 +80,21 @@ 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} Menu` : 'Menu';
|
||||
const label = this.options.label ? `${this.options.label} Context Menu` : 'Context Menu';
|
||||
return label;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleItem(action) {
|
||||
this.hoveredItem = action ?? null;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -37,8 +37,9 @@
|
||||
v-for="action in actionGroups"
|
||||
:key="action.name"
|
||||
role="menuitem"
|
||||
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
|
||||
:title="action.description"
|
||||
:aria-disabled="action.isDisabled"
|
||||
aria-describedby="item-description"
|
||||
:class="action.cssClass"
|
||||
@click="action.onItemClicked"
|
||||
@mouseover="toggleItemDescription(action)"
|
||||
@mouseleave="toggleItemDescription()"
|
||||
@@ -63,7 +64,7 @@
|
||||
role="menuitem"
|
||||
:class="action.cssClass"
|
||||
:aria-label="action.name"
|
||||
:title="action.description"
|
||||
aria-describedby="item-description"
|
||||
@click="action.onItemClicked"
|
||||
@mouseover="toggleItemDescription(action)"
|
||||
@mouseleave="toggleItemDescription()"
|
||||
@@ -73,13 +74,13 @@
|
||||
<li v-if="options.actions.length === 0">No actions defined.</li>
|
||||
</ul>
|
||||
|
||||
<div class="c-super-menu__item-description">
|
||||
<div :class="['l-item-description__icon', 'bg-' + hoveredItem.cssClass]"></div>
|
||||
<div aria-live="polite" class="c-super-menu__item-description">
|
||||
<div :class="itemDescriptionIconClass"></div>
|
||||
<div class="l-item-description__name">
|
||||
{{ hoveredItem.name }}
|
||||
{{ hoveredItemName }}
|
||||
</div>
|
||||
<div class="l-item-description__description">
|
||||
{{ hoveredItem.description }}
|
||||
<div id="item-description" class="l-item-description__description">
|
||||
{{ hoveredItemDescription }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -89,26 +90,39 @@ import popupMenuMixin from '../mixins/popupMenuMixin.js';
|
||||
export default {
|
||||
mixins: [popupMenuMixin],
|
||||
inject: ['options'],
|
||||
data: function () {
|
||||
data() {
|
||||
return {
|
||||
hoveredItem: {}
|
||||
hoveredItem: null
|
||||
};
|
||||
},
|
||||
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 = {}) {
|
||||
toggleItemDescription(action = null) {
|
||||
const hoveredItem = {
|
||||
name: action.name,
|
||||
description: action.description,
|
||||
cssClass: action.cssClass
|
||||
name: action?.name,
|
||||
description: action?.description,
|
||||
cssClass: action?.cssClass
|
||||
};
|
||||
|
||||
this.hoveredItem = Object.assign({}, this.hoveredItem, hoveredItem);
|
||||
this.hoveredItem = hoveredItem;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
import EventEmitter from 'EventEmitter';
|
||||
import _ from 'lodash';
|
||||
|
||||
import utils from './object-utils.js';
|
||||
import { makeKeyString, refresh } 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));
|
||||
utils.refresh(mutable, clone);
|
||||
refresh(mutable, clone);
|
||||
});
|
||||
|
||||
return mutable;
|
||||
@@ -168,7 +168,7 @@ class MutableDomainObject {
|
||||
}
|
||||
|
||||
function qualifiedEventName(object, eventName) {
|
||||
let keystring = utils.makeKeyString(object.identifier);
|
||||
let keystring = makeKeyString(object.identifier);
|
||||
|
||||
return [keystring, eventName].join(':');
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
*****************************************************************************/
|
||||
|
||||
import EventEmitter from 'EventEmitter';
|
||||
import utils from 'objectUtils';
|
||||
import { identifierEquals, makeKeyString, parseKeyString, refresh } from 'objectUtils';
|
||||
|
||||
import ConflictError from './ConflictError.js';
|
||||
import InMemorySearchProvider from './InMemorySearchProvider.js';
|
||||
@@ -82,8 +82,19 @@ import Transaction from './Transaction.js';
|
||||
* @memberof module:openmct
|
||||
*/
|
||||
export default class ObjectAPI {
|
||||
#makeKeyString;
|
||||
#parseKeyString;
|
||||
#identifierEquals;
|
||||
#refresh;
|
||||
#openmct;
|
||||
|
||||
constructor(typeRegistry, openmct) {
|
||||
this.openmct = openmct;
|
||||
this.#makeKeyString = makeKeyString;
|
||||
this.#parseKeyString = parseKeyString;
|
||||
this.#identifierEquals = identifierEquals;
|
||||
this.#refresh = refresh;
|
||||
this.#openmct = openmct;
|
||||
|
||||
this.typeRegistry = typeRegistry;
|
||||
this.SEARCH_TYPES = Object.freeze({
|
||||
OBJECTS: 'OBJECTS',
|
||||
@@ -99,7 +110,13 @@ export default class ObjectAPI {
|
||||
this.cache = {};
|
||||
this.interceptorRegistry = new InterceptorRegistry();
|
||||
|
||||
this.SYNCHRONIZED_OBJECT_TYPES = ['notebook', 'restricted-notebook', 'plan', 'annotation'];
|
||||
this.SYNCHRONIZED_OBJECT_TYPES = [
|
||||
'notebook',
|
||||
'restricted-notebook',
|
||||
'plan',
|
||||
'annotation',
|
||||
'activity-states'
|
||||
];
|
||||
|
||||
this.errors = {
|
||||
Conflict: ConflictError
|
||||
@@ -200,14 +217,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 = utils.parseKeyString(identifier);
|
||||
identifier = parseKeyString(identifier);
|
||||
|
||||
if (this.isTransactionActive()) {
|
||||
let dirtyObject = this.transaction.getDirtyObject(identifier);
|
||||
@@ -221,7 +238,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) {
|
||||
@@ -319,7 +336,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) => {
|
||||
@@ -346,14 +363,17 @@ export default class ObjectAPI {
|
||||
}
|
||||
|
||||
isPersistable(idOrKeyString) {
|
||||
let identifier = utils.parseKeyString(idOrKeyString);
|
||||
let identifier = parseKeyString(idOrKeyString);
|
||||
let provider = this.getProvider(identifier);
|
||||
if (provider?.isReadOnly) {
|
||||
return !provider.isReadOnly();
|
||||
}
|
||||
|
||||
return provider !== undefined && provider.create !== undefined && provider.update !== undefined;
|
||||
}
|
||||
|
||||
isMissing(domainObject) {
|
||||
let identifier = utils.makeKeyString(domainObject.identifier);
|
||||
let identifier = makeKeyString(domainObject.identifier);
|
||||
let missingName = 'Missing: ' + identifier;
|
||||
|
||||
return domainObject.name === missingName;
|
||||
@@ -433,21 +453,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -456,7 +476,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) {
|
||||
@@ -545,7 +565,7 @@ export default class ObjectAPI {
|
||||
*/
|
||||
getRelativePath(objectPath) {
|
||||
return objectPath
|
||||
.map((p) => this.makeKeyString(p.identifier))
|
||||
.map((p) => this.#makeKeyString(p.identifier))
|
||||
.reverse()
|
||||
.join('/');
|
||||
}
|
||||
@@ -565,13 +585,13 @@ export default class ObjectAPI {
|
||||
}
|
||||
|
||||
let sourceTelemetry = null;
|
||||
if (telemetryIdentifier && utils.identifierEquals(identifier, telemetryIdentifier)) {
|
||||
if (telemetryIdentifier && this.#identifierEquals(identifier, telemetryIdentifier)) {
|
||||
sourceTelemetry = identifier;
|
||||
} else if (objectDetails.composition) {
|
||||
sourceTelemetry = objectDetails.composition[0];
|
||||
if (telemetryIdentifier) {
|
||||
sourceTelemetry = objectDetails.composition.find((telemetrySource) =>
|
||||
utils.identifierEquals(telemetrySource, telemetryIdentifier)
|
||||
this.#identifierEquals(telemetrySource, telemetryIdentifier)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -657,7 +677,7 @@ export default class ObjectAPI {
|
||||
mutableObject = MutableDomainObject.createMutable(domainObject, this.eventEmitter);
|
||||
|
||||
// Check if provider supports realtime updates
|
||||
let identifier = utils.parseKeyString(mutableObject.identifier);
|
||||
let identifier = parseKeyString(mutableObject.identifier);
|
||||
let provider = this.getProvider(identifier);
|
||||
|
||||
if (
|
||||
@@ -687,15 +707,17 @@ export default class ObjectAPI {
|
||||
/**
|
||||
* Updates a domain object based on its latest persisted state. Note that this will mutate the provided object.
|
||||
* @param {module:openmct.DomainObject} domainObject an object to refresh from its persistence store
|
||||
* @param {boolean} [forceRemote=false] defaults to false. If true, will skip cached and
|
||||
* dirty/in-transaction objects use and the provider.get method
|
||||
* @returns {Promise} the provided object, updated to reflect the latest persisted state of the object.
|
||||
*/
|
||||
async refresh(domainObject) {
|
||||
const refreshedObject = await this.get(domainObject.identifier);
|
||||
async refresh(domainObject, forceRemote = false) {
|
||||
const refreshedObject = await this.get(domainObject.identifier, null, forceRemote);
|
||||
|
||||
if (domainObject.isMutable) {
|
||||
domainObject.$refresh(refreshedObject);
|
||||
} else {
|
||||
utils.refresh(domainObject, refreshedObject);
|
||||
refresh(domainObject, refreshedObject);
|
||||
}
|
||||
|
||||
return domainObject;
|
||||
@@ -734,7 +756,7 @@ export default class ObjectAPI {
|
||||
* @returns {string} A string representation of the given identifier, including namespace and key
|
||||
*/
|
||||
makeKeyString(identifier) {
|
||||
return utils.makeKeyString(identifier);
|
||||
return makeKeyString(identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -742,7 +764,7 @@ export default class ObjectAPI {
|
||||
* @returns {module:openmct.ObjectAPI~Identifier} An identifier object
|
||||
*/
|
||||
parseKeyString(keyString) {
|
||||
return utils.parseKeyString(keyString);
|
||||
return parseKeyString(keyString);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -750,9 +772,9 @@ export default class ObjectAPI {
|
||||
* @param {module:openmct.ObjectAPI~Identifier[]} identifiers
|
||||
*/
|
||||
areIdsEqual(...identifiers) {
|
||||
const firstIdentifier = utils.parseKeyString(identifiers[0]);
|
||||
const firstIdentifier = this.#parseKeyString(identifiers[0]);
|
||||
|
||||
return identifiers.map(utils.parseKeyString).every((identifier) => {
|
||||
return identifiers.map(this.#parseKeyString).every((identifier) => {
|
||||
return (
|
||||
identifier === firstIdentifier ||
|
||||
(identifier.namespace === firstIdentifier.namespace &&
|
||||
@@ -780,7 +802,7 @@ export default class ObjectAPI {
|
||||
}
|
||||
|
||||
return path.some((pathElement) => {
|
||||
const identifierToCheck = utils.parseKeyString(keyStringToCheck);
|
||||
const identifierToCheck = this.#parseKeyString(keyStringToCheck);
|
||||
|
||||
return this.areIdsEqual(identifierToCheck, pathElement.identifier);
|
||||
});
|
||||
@@ -803,7 +825,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(utils.parseKeyString(location), path, abortSignal);
|
||||
return this.getOriginalPath(this.#parseKeyString(location), path, abortSignal);
|
||||
} else {
|
||||
return path;
|
||||
}
|
||||
@@ -842,8 +864,8 @@ export default class ObjectAPI {
|
||||
await Promise.all(
|
||||
keyStrings.map((keyString) =>
|
||||
this.supportsMutation(keyString)
|
||||
? this.getMutable(utils.parseKeyString(keyString))
|
||||
: this.get(utils.parseKeyString(keyString))
|
||||
? this.getMutable(this.#parseKeyString(keyString))
|
||||
: this.get(this.#parseKeyString(keyString))
|
||||
)
|
||||
)
|
||||
).reverse();
|
||||
@@ -855,7 +877,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)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -362,7 +362,7 @@ describe('The Object API', () => {
|
||||
expect(objectAPI.get).not.toHaveBeenCalled();
|
||||
|
||||
return objectAPI.refresh(testObject).then(() => {
|
||||
expect(objectAPI.get).toHaveBeenCalledWith(testObject.identifier);
|
||||
expect(objectAPI.get).toHaveBeenCalledWith(testObject.identifier, null, false);
|
||||
|
||||
expect(testObject.otherAttribute).toEqual(OTHER_ATTRIBUTE_VALUE);
|
||||
expect(testObject.newAttribute).toEqual(NEW_ATTRIBUTE_VALUE);
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import utils from './object-utils.js';
|
||||
import { isIdentifier } from './object-utils.js';
|
||||
|
||||
export default class RootRegistry {
|
||||
constructor(openmct) {
|
||||
@@ -47,12 +47,12 @@ export default class RootRegistry {
|
||||
}
|
||||
|
||||
_isValid(rootItem) {
|
||||
if (utils.isIdentifier(rootItem) || typeof rootItem === 'function') {
|
||||
if (isIdentifier(rootItem) || typeof rootItem === 'function') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Array.isArray(rootItem)) {
|
||||
return rootItem.every(utils.isIdentifier);
|
||||
return rootItem.every(isIdentifier);
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -47,9 +47,9 @@ export default class Transaction {
|
||||
return Promise.all(promiseArray);
|
||||
}
|
||||
|
||||
createDirtyObjectPromise(object, action) {
|
||||
createDirtyObjectPromise(object, action, ...args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
action(object)
|
||||
action(object, ...args)
|
||||
.then((success) => {
|
||||
const key = this.objectAPI.makeKeyString(object.identifier);
|
||||
|
||||
@@ -75,10 +75,10 @@ export default class Transaction {
|
||||
|
||||
_clear() {
|
||||
const promiseArray = [];
|
||||
const refresh = this.objectAPI.refresh.bind(this.objectAPI);
|
||||
const action = (obj) => this.objectAPI.refresh(obj, true);
|
||||
|
||||
Object.values(this.dirtyObjects).forEach((object) => {
|
||||
promiseArray.push(this.createDirtyObjectPromise(object, refresh));
|
||||
promiseArray.push(this.createDirtyObjectPromise(object, action));
|
||||
});
|
||||
|
||||
return Promise.all(promiseArray);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import utils from 'objectUtils';
|
||||
import { makeKeyString, parseKeyString } from 'objectUtils';
|
||||
|
||||
import Transaction from './Transaction.js';
|
||||
|
||||
@@ -9,7 +9,7 @@ let transaction;
|
||||
describe('Transaction Class', () => {
|
||||
beforeEach(() => {
|
||||
objectAPI = {
|
||||
makeKeyString: (identifier) => utils.makeKeyString(identifier),
|
||||
makeKeyString: (identifier) => 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(utils.parseKeyString).every((identifier) => {
|
||||
return identifiers.map(parseKeyString).every((identifier) => {
|
||||
return (
|
||||
identifier === identifiers[0] ||
|
||||
(identifier.namespace === identifiers[0].namespace &&
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
* Utility for checking if a thing is an Open MCT Identifier.
|
||||
* @private
|
||||
*/
|
||||
function isIdentifier(thing) {
|
||||
export function isIdentifier(thing) {
|
||||
return (
|
||||
typeof thing === 'object' &&
|
||||
Object.prototype.hasOwnProperty.call(thing, 'key') &&
|
||||
@@ -36,7 +36,7 @@ function isIdentifier(thing) {
|
||||
* Utility for checking if a thing is a key string. Not perfect.
|
||||
* @private
|
||||
*/
|
||||
function isKeyString(thing) {
|
||||
export function isKeyString(thing) {
|
||||
return typeof thing === 'string';
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ function isKeyString(thing) {
|
||||
* @param keyString
|
||||
* @returns identifier
|
||||
*/
|
||||
function parseKeyString(keyString) {
|
||||
export function parseKeyString(keyString) {
|
||||
if (isIdentifier(keyString)) {
|
||||
return keyString;
|
||||
}
|
||||
@@ -86,7 +86,7 @@ function parseKeyString(keyString) {
|
||||
* @param identifier
|
||||
* @returns keyString
|
||||
*/
|
||||
function makeKeyString(identifier) {
|
||||
export function makeKeyString(identifier) {
|
||||
if (!identifier) {
|
||||
throw new Error('Cannot make key string from null identifier');
|
||||
}
|
||||
@@ -112,7 +112,7 @@ function makeKeyString(identifier) {
|
||||
* @param domainObject
|
||||
* @returns oldFormatModel
|
||||
*/
|
||||
function toOldFormat(model) {
|
||||
export function toOldFormat(model) {
|
||||
model = JSON.parse(JSON.stringify(model));
|
||||
delete model.identifier;
|
||||
if (model.composition) {
|
||||
@@ -131,7 +131,7 @@ function toOldFormat(model) {
|
||||
* @param keyString
|
||||
* @returns domainObject
|
||||
*/
|
||||
function toNewFormat(model, keyString) {
|
||||
export function toNewFormat(model, keyString) {
|
||||
model = JSON.parse(JSON.stringify(model));
|
||||
model.identifier = parseKeyString(keyString);
|
||||
if (model.composition) {
|
||||
@@ -148,7 +148,7 @@ function toNewFormat(model, keyString) {
|
||||
* @param otherIdentifier
|
||||
* @returns Boolean true if identifiers are equal.
|
||||
*/
|
||||
function identifierEquals(a, b) {
|
||||
export function identifierEquals(a, b) {
|
||||
return a.key === b.key && a.namespace === b.namespace;
|
||||
}
|
||||
|
||||
@@ -160,23 +160,12 @@ function identifierEquals(a, b) {
|
||||
* @param otherDomainOBject
|
||||
* @returns Boolean true if objects are equal.
|
||||
*/
|
||||
function objectEquals(a, b) {
|
||||
export function objectEquals(a, b) {
|
||||
return identifierEquals(a.identifier, b.identifier);
|
||||
}
|
||||
|
||||
function refresh(oldObject, newObject) {
|
||||
export 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
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import objectUtils from 'objectUtils';
|
||||
import { makeKeyString, parseKeyString, toNewFormat, toOldFormat } 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(objectUtils.parseKeyString(keyString)).toEqual(EXPECTATIONS[keyString]);
|
||||
expect(parseKeyString(keyString)).toEqual(EXPECTATIONS[keyString]);
|
||||
});
|
||||
|
||||
it('parses and re-encodes "' + keyString + '"', function () {
|
||||
const identifier = objectUtils.parseKeyString(keyString);
|
||||
expect(objectUtils.makeKeyString(identifier)).toEqual(keyString);
|
||||
const identifier = parseKeyString(keyString);
|
||||
expect(makeKeyString(identifier)).toEqual(keyString);
|
||||
});
|
||||
|
||||
it('is idempotent for "' + keyString + '".', function () {
|
||||
const identifier = objectUtils.parseKeyString(keyString);
|
||||
let again = objectUtils.parseKeyString(identifier);
|
||||
const identifier = parseKeyString(keyString);
|
||||
let again = parseKeyString(identifier);
|
||||
expect(identifier).toEqual(again);
|
||||
again = objectUtils.parseKeyString(again);
|
||||
again = objectUtils.parseKeyString(again);
|
||||
again = parseKeyString(again);
|
||||
again = parseKeyString(again);
|
||||
expect(identifier).toEqual(again);
|
||||
|
||||
let againKeyString = objectUtils.makeKeyString(again);
|
||||
let againKeyString = makeKeyString(again);
|
||||
expect(againKeyString).toEqual(keyString);
|
||||
againKeyString = objectUtils.makeKeyString(againKeyString);
|
||||
againKeyString = objectUtils.makeKeyString(againKeyString);
|
||||
againKeyString = objectUtils.makeKeyString(againKeyString);
|
||||
againKeyString = makeKeyString(againKeyString);
|
||||
againKeyString = makeKeyString(againKeyString);
|
||||
againKeyString = makeKeyString(againKeyString);
|
||||
expect(againKeyString).toEqual(keyString);
|
||||
});
|
||||
});
|
||||
@@ -60,7 +60,7 @@ describe('objectUtils', function () {
|
||||
describe('old object conversions', function () {
|
||||
it('translate ids', function () {
|
||||
expect(
|
||||
objectUtils.toNewFormat(
|
||||
toNewFormat(
|
||||
{
|
||||
prop: 'someValue'
|
||||
},
|
||||
@@ -77,7 +77,7 @@ describe('objectUtils', function () {
|
||||
|
||||
it('translates composition', function () {
|
||||
expect(
|
||||
objectUtils.toNewFormat(
|
||||
toNewFormat(
|
||||
{
|
||||
prop: 'someValue',
|
||||
composition: ['anotherObjectId', 'scratch:anotherObjectId']
|
||||
@@ -107,7 +107,7 @@ describe('objectUtils', function () {
|
||||
describe('new object conversions', function () {
|
||||
it('removes ids', function () {
|
||||
expect(
|
||||
objectUtils.toOldFormat({
|
||||
toOldFormat({
|
||||
prop: 'someValue',
|
||||
identifier: {
|
||||
namespace: '',
|
||||
@@ -121,7 +121,7 @@ describe('objectUtils', function () {
|
||||
|
||||
it('translates composition', function () {
|
||||
expect(
|
||||
objectUtils.toOldFormat({
|
||||
toOldFormat({
|
||||
prop: 'someValue',
|
||||
composition: [
|
||||
{
|
||||
|
||||
@@ -61,6 +61,7 @@ class Overlay extends EventEmitter {
|
||||
dismiss() {
|
||||
this.emit('destroy');
|
||||
this.destroy();
|
||||
this.container.remove();
|
||||
}
|
||||
|
||||
//Ensures that any callers are notified that the overlay is dismissed
|
||||
|
||||
@@ -29,27 +29,29 @@
|
||||
class="c-click-icon c-overlay__close-button icon-x"
|
||||
@click.stop="destroy"
|
||||
></button>
|
||||
<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"
|
||||
<div class="c-overlay__content-wrapper">
|
||||
<div
|
||||
ref="element"
|
||||
class="c-overlay__contents js-notebook-snapshot-item-wrapper"
|
||||
tabindex="0"
|
||||
:class="{ 'c-button--major': focusIndex === index }"
|
||||
@focus="focusIndex = index"
|
||||
@click="buttonClickHandler(button.callback)"
|
||||
>
|
||||
{{ button.label }}
|
||||
</button>
|
||||
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"
|
||||
:class="{ 'c-button--major': focusIndex === index }"
|
||||
@focus="focusIndex = index"
|
||||
@click="buttonClickHandler(button.callback)"
|
||||
>
|
||||
{{ button.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
&__icon {
|
||||
// Holds a background SVG graphic
|
||||
$s: 80px;
|
||||
$s: 50px;
|
||||
flex: 0 0 auto;
|
||||
min-width: $s;
|
||||
min-height: $s;
|
||||
|
||||
@@ -19,7 +19,9 @@
|
||||
z-index: 70;
|
||||
|
||||
&__blocker {
|
||||
display: none; // Mobile-first
|
||||
// Mobile-first: use the blocker to create a full look to dialogs
|
||||
@include abs();
|
||||
background: $colorBodyBg;
|
||||
}
|
||||
|
||||
&__outer {
|
||||
@@ -27,7 +29,13 @@
|
||||
background: $colorBodyBg;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: $overlayInnerMargin;
|
||||
|
||||
body.mobile .l-overlay-fit & {
|
||||
// Vertically center small dialogs in mobile
|
||||
top: 50%;
|
||||
bottom: auto;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
&__close-button {
|
||||
@@ -39,12 +47,32 @@
|
||||
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 {
|
||||
@@ -78,6 +106,10 @@
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: $interiorMargin;
|
||||
body.mobile & {
|
||||
justify-content: flex-end;
|
||||
padding-right: $interiorMargin;
|
||||
}
|
||||
|
||||
> * + * {
|
||||
margin-left: $interiorMargin;
|
||||
|
||||
194
src/api/telemetry/BatchingWebSocket.js
Normal file
194
src/api/telemetry/BatchingWebSocket.js
Normal file
@@ -0,0 +1,194 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT Web, Copyright (c) 2014-2024, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT Web 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 Web 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 installWorker from './WebSocketWorker.js';
|
||||
const DEFAULT_RATE_MS = 1000;
|
||||
/**
|
||||
* Describes the strategy to be used when batching WebSocket messages
|
||||
*
|
||||
* @typedef BatchingStrategy
|
||||
* @property {Function} shouldBatchMessage a function that accepts a single
|
||||
* argument - the raw message received from the websocket. Every message
|
||||
* received will be evaluated against this function so it should be performant.
|
||||
* Note also that this function is executed in a worker, so it must be
|
||||
* completely self-contained with no external dependencies. The function
|
||||
* should return `true` if the message should be batched, and `false` if not.
|
||||
* @property {Function} getBatchIdFromMessage a function that accepts a
|
||||
* single argument - the raw message received from the websocket. Only messages
|
||||
* where `shouldBatchMessage` has evaluated to true will be passed into this
|
||||
* function. The function should return a unique value on which to batch the
|
||||
* messages. For example a telemetry, channel, or parameter identifier.
|
||||
*/
|
||||
/**
|
||||
* Provides a reliable and convenient WebSocket abstraction layer that handles
|
||||
* a lot of boilerplate common to managing WebSocket connections such as:
|
||||
* - Establishing a WebSocket connection to a server
|
||||
* - Reconnecting on error, with a fallback strategy
|
||||
* - Queuing messages so that clients can send messages without concern for the current
|
||||
* connection state of the WebSocket.
|
||||
*
|
||||
* The WebSocket that it manages is based in a dedicated worker so that network
|
||||
* concerns are not handled on the main event loop. This allows for performant receipt
|
||||
* and batching of messages without blocking either the UI or server.
|
||||
*
|
||||
* @memberof module:openmct.telemetry
|
||||
*/
|
||||
class BatchingWebSocket extends EventTarget {
|
||||
#worker;
|
||||
#openmct;
|
||||
#showingRateLimitNotification;
|
||||
#rate;
|
||||
|
||||
constructor(openmct) {
|
||||
super();
|
||||
// Install worker, register listeners etc.
|
||||
const workerFunction = `(${installWorker.toString()})()`;
|
||||
const workerBlob = new Blob([workerFunction]);
|
||||
const workerUrl = URL.createObjectURL(workerBlob, { type: 'application/javascript' });
|
||||
this.#worker = new Worker(workerUrl);
|
||||
this.#openmct = openmct;
|
||||
this.#showingRateLimitNotification = false;
|
||||
this.#rate = DEFAULT_RATE_MS;
|
||||
|
||||
const routeMessageToHandler = this.#routeMessageToHandler.bind(this);
|
||||
this.#worker.addEventListener('message', routeMessageToHandler);
|
||||
openmct.on(
|
||||
'destroy',
|
||||
() => {
|
||||
this.disconnect();
|
||||
URL.revokeObjectURL(workerUrl);
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Will establish a WebSocket connection to the provided url
|
||||
* @param {string} url The URL to connect to
|
||||
*/
|
||||
connect(url) {
|
||||
this.#worker.postMessage({
|
||||
type: 'connect',
|
||||
url
|
||||
});
|
||||
|
||||
this.#readyForNextBatch();
|
||||
}
|
||||
|
||||
#readyForNextBatch() {
|
||||
this.#worker.postMessage({
|
||||
type: 'readyForNextBatch'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to the WebSocket.
|
||||
* @param {any} message The message to send. Can be any type supported by WebSockets.
|
||||
* See https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/send#data
|
||||
*/
|
||||
sendMessage(message) {
|
||||
this.#worker.postMessage({
|
||||
type: 'message',
|
||||
message
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the strategy used to both decide which raw messages to batch, and how to group
|
||||
* them.
|
||||
* @param {BatchingStrategy} strategy The batching strategy to use when evaluating
|
||||
* raw messages from the WebSocket.
|
||||
*/
|
||||
setBatchingStrategy(strategy) {
|
||||
const serializedStrategy = {
|
||||
shouldBatchMessage: strategy.shouldBatchMessage.toString(),
|
||||
getBatchIdFromMessage: strategy.getBatchIdFromMessage.toString()
|
||||
};
|
||||
|
||||
this.#worker.postMessage({
|
||||
type: 'setBatchingStrategy',
|
||||
serializedStrategy
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* Note that this is a fail-safe that is only invoked if performance drops to the
|
||||
* point where Open MCT cannot keep up with the amount of telemetry it is receiving.
|
||||
* In this event it will sacrifice the oldest telemetry in the batch in favor of the
|
||||
* most recent telemetry. The user will be informed that telemetry has been dropped.
|
||||
*
|
||||
* This should be set appropriately for the expected data rate. eg. If telemetry
|
||||
* is received at 10Hz for each telemetry point, then a minimal combination of batch
|
||||
* size and rate is 10 and 1000 respectively. Ideally you would add some margin, so
|
||||
* 15 would probably be a better batch size.
|
||||
*/
|
||||
setMaxBatchSize(maxBatchSize) {
|
||||
this.#worker.postMessage({
|
||||
type: 'setMaxBatchSize',
|
||||
maxBatchSize
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect the associated WebSocket. Generally speaking there is no need to call
|
||||
* this manually.
|
||||
*/
|
||||
disconnect() {
|
||||
this.#worker.postMessage({
|
||||
type: 'disconnect'
|
||||
});
|
||||
}
|
||||
|
||||
#routeMessageToHandler(message) {
|
||||
if (message.data.type === 'batch') {
|
||||
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.' }
|
||||
);
|
||||
this.#showingRateLimitNotification = true;
|
||||
notification.once('minimized', () => {
|
||||
this.#showingRateLimitNotification = false;
|
||||
});
|
||||
}
|
||||
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 {
|
||||
throw new Error(`Unknown message type: ${message.data.type}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default BatchingWebSocket;
|
||||
@@ -20,9 +20,10 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import objectUtils from 'objectUtils';
|
||||
import { makeKeyString } from 'objectUtils';
|
||||
|
||||
import CustomStringFormatter from '../../plugins/displayLayout/CustomStringFormatter.js';
|
||||
import BatchingWebSocket from './BatchingWebSocket.js';
|
||||
import DefaultMetadataProvider from './DefaultMetadataProvider.js';
|
||||
import TelemetryCollection from './TelemetryCollection.js';
|
||||
import TelemetryMetadataManager from './TelemetryMetadataManager.js';
|
||||
@@ -54,6 +55,28 @@ import TelemetryValueFormatter from './TelemetryValueFormatter.js';
|
||||
* @memberof module:openmct.TelemetryAPI~
|
||||
*/
|
||||
|
||||
/**
|
||||
* Describes and bounds requests for telemetry data.
|
||||
*
|
||||
* @typedef TelemetrySubscriptionOptions
|
||||
* @property {String} [strategy] symbolic identifier directing providers on how
|
||||
* to handle telemetry subscriptions. The default behavior is 'latest' which will
|
||||
* always return a single telemetry value with each callback, and in the event
|
||||
* of throttling will always prioritize the latest data, meaning intermediate
|
||||
* data will be skipped. Alternatively, the `batch` strategy can be used, which
|
||||
* will return all telemetry values since the last callback. This strategy is
|
||||
* useful for cases where intermediate data is important, such as when
|
||||
* rendering a telemetry plot or table. If `batch` is specified, the subscription
|
||||
* callback will be invoked with an Array.
|
||||
*
|
||||
* @memberof module:openmct.TelemetryAPI~
|
||||
*/
|
||||
|
||||
const SUBSCRIBE_STRATEGY = {
|
||||
LATEST: 'latest',
|
||||
BATCH: 'batch'
|
||||
};
|
||||
|
||||
/**
|
||||
* Utilities for telemetry
|
||||
* @interface TelemetryAPI
|
||||
@@ -61,6 +84,11 @@ import TelemetryValueFormatter from './TelemetryValueFormatter.js';
|
||||
*/
|
||||
export default class TelemetryAPI {
|
||||
#isGreedyLAD;
|
||||
#subscribeCache;
|
||||
|
||||
get SUBSCRIBE_STRATEGY() {
|
||||
return SUBSCRIBE_STRATEGY;
|
||||
}
|
||||
|
||||
constructor(openmct) {
|
||||
this.openmct = openmct;
|
||||
@@ -78,6 +106,8 @@ export default class TelemetryAPI {
|
||||
this.valueFormatterCache = new WeakMap();
|
||||
this.requestInterceptorRegistry = new TelemetryRequestInterceptorRegistry();
|
||||
this.#isGreedyLAD = true;
|
||||
this.BatchingWebSocket = BatchingWebSocket;
|
||||
this.#subscribeCache = {};
|
||||
}
|
||||
|
||||
abortAllRequests() {
|
||||
@@ -378,54 +408,111 @@ export default class TelemetryAPI {
|
||||
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
|
||||
* @param {module:openmct.DomainObject} domainObject the object
|
||||
* which has associated telemetry
|
||||
* @param {TelemetryRequestOptions} options configuration items for subscription
|
||||
* @param {TelemetrySubscriptionOptions} options configuration items for subscription
|
||||
* @param {Function} callback the callback to invoke with new data, as
|
||||
* it becomes available
|
||||
* @returns {Function} a function which may be called to terminate
|
||||
* the subscription
|
||||
*/
|
||||
subscribe(domainObject, callback, options) {
|
||||
subscribe(domainObject, callback, options = { strategy: SUBSCRIBE_STRATEGY.LATEST }) {
|
||||
const requestedStrategy = options.strategy || SUBSCRIBE_STRATEGY.LATEST;
|
||||
|
||||
if (domainObject.type === 'unknown') {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
const provider = this.findSubscriptionProvider(domainObject);
|
||||
const provider = this.findSubscriptionProvider(domainObject, options);
|
||||
const supportsBatching =
|
||||
Boolean(provider?.supportsBatching) && provider?.supportsBatching(domainObject, options);
|
||||
|
||||
if (!this.subscribeCache) {
|
||||
this.subscribeCache = {};
|
||||
if (!this.#subscribeCache) {
|
||||
this.#subscribeCache = {};
|
||||
}
|
||||
|
||||
const keyString = objectUtils.makeKeyString(domainObject.identifier);
|
||||
let subscriber = this.subscribeCache[keyString];
|
||||
const keyString = makeKeyString(domainObject.identifier);
|
||||
const supportedStrategy = supportsBatching ? requestedStrategy : SUBSCRIBE_STRATEGY.LATEST;
|
||||
// Override the requested strategy with the strategy supported by the provider
|
||||
const optionsWithSupportedStrategy = {
|
||||
...options,
|
||||
strategy: supportedStrategy
|
||||
};
|
||||
// If batching is supported, we need to cache a subscription for each strategy -
|
||||
// latest and batched.
|
||||
const cacheKey = `${keyString}:${supportedStrategy}`;
|
||||
let subscriber = this.#subscribeCache[cacheKey];
|
||||
|
||||
if (!subscriber) {
|
||||
subscriber = this.subscribeCache[keyString] = {
|
||||
callbacks: [callback]
|
||||
subscriber = this.#subscribeCache[cacheKey] = {
|
||||
latestCallbacks: [],
|
||||
batchCallbacks: []
|
||||
};
|
||||
if (provider) {
|
||||
subscriber.unsubscribe = provider.subscribe(
|
||||
domainObject,
|
||||
function (value) {
|
||||
subscriber.callbacks.forEach(function (cb) {
|
||||
cb(value);
|
||||
});
|
||||
},
|
||||
options
|
||||
invokeCallbackWithRequestedStrategy,
|
||||
optionsWithSupportedStrategy
|
||||
);
|
||||
} else {
|
||||
subscriber.unsubscribe = function () {};
|
||||
}
|
||||
}
|
||||
|
||||
if (requestedStrategy === SUBSCRIBE_STRATEGY.BATCH) {
|
||||
subscriber.batchCallbacks.push(callback);
|
||||
} else {
|
||||
subscriber.callbacks.push(callback);
|
||||
subscriber.latestCallbacks.push(callback);
|
||||
}
|
||||
|
||||
// Guarantees that view receive telemetry in the expected form
|
||||
function invokeCallbackWithRequestedStrategy(data) {
|
||||
invokeCallbacksWithArray(data, subscriber.batchCallbacks);
|
||||
invokeCallbacksWithSingleValue(data, subscriber.latestCallbacks);
|
||||
}
|
||||
|
||||
function invokeCallbacksWithArray(data, batchCallbacks) {
|
||||
//
|
||||
if (data === undefined || data === null || data.length === 0) {
|
||||
throw new Error(
|
||||
'Attempt to invoke telemetry subscription callback with no telemetry datum'
|
||||
);
|
||||
}
|
||||
|
||||
if (!Array.isArray(data)) {
|
||||
data = [data];
|
||||
}
|
||||
|
||||
batchCallbacks.forEach((cb) => {
|
||||
cb(data);
|
||||
});
|
||||
}
|
||||
|
||||
function invokeCallbacksWithSingleValue(data, latestCallbacks) {
|
||||
if (Array.isArray(data)) {
|
||||
data = data[data.length - 1];
|
||||
}
|
||||
|
||||
if (data === undefined || data === null) {
|
||||
throw new Error(
|
||||
'Attempt to invoke telemetry subscription callback with no telemetry datum'
|
||||
);
|
||||
}
|
||||
|
||||
latestCallbacks.forEach((cb) => {
|
||||
cb(data);
|
||||
});
|
||||
}
|
||||
|
||||
return function unsubscribe() {
|
||||
subscriber.callbacks = subscriber.callbacks.filter(function (cb) {
|
||||
subscriber.latestCallbacks = subscriber.latestCallbacks.filter(function (cb) {
|
||||
return cb !== callback;
|
||||
});
|
||||
if (subscriber.callbacks.length === 0) {
|
||||
subscriber.batchCallbacks = subscriber.batchCallbacks.filter(function (cb) {
|
||||
return cb !== callback;
|
||||
});
|
||||
|
||||
if (subscriber.latestCallbacks.length === 0 && subscriber.batchCallbacks.length === 0) {
|
||||
subscriber.unsubscribe();
|
||||
delete this.subscribeCache[keyString];
|
||||
delete this.#subscribeCache[cacheKey];
|
||||
}
|
||||
}.bind(this);
|
||||
}
|
||||
@@ -454,7 +541,7 @@ export default class TelemetryAPI {
|
||||
this.stalenessSubscriberCache = {};
|
||||
}
|
||||
|
||||
const keyString = objectUtils.makeKeyString(domainObject.identifier);
|
||||
const keyString = makeKeyString(domainObject.identifier);
|
||||
let stalenessSubscriber = this.stalenessSubscriberCache[keyString];
|
||||
|
||||
if (!stalenessSubscriber) {
|
||||
@@ -513,7 +600,7 @@ export default class TelemetryAPI {
|
||||
this.limitsSubscribeCache = {};
|
||||
}
|
||||
|
||||
const keyString = objectUtils.makeKeyString(domainObject.identifier);
|
||||
const keyString = makeKeyString(domainObject.identifier);
|
||||
let subscriber = this.limitsSubscribeCache[keyString];
|
||||
|
||||
if (!subscriber) {
|
||||
|
||||
@@ -90,7 +90,9 @@ describe('Telemetry API', () => {
|
||||
|
||||
const callback = jasmine.createSpy('callback');
|
||||
const unsubscribe = telemetryAPI.subscribe(domainObject, callback);
|
||||
expect(telemetryProvider.supportsSubscribe).toHaveBeenCalledWith(domainObject);
|
||||
expect(telemetryProvider.supportsSubscribe).toHaveBeenCalledWith(domainObject, {
|
||||
strategy: 'latest'
|
||||
});
|
||||
expect(telemetryProvider.subscribe).not.toHaveBeenCalled();
|
||||
expect(unsubscribe).toEqual(jasmine.any(Function));
|
||||
|
||||
@@ -111,12 +113,16 @@ describe('Telemetry API', () => {
|
||||
const callback = jasmine.createSpy('callback');
|
||||
const unsubscribe = telemetryAPI.subscribe(domainObject, callback);
|
||||
expect(telemetryProvider.supportsSubscribe.calls.count()).toBe(1);
|
||||
expect(telemetryProvider.supportsSubscribe).toHaveBeenCalledWith(domainObject);
|
||||
expect(telemetryProvider.supportsSubscribe).toHaveBeenCalledWith(domainObject, {
|
||||
strategy: 'latest'
|
||||
});
|
||||
expect(telemetryProvider.subscribe.calls.count()).toBe(1);
|
||||
expect(telemetryProvider.subscribe).toHaveBeenCalledWith(
|
||||
domainObject,
|
||||
jasmine.any(Function),
|
||||
undefined
|
||||
{
|
||||
strategy: 'latest'
|
||||
}
|
||||
);
|
||||
|
||||
const notify = telemetryProvider.subscribe.calls.mostRecent().args[1];
|
||||
@@ -321,6 +327,126 @@ describe('Telemetry API', () => {
|
||||
signal
|
||||
});
|
||||
});
|
||||
describe('telemetry batching support', () => {
|
||||
let callbacks;
|
||||
let unsubFunc;
|
||||
|
||||
beforeEach(() => {
|
||||
callbacks = [];
|
||||
unsubFunc = jasmine.createSpy('unsubscribe');
|
||||
telemetryProvider.supportsBatching = jasmine.createSpy('supportsBatching');
|
||||
telemetryProvider.supportsBatching.and.returnValue(true);
|
||||
telemetryProvider.supportsSubscribe.and.returnValue(true);
|
||||
|
||||
telemetryProvider.subscribe.and.callFake(function (obj, cb, options) {
|
||||
callbacks.push(cb);
|
||||
|
||||
return unsubFunc;
|
||||
});
|
||||
|
||||
telemetryAPI.addProvider(telemetryProvider);
|
||||
});
|
||||
|
||||
it('caches subscriptions for batched and latest telemetry subscriptions', () => {
|
||||
const latestCallback1 = jasmine.createSpy('latestCallback1');
|
||||
const unsubscribeFromLatest1 = telemetryAPI.subscribe(domainObject, latestCallback1, {
|
||||
strategy: 'latest'
|
||||
});
|
||||
const latestCallback2 = jasmine.createSpy('latestCallback2');
|
||||
const unsubscribeFromLatest2 = telemetryAPI.subscribe(domainObject, latestCallback2, {
|
||||
strategy: 'latest'
|
||||
});
|
||||
|
||||
//Expect a single cached subscription for latest telemetry
|
||||
expect(telemetryProvider.subscribe.calls.count()).toBe(1);
|
||||
|
||||
const batchedCallback1 = jasmine.createSpy('batchedCallback1');
|
||||
const unsubscribeFromBatched1 = telemetryAPI.subscribe(domainObject, batchedCallback1, {
|
||||
strategy: 'batch'
|
||||
});
|
||||
|
||||
const batchedCallback2 = jasmine.createSpy('batchedCallback2');
|
||||
const unsubscribeFromBatched2 = telemetryAPI.subscribe(domainObject, batchedCallback2, {
|
||||
strategy: 'batch'
|
||||
});
|
||||
|
||||
//Expect a single cached subscription for each strategy telemetry
|
||||
expect(telemetryProvider.subscribe.calls.count()).toBe(2);
|
||||
|
||||
unsubscribeFromLatest1();
|
||||
unsubscribeFromLatest2();
|
||||
unsubscribeFromBatched1();
|
||||
unsubscribeFromBatched2();
|
||||
|
||||
expect(unsubFunc).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
it('subscriptions with the latest strategy are always invoked with a single value', () => {
|
||||
const latestCallback = jasmine.createSpy('latestCallback1');
|
||||
telemetryAPI.subscribe(domainObject, latestCallback, {
|
||||
strategy: 'latest'
|
||||
});
|
||||
|
||||
const batchedValues = [1, 2, 3];
|
||||
callbacks.forEach((cb) => {
|
||||
cb(batchedValues);
|
||||
});
|
||||
|
||||
expect(latestCallback).toHaveBeenCalledWith(3);
|
||||
|
||||
const singleValue = 1;
|
||||
callbacks.forEach((cb) => {
|
||||
cb(singleValue);
|
||||
});
|
||||
|
||||
expect(latestCallback).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it('subscriptions with the batch strategy are always invoked with an array', () => {
|
||||
const batchedCallback = jasmine.createSpy('batchedCallback1');
|
||||
const latestCallback = jasmine.createSpy('latestCallback1');
|
||||
telemetryAPI.subscribe(domainObject, batchedCallback, {
|
||||
strategy: 'batch'
|
||||
});
|
||||
telemetryAPI.subscribe(domainObject, latestCallback, {
|
||||
strategy: 'latest'
|
||||
});
|
||||
|
||||
const batchedValues = [1, 2, 3];
|
||||
callbacks.forEach((cb) => {
|
||||
cb(batchedValues);
|
||||
});
|
||||
|
||||
// Callbacks for the 'batch' strategy are always called with an array of values
|
||||
expect(batchedCallback).toHaveBeenCalledWith(batchedValues);
|
||||
// Callbacks for the 'latest' strategy are always called with a single value
|
||||
expect(latestCallback).toHaveBeenCalledWith(3);
|
||||
|
||||
callbacks.forEach((cb) => {
|
||||
cb(1);
|
||||
});
|
||||
// Callbacks for the 'batch' strategy are always called with an array of values, even if there is only one value
|
||||
expect(batchedCallback).toHaveBeenCalledWith([1]);
|
||||
// Callbacks for the 'latest' strategy are always called with a single value
|
||||
expect(latestCallback).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it('legacy providers are left unchanged, with a single subscription', () => {
|
||||
delete telemetryProvider.supportsBatching;
|
||||
|
||||
const batchCallback = jasmine.createSpy('batchCallback');
|
||||
telemetryAPI.subscribe(domainObject, batchCallback, {
|
||||
strategy: 'batch'
|
||||
});
|
||||
expect(telemetryProvider.subscribe.calls.mostRecent().args[2].strategy).toBe('latest');
|
||||
|
||||
const latestCallback = jasmine.createSpy('latestCallback');
|
||||
telemetryAPI.subscribe(domainObject, latestCallback, {
|
||||
strategy: 'latest'
|
||||
});
|
||||
|
||||
expect(telemetryProvider.subscribe.calls.mostRecent().args[2].strategy).toBe('latest');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('metadata', () => {
|
||||
|
||||
@@ -180,11 +180,14 @@ export default class TelemetryCollection extends EventEmitter {
|
||||
if (this.unsubscribe) {
|
||||
this.unsubscribe();
|
||||
}
|
||||
const options = { ...this.options };
|
||||
//We always want to receive all available values in telemetry tables.
|
||||
options.strategy = this.openmct.telemetry.SUBSCRIBE_STRATEGY.BATCH;
|
||||
|
||||
this.unsubscribe = this.openmct.telemetry.subscribe(
|
||||
this.domainObject,
|
||||
(datum) => this._processNewTelemetry(datum),
|
||||
this.options
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
@@ -209,6 +212,8 @@ export default class TelemetryCollection extends EventEmitter {
|
||||
let added = [];
|
||||
let addedIndices = [];
|
||||
let hasDataBeforeStartBound = false;
|
||||
let size = this.options.size;
|
||||
let enforceSize = size !== undefined && this.options.enforceSize;
|
||||
|
||||
// loop through, sort and dedupe
|
||||
for (let datum of data) {
|
||||
@@ -271,6 +276,13 @@ export default class TelemetryCollection extends EventEmitter {
|
||||
}
|
||||
} else {
|
||||
this.emit('add', added, addedIndices);
|
||||
|
||||
if (enforceSize && this.boundedTelemetry.length > size) {
|
||||
const removeCount = this.boundedTelemetry.length - size;
|
||||
const removed = this.boundedTelemetry.splice(0, removeCount);
|
||||
|
||||
this.emit('remove', removed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user