Compare commits
11 Commits
docs-relea
...
activity-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e880a5020 | ||
|
|
50de0f6f27 | ||
|
|
a0ccc4e300 | ||
|
|
c6a6a18eba | ||
|
|
2e41753f41 | ||
|
|
3c14025501 | ||
|
|
e47bfedaf7 | ||
|
|
02edb9924b | ||
|
|
4d4f83ee95 | ||
|
|
92a30a3485 | ||
|
|
7003f00707 |
@@ -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,11 +105,7 @@ jobs:
|
||||
node-version: <<parameters.node-version>>
|
||||
- browser-tools/install-chrome:
|
||||
replace-existing: false
|
||||
- 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 test
|
||||
- run: npm run cov:unit:publish
|
||||
- save_cache_cmd:
|
||||
node-version: <<parameters.node-version>>
|
||||
@@ -127,20 +123,16 @@ jobs:
|
||||
suite: #stable or full
|
||||
type: string
|
||||
executor: pw-focal-development
|
||||
parallelism: 7
|
||||
parallelism: 6
|
||||
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:
|
||||
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
|
||||
- run: SHARD="$((${CIRCLE_NODE_INDEX}+1))"; npm run test:e2e:<<parameters.suite>> -- --shard=${SHARD}/${CIRCLE_NODE_TOTAL}
|
||||
- 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
|
||||
@@ -160,31 +152,6 @@ jobs:
|
||||
equal: [42, 42] # Always generate version artifacts regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2
|
||||
steps:
|
||||
- generate_and_store_version_and_filesystem_artifacts
|
||||
e2e-mobile:
|
||||
executor: pw-focal-development
|
||||
steps:
|
||||
- build_and_install:
|
||||
node-version: lts/hydrogen
|
||||
- run: npm run test:e2e:mobile
|
||||
- when:
|
||||
condition:
|
||||
equal: [42, 42] # Always run codecov reports regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2
|
||||
steps:
|
||||
- generate_e2e_code_cov_report:
|
||||
suite: full
|
||||
- store_test_results:
|
||||
path: test-results/results.xml
|
||||
- store_artifacts:
|
||||
path: test-results
|
||||
- store_artifacts:
|
||||
path: coverage
|
||||
- store_artifacts:
|
||||
path: html-test-results
|
||||
- when:
|
||||
condition:
|
||||
equal: [42, 42] # Always generate version artifacts regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2
|
||||
steps:
|
||||
- generate_and_store_version_and_filesystem_artifacts
|
||||
e2e-couchdb:
|
||||
executor: ubuntu
|
||||
steps:
|
||||
@@ -272,7 +239,6 @@ jobs:
|
||||
equal: [42, 42] # Always generate version artifacts regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2
|
||||
steps:
|
||||
- generate_and_store_version_and_filesystem_artifacts
|
||||
|
||||
workflows:
|
||||
overall-circleci-commit-status: #These jobs run on every commit
|
||||
jobs:
|
||||
@@ -285,9 +251,10 @@ workflows:
|
||||
- e2e-test:
|
||||
name: e2e-stable
|
||||
suite: stable
|
||||
- e2e-mobile
|
||||
- mem-test
|
||||
- perf-test
|
||||
- visual-a11y-tests:
|
||||
name: visual-a11y-test-ci
|
||||
name: visual-test-ci
|
||||
suite: ci
|
||||
|
||||
the-nightly: #These jobs do not run on PRs, but against master at night
|
||||
@@ -303,16 +270,15 @@ workflows:
|
||||
- e2e-test:
|
||||
name: e2e-full-nightly
|
||||
suite: full
|
||||
- e2e-mobile
|
||||
- perf-test
|
||||
- mem-test
|
||||
- perf-test
|
||||
- visual-a11y-tests:
|
||||
name: visual-a11y-test-nightly
|
||||
name: visual-test-nightly
|
||||
suite: full
|
||||
- e2e-couchdb
|
||||
triggers:
|
||||
- schedule:
|
||||
cron: "0 0 * * *"
|
||||
cron: '0 0 * * *'
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
|
||||
@@ -492,14 +492,9 @@
|
||||
"gcov",
|
||||
"WCAG",
|
||||
"stackedplot",
|
||||
"Andale",
|
||||
"unnormalized",
|
||||
"checksnapshots",
|
||||
"specced",
|
||||
"composables",
|
||||
"countup"
|
||||
"Andale"
|
||||
],
|
||||
"dictionaries": ["npm", "softwareTerms", "node", "html", "css", "bash", "en_US", "en-gb", "misc"],
|
||||
"dictionaries": ["npm", "softwareTerms", "node", "html", "css", "bash", "en_US"],
|
||||
"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 a notable change that will require a special callout in the release notes [Notable Change](../docs/src/process/release.md) ? For example, will this break compatibility with existing APIs or projects which source these plugins?
|
||||
* [ ] Is this change backwards compatible? For example, developers won't need to change how they are calling the API or how they've extended core plugins such as Tables or Plots.
|
||||
|
||||
### Author Checklist
|
||||
|
||||
|
||||
5
.github/release.yml
vendored
5
.github/release.yml
vendored
@@ -1,8 +1,5 @@
|
||||
changelog:
|
||||
categories:
|
||||
- title: 💥 Notable Changes
|
||||
labels:
|
||||
- notable_change
|
||||
- title: 🏕 Features
|
||||
labels:
|
||||
- type:feature
|
||||
@@ -23,4 +20,4 @@ changelog:
|
||||
- dependencies
|
||||
- title: 🐛 Bug Fixes
|
||||
labels:
|
||||
- "*"
|
||||
- '*'
|
||||
|
||||
61
.github/workflows/e2e-flakefinder.yml
vendored
61
.github/workflows/e2e-flakefinder.yml
vendored
@@ -1,61 +0,0 @@
|
||||
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
58
.github/workflows/e2e-perf.yml
vendored
@@ -1,58 +0,0 @@
|
||||
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}`);
|
||||
}
|
||||
14
.vscode/extensions.json
vendored
14
.vscode/extensions.json
vendored
@@ -1,14 +0,0 @@
|
||||
{
|
||||
// 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",
|
||||
"Vue.vscode-typescript-vue-plugin",
|
||||
"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"]
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
|
||||
# Release of NASA Open MCT NPM Package
|
||||
|
||||
This document outlines the process and key considerations for releasing a new version of the NASA Open MCT project as an NPM (Node Package Manager) package.
|
||||
|
||||
## FAQ
|
||||
|
||||
1. When do we publish a new version of Open MCT?
|
||||
- At the end of a working sprint (typically) after all blocking issues have been resolved.
|
||||
2. Where do we publish?
|
||||
- [NPM](https://www.npmjs.com/package/openmct)
|
||||
- [Github Releases](https://github.com/nasa/openmct/releases)
|
||||
2. What do we publish?
|
||||
- What constitutes a "stable" release?
|
||||
- TODO
|
||||
- What constitutes a "latest" release?
|
||||
- The most recently published release.
|
||||
- What constitutes a "nightly" release?
|
||||
- TODO
|
||||
4. What necessitates a patch release?
|
||||
-
|
||||
|
||||
## 1. Pre-requisites
|
||||
|
||||
Before releasing a new version of Open MCT, ensure that all dependencies are updated, and
|
||||
comprehensive testing is performed.
|
||||
|
||||
## 2. Versioning
|
||||
|
||||
Open MCT follows [Semantic Versioning 2.0.0 (SemVer)](https://semver.org) that consists of three
|
||||
major components: `MAJOR.MINOR.PATCH` (i.e. `1.2.3`).
|
||||
|
||||
Major releases are necessitated by fundamental framework changes that are expected to be incompatible
|
||||
with previous releases.
|
||||
|
||||
Minor releases are necessitated by non-backwards-compatible application, API changes, or new
|
||||
features or enhancements.
|
||||
|
||||
Patch releases are created for backporting fixes to blocking bugs that were discovered _after_
|
||||
the release of a major or minor version. They are not to introduce new features, enhancements, or
|
||||
dependency changes.
|
||||
|
||||
## 3. Changelog Maintenance
|
||||
|
||||
Changelogs can be found in the GitHub releases section of the repository and are auto-generated
|
||||
using [GitHub's feature](https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes).
|
||||
|
||||
## 4. Pull Request Labeling
|
||||
|
||||
Generation of release notes is automated by the use of labels on pull requests. The following
|
||||
labels are used to categorize pull requests:
|
||||
|
||||
### `type:bug`
|
||||
|
||||
Pull requests are to be labeled with `type:bug` if they contain changes that intend to fix a bug.
|
||||
|
||||
### `type:enhancement`
|
||||
|
||||
Pull requests are to be labeled with `type:enhancement` if they contain changes that intend to
|
||||
enhance existing functionality of Open MCT.
|
||||
|
||||
### `type:feature`
|
||||
|
||||
Pull requests are to be labeled with `type:feature` if they contain changes that intend to introduce
|
||||
new functionality to Open MCT.
|
||||
|
||||
### `type:maintenance`
|
||||
|
||||
Pull requests are to be labeled with `type:maintenance` if they contain changes that introduce
|
||||
new tests, documentation, or other maintenance-related changes.
|
||||
|
||||
### `performance`
|
||||
|
||||
Pull requests are to be labeled with `performance` if they contain changes that are intended to
|
||||
improve the performance of Open MCT.
|
||||
|
||||
### `notable_change`
|
||||
|
||||
Pull requests are to be labeled with `notable_change` if they contain changes that fit any of the
|
||||
following criteria:
|
||||
|
||||
- **Breaking Change**
|
||||
- Highlights the integration of changes that are suspected to break, or without a doubt will
|
||||
break, backwards 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 any change to the Open MCT API such as the addition of new methods, or the
|
||||
modification or deprecation of existing methods. API changes may or may not constitute a
|
||||
breaking change.
|
||||
- **Default Behavior Change**
|
||||
- Any change to the default behavior of Open MCT, such as the default configuration of a plugin,
|
||||
or the default behavior of a user interface component or feature (i.e.: autoscale being enabled
|
||||
by default on plots).
|
||||
|
||||
## 5. Community & Contributions
|
||||
|
||||
Open MCT is an open-source project and contributions are welcome. As such, it is important to
|
||||
acknowledge the contributions of the community and contributors. Pull requests by contributors
|
||||
will be labeled with `source:community` to signify that the contribution was made by a member of
|
||||
the community.
|
||||
|
||||
## 6. Release Process
|
||||
|
||||
Currently, the release process is manual and requires the following steps:
|
||||
|
||||
1. Clone a fresh copy of the repository.
|
||||
- `git clone git@github.com:nasa/openmct.git`
|
||||
2. Check out the appropriate release branch.
|
||||
- `git checkout release/1.2.3`
|
||||
3. Ensure that the `package.json` file is updated with the correct version number and does not
|
||||
contain the `-next` suffix (which implies a pre-release).
|
||||
4. Create a tag for the release if it does not already exist.
|
||||
- `git tag v1.2.3`
|
||||
5. Push the tag to the repository.
|
||||
- `git push origin v1.2.3`
|
||||
6. Run `npm install` to install dependencies.
|
||||
7. Publish the release to NPM (You will need to be logged in to an NPM account with the appropriate permissions).
|
||||
- `npm publish`
|
||||
8. Create a release on GitHub.
|
||||
- Navigate to the Releases page on the Open MCT repository.
|
||||
- Click [draft a new release.](https://github.com/nasa/openmct/releases/new)
|
||||
- Choose the tag that was just created for the release.
|
||||
- For "Previous tag", choose the tag that was most recently released.
|
||||
- Click "Generate release notes" to auto-generate release notes.
|
||||
- Click "Publish release" to publish the release.
|
||||
@@ -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
|
||||
npm run test:e2e:checksnapshots
|
||||
npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @snapshot
|
||||
```
|
||||
|
||||
### Updating Snapshots
|
||||
@@ -134,12 +134,6 @@ 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:
|
||||
@@ -229,7 +223,7 @@ Current list of test tags:
|
||||
|
||||
|Test Tag|Description|
|
||||
|:-:|-|
|
||||
|`@mobile` | Test case or test suite is compatible with Playwright's iPad support and Open MCT's read-only mobile view (i.e. no create button).|
|
||||
|`@ipad` | Test case or test suite is compatible with Playwright's iPad support and Open MCT's read-only mobile view (i.e. no create button).|
|
||||
|`@a11y` | Test case or test suite to execute playwright-axe accessibility checks and generate a11y reports.|
|
||||
|`@gds` | Denotes a GDS Test Case used in the VIPER Mission.|
|
||||
|`@addInit` | Initializes the browser with an injected and artificial state. Useful for loading non-default plugins. Likely will not work outside of `npm start`.|
|
||||
@@ -329,15 +323,9 @@ In terms of operating system testing, we're only limited by what the CI provider
|
||||
|
||||
#### **Mobile**
|
||||
|
||||
We have a Mission-need to support iPad and mobile devices. To run our test suites with mobile devices, please see our `playwright-mobile.config.js` projects.
|
||||
We have the Mission-need to support iPad. To run our iPad suite, please see our `playwright-*.config.js` with the 'iPad' project.
|
||||
|
||||
In general, our test suite is not designed to run against mobile devices as the mobile experience is a focused version of the application. Core functionality is missing (chiefly the 'Create' button). To bypass the object creation, we leverage the `storageState` properties for starting the mobile tests with localstorage.
|
||||
|
||||
For now, the mobile tests will exist in the /tests/mobile/ suites and be executed with the
|
||||
```sh
|
||||
npm run test:e2e:mobile
|
||||
```
|
||||
command.
|
||||
In general, our test suite is not designed to run against mobile devices as the mobile experience is a focused version of the application. Core functionality is missing (chiefly the 'Create' button) and so this will likely turn into a separate suite.
|
||||
|
||||
#### **Skipping or executing tests based on browser, os, and/os browser version:**
|
||||
|
||||
|
||||
@@ -284,7 +284,7 @@ async function navigateToObjectWithFixedTimeBounds(page, url, start, end) {
|
||||
*/
|
||||
async function openObjectTreeContextMenu(page, url) {
|
||||
await page.goto(url);
|
||||
await page.getByLabel('Show selected item in tree').click();
|
||||
await page.click('button[title="Show selected item in tree"]');
|
||||
await page.locator('.is-navigated-object').click({
|
||||
button: 'right'
|
||||
});
|
||||
|
||||
@@ -61,6 +61,7 @@ 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
|
||||
|
||||
@@ -49,7 +49,7 @@ async function dragAndDropEmbed(page, notebookObject) {
|
||||
// Navigate to notebook
|
||||
await page.goto(notebookObject.url);
|
||||
// Expand the tree to reveal the notebook
|
||||
await page.getByLabel('Show selected item in tree').click();
|
||||
await page.click('button[title="Show selected item in tree"]');
|
||||
// Drag and drop the SWG into the notebook
|
||||
await page.dragAndDrop(`text=${swg.name}`, NOTEBOOK_DROP_AREA);
|
||||
await commitEntry(page);
|
||||
|
||||
@@ -10,7 +10,6 @@ const NUM_WORKERS = 2;
|
||||
const config = {
|
||||
retries: 2, //Retries 2 times for a total of 3 runs. When running sharded and with max-failures=5, this should ensure that flake is managed without failing the full suite
|
||||
testDir: 'tests',
|
||||
grepInvert: /@mobile/, //Ignore mobile tests
|
||||
testIgnore: '**/*.perf.spec.js', //Ignore performance tests and define in playwright-perfromance.config.js
|
||||
timeout: 60 * 1000,
|
||||
webServer: {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
// playwright.config.js
|
||||
// @ts-check
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { devices } from '@playwright/test';
|
||||
|
||||
/** @type {import('@playwright/test').PlaywrightTestConfig} */
|
||||
const config = {
|
||||
retries: 0,
|
||||
testDir: 'tests',
|
||||
testMatch: '**/*.e2e.spec.js', // only run e2e tests
|
||||
testIgnore: '**/*.perf.spec.js',
|
||||
timeout: 30 * 1000,
|
||||
webServer: {
|
||||
@@ -33,6 +35,7 @@ const config = {
|
||||
},
|
||||
{
|
||||
name: 'MMOC',
|
||||
testMatch: '**/*.e2e.spec.js', // only run e2e tests
|
||||
grepInvert: /@snapshot/,
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
@@ -44,6 +47,8 @@ const config = {
|
||||
},
|
||||
{
|
||||
name: 'safari',
|
||||
testMatch: '**/*.e2e.spec.js', // only run e2e tests
|
||||
grep: /@ipad/, // only run ipad tests due to this bug https://github.com/microsoft/playwright/issues/8340
|
||||
grepInvert: /@snapshot/,
|
||||
use: {
|
||||
browserName: 'webkit'
|
||||
@@ -51,6 +56,7 @@ const config = {
|
||||
},
|
||||
{
|
||||
name: 'firefox',
|
||||
testMatch: '**/*.e2e.spec.js', // only run e2e tests
|
||||
grepInvert: /@snapshot/,
|
||||
use: {
|
||||
browserName: 'firefox'
|
||||
@@ -58,6 +64,7 @@ const config = {
|
||||
},
|
||||
{
|
||||
name: 'canary',
|
||||
testMatch: '**/*.e2e.spec.js', // only run e2e tests
|
||||
grepInvert: /@snapshot/,
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
@@ -66,11 +73,22 @@ const config = {
|
||||
},
|
||||
{
|
||||
name: 'chrome-beta',
|
||||
testMatch: '**/*.e2e.spec.js', // only run e2e tests
|
||||
grepInvert: /@snapshot/,
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
channel: 'chrome-beta'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'ipad',
|
||||
testMatch: '**/*.e2e.spec.js', // only run e2e tests
|
||||
grep: /@ipad/,
|
||||
grepInvert: /@snapshot/,
|
||||
use: {
|
||||
browserName: 'webkit',
|
||||
...devices['iPad (gen 7) landscape'] // Complete List https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json
|
||||
}
|
||||
}
|
||||
],
|
||||
reporter: [
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
// playwright.config.js
|
||||
// @ts-check
|
||||
|
||||
import { devices } from '@playwright/test';
|
||||
const MAX_FAILURES = 5;
|
||||
const NUM_WORKERS = 2;
|
||||
|
||||
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: NUM_WORKERS, //Limit to 2 for CircleCI Agent
|
||||
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,9 +1,6 @@
|
||||
// playwright.config.js
|
||||
// @ts-check
|
||||
|
||||
import { devices } from '@playwright/test';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
/** @type {import('@playwright/test').PlaywrightTestConfig} */
|
||||
const config = {
|
||||
retries: 0, //Retries are not needed with watch mode
|
||||
@@ -31,28 +28,6 @@ const config = {
|
||||
use: {
|
||||
browserName: 'chromium'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'ipad',
|
||||
grep: /@mobile/,
|
||||
use: {
|
||||
storageState: fileURLToPath(
|
||||
new URL('./test-data/display_layout_with_child_layouts.json', import.meta.url)
|
||||
),
|
||||
browserName: 'webkit',
|
||||
...devices['iPad (gen 7) landscape'] // Complete List https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'iphone',
|
||||
grep: /@mobile/,
|
||||
use: {
|
||||
storageState: fileURLToPath(
|
||||
new URL('./test-data/display_layout_with_child_layouts.json', import.meta.url)
|
||||
),
|
||||
browserName: 'webkit',
|
||||
...devices['iPhone 14 Pro'] // Complete List https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json
|
||||
}
|
||||
}
|
||||
],
|
||||
reporter: [
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
"end": 1660343797000,
|
||||
"type": "Group 1",
|
||||
"color": "orange",
|
||||
"textColor": "white",
|
||||
"id": 1
|
||||
"textColor": "white"
|
||||
},
|
||||
{
|
||||
"name": "Past event 2",
|
||||
@@ -15,8 +14,7 @@
|
||||
"end": 1660429160000,
|
||||
"type": "Group 1",
|
||||
"color": "orange",
|
||||
"textColor": "white",
|
||||
"id": 2
|
||||
"textColor": "white"
|
||||
},
|
||||
{
|
||||
"name": "Past event 3",
|
||||
@@ -24,8 +22,7 @@
|
||||
"end": 1660503981000,
|
||||
"type": "Group 1",
|
||||
"color": "orange",
|
||||
"textColor": "white",
|
||||
"id": 3
|
||||
"textColor": "white"
|
||||
},
|
||||
{
|
||||
"name": "Past event 4",
|
||||
@@ -33,8 +30,7 @@
|
||||
"end": 1660624108000,
|
||||
"type": "Group 1",
|
||||
"color": "orange",
|
||||
"textColor": "white",
|
||||
"id": 4
|
||||
"textColor": "white"
|
||||
},
|
||||
{
|
||||
"name": "Past event 5",
|
||||
@@ -42,8 +38,7 @@
|
||||
"end": 1660681529000,
|
||||
"type": "Group 1",
|
||||
"color": "orange",
|
||||
"textColor": "white",
|
||||
"id": 5
|
||||
"textColor": "white"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
"end": 1660343797000,
|
||||
"type": "Group 1",
|
||||
"color": "orange",
|
||||
"textColor": "white",
|
||||
"id": 1
|
||||
"textColor": "white"
|
||||
},
|
||||
{
|
||||
"name": "Time until supper",
|
||||
@@ -15,8 +14,7 @@
|
||||
"end": 1650420410000,
|
||||
"type": "Group 2",
|
||||
"color": "blue",
|
||||
"textColor": "white",
|
||||
"id": 2
|
||||
"textColor": "white"
|
||||
}
|
||||
],
|
||||
"Group 2": [
|
||||
@@ -26,8 +24,7 @@
|
||||
"end": 1650320102001,
|
||||
"type": "Group 2",
|
||||
"color": "green",
|
||||
"textColor": "white",
|
||||
"id": 3
|
||||
"textColor": "white"
|
||||
},
|
||||
{
|
||||
"name": "Time since last accident",
|
||||
@@ -35,8 +32,7 @@
|
||||
"end": 1650320102002,
|
||||
"type": "Group 1",
|
||||
"color": "yellow",
|
||||
"textColor": "white",
|
||||
"id": 4
|
||||
"textColor": "white"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -23,8 +23,7 @@
|
||||
import {
|
||||
createDomainObjectWithDefaults,
|
||||
createNotification,
|
||||
expandEntireTree,
|
||||
openObjectTreeContextMenu
|
||||
expandEntireTree
|
||||
} from '../../appActions.js';
|
||||
import { expect, test } from '../../pluginFixtures.js';
|
||||
|
||||
@@ -167,13 +166,4 @@ test.describe('AppActions', () => {
|
||||
const locatorTreeCollapsedItems = locatorTree.locator('role=treeitem[expanded=false]');
|
||||
expect(await locatorTreeCollapsedItems.count()).toBe(0);
|
||||
});
|
||||
test('openObjectTreeContextMenu', async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
const folder = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder'
|
||||
});
|
||||
await openObjectTreeContextMenu(page, folder.url);
|
||||
await expect(page.getByLabel('Menu')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
This test suite is dedicated to tests which 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 { expect, test } from '../../../pluginFixtures.js';
|
||||
import { test } from '../../../pluginFixtures.js';
|
||||
|
||||
const testPlan1 = JSON.parse(
|
||||
fs.readFileSync(
|
||||
@@ -63,47 +63,4 @@ 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'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,11 +30,6 @@ const examplePlanSmall3 = JSON.parse(
|
||||
new URL('../../../test-data/examplePlans/ExamplePlan_Small3.json', import.meta.url)
|
||||
)
|
||||
);
|
||||
const examplePlanSmall1 = JSON.parse(
|
||||
fs.readFileSync(
|
||||
new URL('../../../test-data/examplePlans/ExamplePlan_Small1.json', import.meta.url)
|
||||
)
|
||||
);
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const START_TIME_COLUMN = 0;
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
@@ -43,10 +38,55 @@ const TIME_TO_FROM_COLUMN = 2;
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const ACTIVITY_COLUMN = 3;
|
||||
const HEADER_ROW = 0;
|
||||
const NUM_COLUMNS = 5;
|
||||
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, verify all the activities are displayed with no milliseconds and selecting an activity shows it's properties", async ({
|
||||
test('Create a Time List, add a single Plan to it and verify all the activities are displayed with no milliseconds', async ({
|
||||
page
|
||||
}) => {
|
||||
// Goto baseURL
|
||||
@@ -63,16 +103,12 @@ test.describe('Time List', () => {
|
||||
await test.step('Create a Plan and add it to the timelist', async () => {
|
||||
await createPlanFromJSON(page, {
|
||||
name: 'Test Plan',
|
||||
json: examplePlanSmall1,
|
||||
json: testPlan,
|
||||
parent: timelist.uuid
|
||||
});
|
||||
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;
|
||||
|
||||
const startBound = testPlan.TEST_GROUP[0].start;
|
||||
const endBound = testPlan.TEST_GROUP[testPlan.TEST_GROUP.length - 1].end;
|
||||
|
||||
// Switch to fixed time mode with all plan events within the bounds
|
||||
await page.goto(
|
||||
@@ -82,7 +118,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(firstGroupItems.length);
|
||||
await expect(eventCount - 1).toEqual(testPlan.TEST_GROUP.length);
|
||||
});
|
||||
|
||||
await test.step('Does not show milliseconds in times', async () => {
|
||||
@@ -95,81 +131,6 @@ test.describe('Time List', () => {
|
||||
await expect(row.locator('.--end')).not.toContainText('.');
|
||||
await expect(row.locator('.--duration')).not.toContainText('.');
|
||||
});
|
||||
|
||||
await test.step('Shows activity properties when a row is selected', async () => {
|
||||
await page.getByRole('row').nth(2).click();
|
||||
|
||||
// Find the activity state section in the inspector
|
||||
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'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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' });
|
||||
|
||||
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);
|
||||
|
||||
return createdTimeList;
|
||||
});
|
||||
|
||||
await test.step('Create a Plan and add it to the timelist', async () => {
|
||||
await createPlanFromJSON(page, {
|
||||
name: 'Test Plan',
|
||||
json: examplePlanSmall1,
|
||||
parent: timelist.uuid
|
||||
});
|
||||
|
||||
// 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', 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'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -186,8 +147,8 @@ test("View a timelist in expanded view, verify all the activities are displayed
|
||||
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).
|
||||
* @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.
|
||||
@@ -259,13 +220,11 @@ test.describe('Time List with controlled clock', () => {
|
||||
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('-');
|
||||
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 getAndAssertCountdownOrUpObject(page, i + 3);
|
||||
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));
|
||||
});
|
||||
@@ -274,17 +233,15 @@ test.describe('Time List with controlled clock', () => {
|
||||
// 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];
|
||||
const countdownCell = 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('+');
|
||||
const beforeCountdown = await getAndAssertCountdownObject(page, i + 1);
|
||||
// Wait until it changes
|
||||
await expect(countUpCell).not.toHaveText(beforeCountUp.toString());
|
||||
await expect(countdownCell).not.toHaveText(beforeCountdown.toString());
|
||||
// Get the new count-up timestamp object
|
||||
const afterCountUp = await getAndAssertCountdownOrUpObject(page, i + 1);
|
||||
const afterCountdown = await getAndAssertCountdownObject(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));
|
||||
expect(Number(afterCountdown.seconds)).toBeGreaterThan(Number(beforeCountdown.seconds));
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -314,13 +271,13 @@ async function getCellTextByIndex(page, rowIndex, columnIndex) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the text from the countdown (or countup) cell in the given row, assert that it matches the countdown/countup
|
||||
* 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<CountdownOrUpObject>} The countdown (or countup) object
|
||||
* @returns {Promise<CountdownObject>} countdownObject
|
||||
*/
|
||||
async function getAndAssertCountdownOrUpObject(page, rowIndex) {
|
||||
async function getAndAssertCountdownObject(page, rowIndex) {
|
||||
const timeToFrom = await getCellTextByIndex(page, HEADER_ROW + rowIndex, TIME_TO_FROM_COLUMN);
|
||||
|
||||
expect(timeToFrom).toMatch(COUNTDOWN_REGEXP);
|
||||
|
||||
@@ -35,7 +35,7 @@ import { expect, test } from '../../../../pluginFixtures.js';
|
||||
|
||||
let conditionSetUrl;
|
||||
|
||||
test.describe.serial('Condition Set CRUD Operations on @localStorage @2p', () => {
|
||||
test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
//TODO: This needs to be refactored
|
||||
const context = await browser.newContext();
|
||||
@@ -68,35 +68,30 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage @2p', () =>
|
||||
});
|
||||
|
||||
//Begin suite of tests again localStorage
|
||||
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' });
|
||||
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' });
|
||||
|
||||
//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;
|
||||
|
||||
|
||||
@@ -161,13 +161,6 @@ 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
|
||||
|
||||
@@ -136,11 +136,7 @@ test.describe('Gauge', () => {
|
||||
// TODO: Verify changes in the UI
|
||||
});
|
||||
|
||||
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'
|
||||
});
|
||||
test('Gauge does not display NaN when data not available', async ({ page }) => {
|
||||
// Create a Gauge
|
||||
const gauge = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Gauge'
|
||||
|
||||
@@ -363,7 +363,7 @@ test.describe('Example Imagery in Display Layout', () => {
|
||||
await page.locator('li[title="View Large"]').click();
|
||||
await expect(pausePlayButton).toHaveClass(/is-paused/);
|
||||
|
||||
await page.getByRole('button', { name: 'Close' }).click();
|
||||
await page.locator('[aria-label="Close"]').click();
|
||||
await expect.soft(pausePlayButton).not.toHaveClass(/is-paused/);
|
||||
});
|
||||
|
||||
@@ -386,7 +386,7 @@ test.describe('Example Imagery in Display Layout', () => {
|
||||
await page.locator('li[title="View Large"]').click();
|
||||
await expect(pausePlayButton).toHaveClass(/is-paused/);
|
||||
|
||||
await page.getByRole('button', { name: 'Close' }).click();
|
||||
await page.locator('[aria-label="Close"]').click();
|
||||
await expect.soft(pausePlayButton).toHaveClass(/is-paused/);
|
||||
});
|
||||
|
||||
@@ -509,7 +509,7 @@ test.describe('Example Imagery in Flexible layout', () => {
|
||||
await page.getByRole('button', { name: 'Background Image', state: 'visible' });
|
||||
|
||||
// Close the large view
|
||||
await page.getByRole('button', { name: 'Close' }).click();
|
||||
await page.getByLabel('Close').click();
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
|
||||
@@ -24,182 +24,36 @@
|
||||
This test suite is dedicated to tests which verify the basic operations surrounding exportAsJSON.
|
||||
*/
|
||||
|
||||
import fs from 'fs/promises';
|
||||
|
||||
import {
|
||||
createDomainObjectWithDefaults,
|
||||
openObjectTreeContextMenu
|
||||
} from '../../../../appActions.js';
|
||||
// FIXME: Remove this eslint exception once tests are implemented
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { expect, test } from '../../../../baseFixtures.js';
|
||||
import { navigateToFaultManagementWithExample } from '../../../../helper/faultUtils.js';
|
||||
|
||||
test.describe('ExportAsJSON', () => {
|
||||
let folder;
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Go to baseURL
|
||||
await page.goto('./');
|
||||
// Perform actions to create the domain object
|
||||
folder = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder',
|
||||
name: 'e2e folder'
|
||||
});
|
||||
});
|
||||
test('Create a basic object and verify that it can be exported as JSON from Tree', async ({
|
||||
page
|
||||
}) => {
|
||||
// Navigate to the page
|
||||
await page.goto(folder.url);
|
||||
|
||||
// Open context menu and initiate download
|
||||
await openObjectTreeContextMenu(page, folder.url);
|
||||
const [download] = await Promise.all([
|
||||
page.waitForEvent('download'), // Waits for the download event
|
||||
page.getByLabel('Export as JSON').click() // Triggers the download
|
||||
]);
|
||||
|
||||
// Wait for the download process to complete
|
||||
const path = await download.path();
|
||||
|
||||
// Read the contents of the downloaded file using readFile from fs/promises
|
||||
const fileContents = await fs.readFile(path, 'utf8');
|
||||
const jsonData = JSON.parse(fileContents);
|
||||
|
||||
// Use the function to retrieve the key
|
||||
const key = getFirstKeyFromOpenMctJson(jsonData);
|
||||
|
||||
// Verify the contents of the JSON file
|
||||
expect(jsonData.openmct[key]).toHaveProperty('name', 'e2e folder');
|
||||
expect(jsonData.openmct[key]).toHaveProperty('type', 'folder');
|
||||
});
|
||||
test('Create a basic object and verify that it can be exported as JSON from 3 dot menu', async ({
|
||||
page
|
||||
}) => {
|
||||
// Navigate to the page
|
||||
await page.goto(folder.url);
|
||||
//3 dot menu
|
||||
await page.getByLabel('More actions').click();
|
||||
// Open context menu and initiate download
|
||||
const [download] = await Promise.all([
|
||||
page.waitForEvent('download'), // Waits for the download event
|
||||
page.getByLabel('Export as JSON').click() // Triggers the download
|
||||
]);
|
||||
|
||||
// Read the contents of the downloaded file using readFile from fs/promises
|
||||
const fileContents = await fs.readFile(await download.path(), 'utf8');
|
||||
const jsonData = JSON.parse(fileContents);
|
||||
|
||||
// Use the function to retrieve the key
|
||||
const key = getFirstKeyFromOpenMctJson(jsonData);
|
||||
|
||||
// Verify the contents of the JSON file
|
||||
expect(jsonData.openmct[key]).toHaveProperty('name', 'e2e folder');
|
||||
expect(jsonData.openmct[key]).toHaveProperty('type', 'folder');
|
||||
});
|
||||
test('Verify that a nested Object can be exported as JSON', async ({ page }) => {
|
||||
const timer = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Timer',
|
||||
name: 'timer',
|
||||
parent: folder.uuid
|
||||
});
|
||||
// Navigate to the page
|
||||
await page.goto(timer.url);
|
||||
|
||||
//do this against parent folder.url, NOT timer.url child
|
||||
await openObjectTreeContextMenu(page, folder.url);
|
||||
// Open context menu and initiate download
|
||||
const [download] = await Promise.all([
|
||||
page.waitForEvent('download'), // Waits for the download event
|
||||
page.getByLabel('Export as JSON').click() // Triggers the download
|
||||
]);
|
||||
|
||||
// Read the contents of the downloaded file
|
||||
const fileContents = await fs.readFile(await download.path(), 'utf8');
|
||||
const jsonData = JSON.parse(fileContents);
|
||||
|
||||
// Retrieve the keys for folder and timer
|
||||
const folderKey = getFirstKeyFromOpenMctJson(jsonData);
|
||||
const timerKey = jsonData.openmct[folderKey].composition[0].key;
|
||||
|
||||
// Verify the folder properties
|
||||
expect(jsonData.openmct[folderKey]).toHaveProperty('name', 'e2e folder');
|
||||
expect(jsonData.openmct[folderKey]).toHaveProperty('type', 'folder');
|
||||
|
||||
// Verify the timer properties
|
||||
expect(jsonData.openmct[timerKey]).toHaveProperty('name', 'timer');
|
||||
expect(jsonData.openmct[timerKey]).toHaveProperty('type', 'timer');
|
||||
|
||||
// Verify the composition of the folder includes the timer
|
||||
expect(jsonData.openmct[folderKey].composition).toEqual(
|
||||
expect.arrayContaining([expect.objectContaining({ key: timerKey })])
|
||||
);
|
||||
test.fixme(
|
||||
'Create a basic object and verify that it can be exported as JSON from Tree',
|
||||
async ({ page }) => {
|
||||
//Create domain object
|
||||
//Save Domain Object
|
||||
//Verify that the newly created domain object can be exported as JSON from the Tree
|
||||
}
|
||||
);
|
||||
test.fixme(
|
||||
'Create a basic object and verify that it can be exported as JSON from 3 dot menu',
|
||||
async ({ page }) => {
|
||||
//Create domain object
|
||||
//Save Domain Object
|
||||
//Verify that the newly created domain object can be exported as JSON from the 3 dot menu
|
||||
}
|
||||
);
|
||||
test.fixme('Verify that a nested Object can be exported as JSON', async ({ page }) => {
|
||||
// Create 2 objects with hierarchy
|
||||
// Export as JSON
|
||||
// Verify Hierarchy
|
||||
});
|
||||
test.fixme(
|
||||
'Verify that the ExportAsJSON dropdown does not appear for the item X',
|
||||
async ({ page }) => {
|
||||
// Other than non-persistable objects
|
||||
}
|
||||
);
|
||||
});
|
||||
test.describe('ExportAsJSON Disabled Actions', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
//Use a Fault Management Object which is not composable
|
||||
await navigateToFaultManagementWithExample(page);
|
||||
});
|
||||
test('Verify that the ExportAsJSON dropdown does not appear for the item X', async ({ page }) => {
|
||||
await page.getByLabel('More actions').click();
|
||||
await expect(await page.getByLabel('Export as JSON')).toHaveCount(0);
|
||||
|
||||
await page.getByRole('treeitem', { name: 'Fault Management' }).click({
|
||||
button: 'right'
|
||||
});
|
||||
await expect(await page.getByLabel('Export as JSON')).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
test.describe('ExportAsJSON ProgressBar @couchdb', () => {
|
||||
let folder;
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
// Perform actions to create the domain object
|
||||
folder = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder'
|
||||
});
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Timer',
|
||||
parent: folder.uuid
|
||||
});
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Timer',
|
||||
parent: folder.uuid
|
||||
});
|
||||
});
|
||||
test('Verify that the ExportAsJSON action creates a progressbar', async ({ page }) => {
|
||||
// Navigate to the page
|
||||
await page.goto(folder.url);
|
||||
|
||||
//Export My Items to create a large export
|
||||
await page.getByRole('treeitem', { name: 'My Items' }).click({ button: 'right' });
|
||||
// Open context menu and initiate download
|
||||
await Promise.all([
|
||||
page.getByRole('progressbar'), // This is just a check for the progress bar
|
||||
page.getByText(
|
||||
'Do not navigate away from this page or close this browser tab while this message'
|
||||
), // This is the text associated with the download
|
||||
page.waitForEvent('download'), // Waits for the download event
|
||||
page.getByLabel('Export as JSON').click() // Triggers the download
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Retrieves the first key from the 'openmct' property of the provided JSON object.
|
||||
*
|
||||
* @param {Object} jsonData - The JSON object containing the 'openmct' property.
|
||||
* @returns {string} The first key found in the 'openmct' object.
|
||||
* @throws {Error} If no keys are found in the 'openmct' object.
|
||||
*/
|
||||
function getFirstKeyFromOpenMctJson(jsonData) {
|
||||
if (!jsonData.openmct) {
|
||||
throw new Error("The provided JSON object does not have an 'openmct' property.");
|
||||
}
|
||||
|
||||
const keys = Object.keys(jsonData.openmct);
|
||||
if (keys.length === 0) {
|
||||
throw new Error('No keys found in the openmct object');
|
||||
}
|
||||
|
||||
return keys[0];
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ test.describe('Testing numeric data with inspector data visualization (i.e., dat
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
});
|
||||
|
||||
test('Can click on telemetry and see data in inspector @2p', async ({ page, context }) => {
|
||||
test('Can click on telemetry and see data in inspector', async ({ page }) => {
|
||||
const exampleDataVisualizationSource = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Example Data Visualization Source'
|
||||
});
|
||||
@@ -67,16 +67,5 @@ test.describe('Testing numeric data with inspector data visualization (i.e., dat
|
||||
page.locator('span.plot-series-name', { hasText: 'Second Sine Wave Generator Hz' })
|
||||
).toBeVisible();
|
||||
await expect(page.locator('.js-series-data-loaded')).toBeVisible();
|
||||
|
||||
// test new tab
|
||||
await page.getByLabel('Inspector Views').getByLabel('More actions').click();
|
||||
const pagePromise = context.waitForEvent('page');
|
||||
await page.getByRole('menuitem', { name: /Open In New Tab/ }).click();
|
||||
|
||||
// ensure our new tab's title is correct
|
||||
const newPage = await pagePromise;
|
||||
await newPage.waitForLoadState();
|
||||
// expect new tab title to contain 'Second Sine Wave Generator'
|
||||
await expect(newPage).toHaveTitle('Second Sine Wave Generator');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -56,9 +56,9 @@ test.describe('Testing LAD table configuration', () => {
|
||||
await page.getByRole('tab', { name: 'LAD Table Configuration' }).click();
|
||||
|
||||
// make sure headers are visible initially
|
||||
await expect(page.getByRole('cell', { name: 'Timestamp', exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'Units' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'Type', exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'Type' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'WATCH' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'WARNING' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'DISTRESS' })).toBeVisible();
|
||||
@@ -66,10 +66,10 @@ test.describe('Testing LAD table configuration', () => {
|
||||
await expect(page.getByRole('cell', { name: 'SEVERE' })).toBeVisible();
|
||||
|
||||
// hide timestamp column
|
||||
await page.getByLabel('Timestamp', { exact: true }).uncheck();
|
||||
await expect(page.getByRole('cell', { name: 'Timestamp', exact: true })).toBeHidden();
|
||||
await page.getByLabel('Timestamp').uncheck();
|
||||
await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeHidden();
|
||||
await expect(page.getByRole('cell', { name: 'Units' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'Type', exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'Type' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'WATCH' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'WARNING' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'DISTRESS' })).toBeVisible();
|
||||
@@ -78,10 +78,10 @@ test.describe('Testing LAD table configuration', () => {
|
||||
|
||||
// hide units & type column
|
||||
await page.getByLabel('Units').uncheck();
|
||||
await page.getByLabel('Type', { exact: true }).uncheck();
|
||||
await expect(page.getByRole('cell', { name: 'Timestamp', exact: true })).toBeHidden();
|
||||
await page.getByLabel('Type').uncheck();
|
||||
await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeHidden();
|
||||
await expect(page.getByRole('cell', { name: 'Units' })).toBeHidden();
|
||||
await expect(page.getByRole('cell', { name: 'Type', exact: true })).toBeHidden();
|
||||
await expect(page.getByRole('cell', { name: 'Type' })).toBeHidden();
|
||||
await expect(page.getByRole('cell', { name: 'WATCH' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'WARNING' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'DISTRESS' })).toBeVisible();
|
||||
@@ -90,9 +90,9 @@ test.describe('Testing LAD table configuration', () => {
|
||||
|
||||
// hide WATCH column
|
||||
await page.getByLabel('WATCH').uncheck();
|
||||
await expect(page.getByRole('cell', { name: 'Timestamp', exact: true })).toBeHidden();
|
||||
await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeHidden();
|
||||
await expect(page.getByRole('cell', { name: 'Units' })).toBeHidden();
|
||||
await expect(page.getByRole('cell', { name: 'Type', exact: true })).toBeHidden();
|
||||
await expect(page.getByRole('cell', { name: 'Type' })).toBeHidden();
|
||||
await expect(page.getByRole('cell', { name: 'WATCH' })).toBeHidden();
|
||||
await expect(page.getByRole('cell', { name: 'WARNING' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'DISTRESS' })).toBeVisible();
|
||||
@@ -103,9 +103,9 @@ test.describe('Testing LAD table configuration', () => {
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
await page.reload();
|
||||
await expect(page.getByRole('cell', { name: 'Timestamp', exact: true })).toBeHidden();
|
||||
await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeHidden();
|
||||
await expect(page.getByRole('cell', { name: 'Units' })).toBeHidden();
|
||||
await expect(page.getByRole('cell', { name: 'Type', exact: true })).toBeHidden();
|
||||
await expect(page.getByRole('cell', { name: 'Type' })).toBeHidden();
|
||||
await expect(page.getByRole('cell', { name: 'WATCH' })).toBeHidden();
|
||||
await expect(page.getByRole('cell', { name: 'WARNING' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'DISTRESS' })).toBeVisible();
|
||||
@@ -117,10 +117,10 @@ test.describe('Testing LAD table configuration', () => {
|
||||
await page.getByRole('tab', { name: 'LAD Table Configuration' }).click();
|
||||
|
||||
// show timestamp column
|
||||
await page.getByLabel('Timestamp', { exact: true }).check();
|
||||
await page.getByLabel('Timestamp').check();
|
||||
await expect(page.getByRole('cell', { name: 'Units' })).toBeHidden();
|
||||
await expect(page.getByRole('cell', { name: 'Type', exact: true })).toBeHidden();
|
||||
await expect(page.getByRole('cell', { name: 'Timestamp', exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'Type' })).toBeHidden();
|
||||
await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'WATCH' })).toBeHidden();
|
||||
await expect(page.getByRole('cell', { name: 'WARNING' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'DISTRESS' })).toBeVisible();
|
||||
@@ -132,8 +132,8 @@ test.describe('Testing LAD table configuration', () => {
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
await page.reload();
|
||||
await expect(page.getByRole('cell', { name: 'Units' })).toBeHidden();
|
||||
await expect(page.getByRole('cell', { name: 'Type', exact: true })).toBeHidden();
|
||||
await expect(page.getByRole('cell', { name: 'Timestamp', exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'Type' })).toBeHidden();
|
||||
await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'WATCH' })).toBeHidden();
|
||||
await expect(page.getByRole('cell', { name: 'WARNING' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'DISTRESS' })).toBeVisible();
|
||||
@@ -146,11 +146,11 @@ test.describe('Testing LAD table configuration', () => {
|
||||
|
||||
// show units, type, and WATCH columns
|
||||
await page.getByLabel('Units').check();
|
||||
await page.getByLabel('Type', { exact: true }).check();
|
||||
await page.getByLabel('Type').check();
|
||||
await page.getByLabel('WATCH').check();
|
||||
await expect(page.getByRole('cell', { name: 'Timestamp', exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'Units' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'Type', exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'Type' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'WATCH' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'WARNING' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'DISTRESS' })).toBeVisible();
|
||||
@@ -161,9 +161,9 @@ test.describe('Testing LAD table configuration', () => {
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
await page.reload();
|
||||
await expect(page.getByRole('cell', { name: 'Timestamp', exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'Units' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'Type', exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'Type' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'WATCH' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'WARNING' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'DISTRESS' })).toBeVisible();
|
||||
@@ -185,9 +185,9 @@ test.describe('Testing LAD table configuration', () => {
|
||||
await page.getByRole('tab', { name: 'LAD Table Configuration' }).click();
|
||||
|
||||
// make sure Sine Wave headers are visible initially too
|
||||
await expect(page.getByRole('cell', { name: 'Timestamp', exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'Units' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'Type', exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'Type' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'WATCH' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'WARNING' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'DISTRESS' })).toBeVisible();
|
||||
@@ -206,8 +206,8 @@ test.describe('Testing LAD table configuration', () => {
|
||||
// Ensure Units & Limit columns are gone
|
||||
// as Event Generator don't have them
|
||||
await page.goto(ladTable.url);
|
||||
await expect(page.getByRole('cell', { name: 'Timestamp', exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'Type', exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'Type' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'Units' })).toBeHidden();
|
||||
await expect(page.getByRole('cell', { name: 'WATCH' })).toBeHidden();
|
||||
await expect(page.getByRole('cell', { name: 'WARNING' })).toBeHidden();
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import { createDomainObjectWithDefaults } from '../../../../appActions.js';
|
||||
import { expect, test } from '../../../../pluginFixtures.js';
|
||||
|
||||
test.describe('LAD Table Sets', () => {
|
||||
test('Ensure we have numbers in cells', async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
const ladTableSet = await createDomainObjectWithDefaults(page, {
|
||||
type: 'LAD Table Set'
|
||||
});
|
||||
|
||||
const firstLadTable = await createDomainObjectWithDefaults(page, {
|
||||
type: 'LAD Table',
|
||||
parent: ladTableSet.uuid
|
||||
});
|
||||
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
parent: firstLadTable.uuid
|
||||
});
|
||||
|
||||
const secondLadTable = await createDomainObjectWithDefaults(page, {
|
||||
type: 'LAD Table',
|
||||
parent: ladTableSet.uuid
|
||||
});
|
||||
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
parent: secondLadTable.uuid
|
||||
});
|
||||
|
||||
await page.goto(ladTableSet.url);
|
||||
|
||||
// Wait for the initial value to show after mount
|
||||
await expect(page.getByLabel('lad value').first()).not.toContainText('---');
|
||||
|
||||
const valueFromFirstSineWave = await page.getByLabel('lad value').first().innerText();
|
||||
const firstSineWaveNumber = parseFloat(valueFromFirstSineWave);
|
||||
// ensure we have a float value in the cell and it's finite
|
||||
expect(Number.isFinite(firstSineWaveNumber)).toBeTruthy();
|
||||
|
||||
const valueFromSecondSineWave = await page.getByLabel('lad value').last().innerText();
|
||||
const secondSineWaveNumber = parseFloat(valueFromSecondSineWave);
|
||||
// ensure we have a float value in the cell and it's finite
|
||||
expect(Number.isFinite(secondSineWaveNumber)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,147 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
This test suite is dedicated to tests which verify the basic operations surrounding Notebooks.
|
||||
*/
|
||||
|
||||
import fs from 'fs/promises';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
import { createDomainObjectWithDefaults } from '../../../../appActions.js';
|
||||
import { expect, test } from '../../../../pluginFixtures.js';
|
||||
|
||||
const NOTEBOOK_NAME = 'Notebook';
|
||||
|
||||
test.describe('Snapshot image tests', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
//Navigate to baseURL
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Create Notebook
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: NOTEBOOK_NAME
|
||||
});
|
||||
});
|
||||
|
||||
test('Can drop an image onto a notebook and create a new entry', async ({ page }) => {
|
||||
const imageData = await fs.readFile(
|
||||
fileURLToPath(
|
||||
new URL('../../../../../src/images/favicons/favicon-96x96.png', import.meta.url)
|
||||
)
|
||||
);
|
||||
const imageArray = new Uint8Array(imageData);
|
||||
const fileData = Array.from(imageArray);
|
||||
|
||||
const dropTransfer = await page.evaluateHandle((data) => {
|
||||
const dataTransfer = new DataTransfer();
|
||||
const file = new File([new Uint8Array(data)], 'favicon-96x96.png', { type: 'image/png' });
|
||||
dataTransfer.items.add(file);
|
||||
return dataTransfer;
|
||||
}, fileData);
|
||||
|
||||
await page.dispatchEvent('.c-notebook__drag-area', 'drop', { dataTransfer: dropTransfer });
|
||||
await page.locator('.c-ne__save-button > button').click();
|
||||
// be sure that entry was created
|
||||
await expect(page.getByText('favicon-96x96.png')).toBeVisible();
|
||||
|
||||
await page.getByRole('img', { name: 'favicon-96x96.png thumbnail' }).click();
|
||||
// expect large image to be displayed
|
||||
await expect(page.getByRole('dialog').getByText('favicon-96x96.png')).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Close' }).click();
|
||||
|
||||
// drop another image onto the entry
|
||||
await page.dispatchEvent('.c-snapshots', 'drop', { dataTransfer: dropTransfer });
|
||||
|
||||
const secondThumbnail = page.getByRole('img', { name: 'favicon-96x96.png thumbnail' }).nth(1);
|
||||
await secondThumbnail.waitFor({ state: 'attached' });
|
||||
// expect two embedded images now
|
||||
expect(await page.getByRole('img', { name: 'favicon-96x96.png thumbnail' }).count()).toBe(2);
|
||||
|
||||
await page.locator('.c-snapshot.c-ne__embed').first().getByTitle('More actions').click();
|
||||
|
||||
await page.getByRole('menuitem', { name: /Remove This Embed/ }).click();
|
||||
await page.getByRole('button', { name: 'Ok', exact: true }).click();
|
||||
// Ensure that the thumbnail is removed before we assert
|
||||
await secondThumbnail.waitFor({ state: 'detached' });
|
||||
|
||||
// expect one embedded image now as we deleted the other
|
||||
expect(await page.getByRole('img', { name: 'favicon-96x96.png thumbnail' }).count()).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Snapshot image failure tests', () => {
|
||||
test.use({ failOnConsoleError: false });
|
||||
test.beforeEach(async ({ page }) => {
|
||||
//Navigate to baseURL
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Create Notebook
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: NOTEBOOK_NAME
|
||||
});
|
||||
});
|
||||
|
||||
test('Get an error notification when dropping unknown file onto notebook entry', async ({
|
||||
page
|
||||
}) => {
|
||||
// fill Uint8Array array with some garbage data
|
||||
const garbageData = new Uint8Array(100);
|
||||
const fileData = Array.from(garbageData);
|
||||
|
||||
const dropTransfer = await page.evaluateHandle((data) => {
|
||||
const dataTransfer = new DataTransfer();
|
||||
const file = new File([new Uint8Array(data)], 'someGarbage.foo', { type: 'unknown/garbage' });
|
||||
dataTransfer.items.add(file);
|
||||
return dataTransfer;
|
||||
}, fileData);
|
||||
|
||||
await page.dispatchEvent('.c-notebook__drag-area', 'drop', { dataTransfer: dropTransfer });
|
||||
|
||||
// should have gotten a notification from OpenMCT that we couldn't add it
|
||||
await expect(page.getByText('Unknown object(s) dropped and cannot embed')).toBeVisible();
|
||||
});
|
||||
|
||||
test('Get an error notification when dropping big files onto notebook entry', async ({
|
||||
page
|
||||
}) => {
|
||||
const garbageSize = 15 * 1024 * 1024; // 15 megabytes
|
||||
|
||||
await page.addScriptTag({
|
||||
// make the garbage client side
|
||||
content: `window.bigGarbageData = new Uint8Array(${garbageSize})`
|
||||
});
|
||||
|
||||
const bigDropTransfer = await page.evaluateHandle(() => {
|
||||
const dataTransfer = new DataTransfer();
|
||||
const file = new File([window.bigGarbageData], 'bigBoy.png', { type: 'image/png' });
|
||||
dataTransfer.items.add(file);
|
||||
return dataTransfer;
|
||||
});
|
||||
|
||||
await page.dispatchEvent('.c-notebook__drag-area', 'drop', { dataTransfer: bigDropTransfer });
|
||||
|
||||
// should have gotten a notification from OpenMCT that we couldn't add it as it's too big
|
||||
await expect(page.getByText('unable to embed')).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -24,8 +24,14 @@
|
||||
This test suite is dedicated to tests which verify the basic operations surrounding Notebooks.
|
||||
*/
|
||||
|
||||
import fs from 'fs/promises';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
import { createDomainObjectWithDefaults } from '../../../../appActions.js';
|
||||
import { expect, test } from '../../../../pluginFixtures.js';
|
||||
|
||||
const NOTEBOOK_NAME = 'Notebook';
|
||||
|
||||
test.describe('Snapshot Menu tests', () => {
|
||||
test.fixme(
|
||||
'When no default notebook is selected, Snapshot Menu dropdown should only have a single option',
|
||||
@@ -85,13 +91,22 @@ test.describe('Snapshot Container tests', () => {
|
||||
|
||||
await page.getByLabel('Take a Notebook Snapshot').click();
|
||||
await page.getByRole('menuitem', { name: 'Save to Notebook Snapshots' }).click();
|
||||
await page.getByLabel('Show Snapshots').click();
|
||||
await page.getByRole('button', { name: 'Show' }).click();
|
||||
});
|
||||
test('A snapshot can be Quick Viewed from Container with 3 dot action menu', async ({ page }) => {
|
||||
await page.locator('.c-snapshot.c-ne__embed').first().getByTitle('More actions').click();
|
||||
await page.getByRole('menuitem', { name: 'Quick View' }).click();
|
||||
await expect(page.locator('.c-overlay__outer')).toBeVisible();
|
||||
});
|
||||
test.fixme('5 Snapshots can be added to a container', async ({ page }) => {});
|
||||
test.fixme(
|
||||
'5 Snapshots can be added to a container and Deleted with Delete All action',
|
||||
async ({ page }) => {}
|
||||
);
|
||||
test.fixme(
|
||||
'A snapshot can be Deleted from Container with 3 dot action menu',
|
||||
async ({ page }) => {}
|
||||
);
|
||||
test.fixme(
|
||||
'A snapshot can be Viewed, Annotated, display deleted, and saved from Container with 3 dot action menu',
|
||||
async ({ page }) => {
|
||||
@@ -107,15 +122,7 @@ test.describe('Snapshot Container tests', () => {
|
||||
//await expect(await page.locator)
|
||||
}
|
||||
);
|
||||
test.fixme('5 Snapshots can be added to a container', async ({ page }) => {});
|
||||
test.fixme(
|
||||
'5 Snapshots can be added to a container and Deleted with Delete All action',
|
||||
async ({ page }) => {}
|
||||
);
|
||||
test.fixme(
|
||||
'A snapshot can be Deleted from Container with 3 dot action menu',
|
||||
async ({ page }) => {}
|
||||
);
|
||||
|
||||
test.fixme(
|
||||
'A snapshot can be Navigated To from Container with 3 dot action menu',
|
||||
async ({ page }) => {}
|
||||
@@ -159,3 +166,117 @@ test.describe('Snapshot Container tests', () => {
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test.describe('Snapshot image tests', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
//Navigate to baseURL
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Create Notebook
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: NOTEBOOK_NAME
|
||||
});
|
||||
});
|
||||
|
||||
test('Can drop an image onto a notebook and create a new entry', async ({ page }) => {
|
||||
const imageData = await fs.readFile(
|
||||
fileURLToPath(
|
||||
new URL('../../../../../src/images/favicons/favicon-96x96.png', import.meta.url)
|
||||
)
|
||||
);
|
||||
const imageArray = new Uint8Array(imageData);
|
||||
const fileData = Array.from(imageArray);
|
||||
|
||||
const dropTransfer = await page.evaluateHandle((data) => {
|
||||
const dataTransfer = new DataTransfer();
|
||||
const file = new File([new Uint8Array(data)], 'favicon-96x96.png', { type: 'image/png' });
|
||||
dataTransfer.items.add(file);
|
||||
return dataTransfer;
|
||||
}, fileData);
|
||||
|
||||
await page.dispatchEvent('.c-notebook__drag-area', 'drop', { dataTransfer: dropTransfer });
|
||||
await page.locator('.c-ne__save-button > button').click();
|
||||
// be sure that entry was created
|
||||
await expect(page.getByText('favicon-96x96.png')).toBeVisible();
|
||||
|
||||
await page.getByRole('img', { name: 'favicon-96x96.png thumbnail' }).click();
|
||||
// expect large image to be displayed
|
||||
await expect(page.getByRole('dialog').getByText('favicon-96x96.png')).toBeVisible();
|
||||
|
||||
await page.getByLabel('Close').click();
|
||||
|
||||
// drop another image onto the entry
|
||||
await page.dispatchEvent('.c-snapshots', 'drop', { dataTransfer: dropTransfer });
|
||||
|
||||
const secondThumbnail = page.getByRole('img', { name: 'favicon-96x96.png thumbnail' }).nth(1);
|
||||
await secondThumbnail.waitFor({ state: 'attached' });
|
||||
// expect two embedded images now
|
||||
expect(await page.getByRole('img', { name: 'favicon-96x96.png thumbnail' }).count()).toBe(2);
|
||||
|
||||
await page.locator('.c-snapshot.c-ne__embed').first().getByTitle('More actions').click();
|
||||
|
||||
await page.getByRole('menuitem', { name: /Remove This Embed/ }).click();
|
||||
await page.getByRole('button', { name: 'Ok', exact: true }).click();
|
||||
// Ensure that the thumbnail is removed before we assert
|
||||
await secondThumbnail.waitFor({ state: 'detached' });
|
||||
|
||||
// expect one embedded image now as we deleted the other
|
||||
expect(await page.getByRole('img', { name: 'favicon-96x96.png thumbnail' }).count()).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Snapshot image failure tests', () => {
|
||||
test.use({ failOnConsoleError: false });
|
||||
test.beforeEach(async ({ page }) => {
|
||||
//Navigate to baseURL
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Create Notebook
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: NOTEBOOK_NAME
|
||||
});
|
||||
});
|
||||
|
||||
test('Get an error notification when dropping unknown file onto notebook entry', async ({
|
||||
page
|
||||
}) => {
|
||||
// fill Uint8Array array with some garbage data
|
||||
const garbageData = new Uint8Array(100);
|
||||
const fileData = Array.from(garbageData);
|
||||
|
||||
const dropTransfer = await page.evaluateHandle((data) => {
|
||||
const dataTransfer = new DataTransfer();
|
||||
const file = new File([new Uint8Array(data)], 'someGarbage.foo', { type: 'unknown/garbage' });
|
||||
dataTransfer.items.add(file);
|
||||
return dataTransfer;
|
||||
}, fileData);
|
||||
|
||||
await page.dispatchEvent('.c-notebook__drag-area', 'drop', { dataTransfer: dropTransfer });
|
||||
|
||||
// should have gotten a notification from OpenMCT that we couldn't add it
|
||||
await expect(page.getByText('Unknown object(s) dropped and cannot embed')).toBeVisible();
|
||||
});
|
||||
|
||||
test('Get an error notification when dropping big files onto notebook entry', async ({
|
||||
page
|
||||
}) => {
|
||||
const garbageSize = 15 * 1024 * 1024; // 15 megabytes
|
||||
|
||||
await page.addScriptTag({
|
||||
// make the garbage client side
|
||||
content: `window.bigGarbageData = new Uint8Array(${garbageSize})`
|
||||
});
|
||||
|
||||
const bigDropTransfer = await page.evaluateHandle(() => {
|
||||
const dataTransfer = new DataTransfer();
|
||||
const file = new File([window.bigGarbageData], 'bigBoy.png', { type: 'image/png' });
|
||||
dataTransfer.items.add(file);
|
||||
return dataTransfer;
|
||||
});
|
||||
|
||||
await page.dispatchEvent('.c-notebook__drag-area', 'drop', { dataTransfer: bigDropTransfer });
|
||||
|
||||
// should have gotten a notification from OpenMCT that we couldn't add it as it's too big
|
||||
await expect(page.getByText('unable to embed')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -51,7 +51,7 @@ test.describe('Operator Status', () => {
|
||||
// Description should be empty https://github.com/nasa/openmct/issues/6978
|
||||
await expect(page.locator('.c-message__action-text')).toBeHidden();
|
||||
// set role
|
||||
await page.getByRole('button', { name: 'Select', exact: true }).click();
|
||||
await page.getByRole('button', { name: 'Select' }).click();
|
||||
// dismiss role confirmation popup
|
||||
await page.getByRole('button', { name: 'Dismiss' }).click();
|
||||
});
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 19 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 20 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,63 +63,6 @@ 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
|
||||
}) => {
|
||||
@@ -127,6 +70,10 @@ test.describe('Overlay Plot', () => {
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/6338'
|
||||
});
|
||||
// Create an Overlay Plot with a default SWG
|
||||
const overlayPlot = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Overlay Plot'
|
||||
});
|
||||
|
||||
const swgA = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
@@ -192,6 +139,10 @@ test.describe('Overlay Plot', () => {
|
||||
test('The elements pool supports dragging series into multiple y-axis buckets', async ({
|
||||
page
|
||||
}) => {
|
||||
const overlayPlot = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Overlay Plot'
|
||||
});
|
||||
|
||||
const swgA = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
parent: overlayPlot.uuid
|
||||
@@ -273,34 +224,13 @@ test.describe('Overlay Plot', () => {
|
||||
expect(yAxis3Group.getByRole('listitem').nth(0).getByText(swgB.name)).toBeTruthy();
|
||||
});
|
||||
|
||||
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'
|
||||
});
|
||||
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'
|
||||
});
|
||||
|
||||
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
|
||||
@@ -313,12 +243,11 @@ test.describe('Overlay Plot', () => {
|
||||
|
||||
await page.getByRole('tab', { name: 'Elements' }).click();
|
||||
|
||||
const swgAElementsPoolItem = page.getByLabel(`Preview ${swgA.name}`);
|
||||
await expect(swgAElementsPoolItem).toBeVisible();
|
||||
await swgAElementsPoolItem.click({ button: 'right' });
|
||||
await page.getByRole('menuitem', { name: 'Remove' }).click();
|
||||
await page.getByRole('button', { name: 'OK', exact: true }).click();
|
||||
await expect(swgAElementsPoolItem).toBeHidden();
|
||||
await page.locator(`#inspector-elements-tree >> text=${swgA.name}`).click();
|
||||
|
||||
const plotPixels = await getCanvasPixels(page, '.js-overlay canvas');
|
||||
const plotPixelSize = plotPixels.length;
|
||||
expect(plotPixelSize).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -331,9 +260,9 @@ async function assertLimitLinesExistAndAreVisible(page) {
|
||||
await waitForPlotsToRender(page);
|
||||
// Wait for limit lines to be created
|
||||
await page.waitForSelector('.js-limit-area', { state: 'attached' });
|
||||
// There should be 10 limit lines created by default
|
||||
await expect(page.locator('.c-plot-limit-line')).toHaveCount(10);
|
||||
const limitLineCount = await page.locator('.c-plot-limit-line').count();
|
||||
// There should be 10 limit lines created by default
|
||||
expect(await page.locator('.c-plot-limit-line').count()).toBe(10);
|
||||
for (let i = 0; i < limitLineCount; i++) {
|
||||
await expect(page.locator('.c-plot-limit-line').nth(i)).toBeVisible();
|
||||
}
|
||||
|
||||
@@ -52,11 +52,7 @@ test.describe('Plot Rendering', () => {
|
||||
expect(createMineFolderRequests.length).toEqual(0);
|
||||
});
|
||||
|
||||
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'
|
||||
});
|
||||
test('Plot is rendered when infinity values exist', async ({ page }) => {
|
||||
// Edit Plot
|
||||
await editSineWaveToUseInfinityOption(page, sineWaveGeneratorObject);
|
||||
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import { createDomainObjectWithDefaults } from '../../../../appActions.js';
|
||||
import { expect, test } from '../../../../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,56 +257,6 @@ 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);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* 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 = '#acacac';
|
||||
const defaultTextColor = '#acacac'; // default text color
|
||||
const inheritedColor = '#acacac'; // inherited from the body style
|
||||
const pukeGreen = '#6aa84f'; //Ugliest green known to man 🤮
|
||||
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 NO_STYLE_RGBA = 'rgba(0, 0, 0, 0)'; //default background color value
|
||||
|
||||
test.describe('Flexible Layout styling', () => {
|
||||
@@ -397,8 +397,8 @@ test.describe('Flexible Layout styling', () => {
|
||||
page.getByLabel('StackedPlot1 Frame').getByLabel('Stacked Plot Style Target')
|
||||
);
|
||||
// Save Flexible Layout
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
// Reload page and verify that styles persist
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
@@ -411,39 +411,4 @@ test.describe('Flexible Layout styling', () => {
|
||||
page.getByLabel('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 = '#acacac'; // default text color
|
||||
const defaultTextColor = '#aaaaaa'; // default text color
|
||||
const NO_STYLE_RGBA = 'rgba(0, 0, 0, 0)'; //default background color value
|
||||
const DEFAULT_PLOT_VIEW_BORDER_COLOR = '#acacac';
|
||||
const DEFAULT_PLOT_VIEW_BORDER_COLOR = '#AAAAAA';
|
||||
const setFontSize = '72px';
|
||||
const setFontWeight = '700'; //bold for monospace bold
|
||||
const setFontFamily = '"Andale Mono", sans-serif';
|
||||
|
||||
@@ -24,18 +24,13 @@ import { createDomainObjectWithDefaults } from '../../../../appActions.js';
|
||||
import { expect, test } from '../../../../pluginFixtures.js';
|
||||
|
||||
test.describe('Tabs View', () => {
|
||||
let tabsView;
|
||||
let table;
|
||||
let notebook;
|
||||
let sineWaveGenerator;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
test('Renders tabbed elements', async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
tabsView = await createDomainObjectWithDefaults(page, {
|
||||
const tabsView = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Tabs View'
|
||||
});
|
||||
table = await createDomainObjectWithDefaults(page, {
|
||||
const table = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Telemetry Table',
|
||||
parent: tabsView.uuid
|
||||
});
|
||||
@@ -43,38 +38,36 @@ test.describe('Tabs View', () => {
|
||||
type: 'Event Message Generator',
|
||||
parent: table.uuid
|
||||
});
|
||||
notebook = await createDomainObjectWithDefaults(page, {
|
||||
const notebook = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Notebook',
|
||||
parent: tabsView.uuid
|
||||
});
|
||||
sineWaveGenerator = await createDomainObjectWithDefaults(page, {
|
||||
const sineWaveGenerator = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
parent: tabsView.uuid
|
||||
});
|
||||
});
|
||||
|
||||
test('Renders tabbed elements', async ({ page }) => {
|
||||
await page.goto(tabsView.url);
|
||||
page.goto(tabsView.url);
|
||||
|
||||
// select first tab
|
||||
await page.getByLabel(`${table.name} tab`, { exact: true }).click();
|
||||
await page.getByLabel(`${table.name} tab`).click();
|
||||
// ensure table header visible
|
||||
await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible();
|
||||
|
||||
// no canvas (i.e., sine wave generator) in the document should be visible
|
||||
await expect(page.locator('canvas[id=webglContext]')).toBeHidden();
|
||||
await expect(page.locator('canvas')).toBeHidden();
|
||||
|
||||
// select second tab
|
||||
await page.getByLabel(`${notebook.name} tab`, { exact: true }).click();
|
||||
await page.getByLabel(`${notebook.name} tab`).click();
|
||||
|
||||
// ensure notebook visible
|
||||
await expect(page.locator('.c-notebook__drag-area')).toBeVisible();
|
||||
|
||||
// no canvas (i.e., sine wave generator) in the document should be visible
|
||||
await expect(page.locator('canvas[id=webglContext]')).toBeHidden();
|
||||
await expect(page.locator('canvas')).toBeHidden();
|
||||
|
||||
// select third tab
|
||||
await page.getByLabel(`${sineWaveGenerator.name} tab`, { exact: true }).click();
|
||||
await page.getByLabel(`${sineWaveGenerator.name} tab`).click();
|
||||
|
||||
// expect sine wave generator visible
|
||||
await expect(page.locator('.c-plot')).toBeVisible();
|
||||
@@ -85,37 +78,11 @@ 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`, { exact: true }).click();
|
||||
await page.getByLabel(`${table.name} tab`).click();
|
||||
// ensure table header visible
|
||||
await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible();
|
||||
|
||||
// no canvas (i.e., sine wave generator) in the document should be visible
|
||||
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();
|
||||
await expect(page.locator('canvas')).toBeHidden();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
/*
|
||||
* This test suite is dedicated to 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,9 +64,10 @@ test.describe('Telemetry Table', () => {
|
||||
|
||||
// Get the most recent telemetry date
|
||||
const latestTelemetryDate = await page
|
||||
.getByLabel('table content')
|
||||
.getByLabel('utc table cell')
|
||||
.locator('table.c-telemetry-table__body > tbody > tr')
|
||||
.last()
|
||||
.locator('td')
|
||||
.nth(1)
|
||||
.getAttribute('title');
|
||||
|
||||
// Verify that it is <= our new end bound
|
||||
@@ -90,7 +91,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').getByText(/Roger/).all();
|
||||
let cells = await page.getByRole('cell', { name: /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
|
||||
@@ -102,10 +103,7 @@ 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')
|
||||
.getByText(/Dodger/)
|
||||
.all();
|
||||
cells = await page.getByRole('cell', { name: /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
|
||||
@@ -137,7 +135,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').getByText(/Roger/).all();
|
||||
let cells = await page.getByRole('cell', { name: /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
|
||||
@@ -149,10 +147,7 @@ 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')
|
||||
.getByText(/Dodger/)
|
||||
.all();
|
||||
cells = await page.getByRole('cell', { name: /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
|
||||
|
||||
@@ -298,7 +298,7 @@ test.describe('Recent Objects', () => {
|
||||
// Assert that the list is empty
|
||||
expect(await recentObjectsList.locator('.c-recentobjects-listitem').count()).toBe(0);
|
||||
});
|
||||
test('Verify functionality of "clear" and "collapse pane" buttons', async ({ page }) => {
|
||||
test('Ensure clear recent objects button is active or inactive', async ({ page }) => {
|
||||
// Assert that the list initially contains 3 objects (clock, folder, my items)
|
||||
expect(await recentObjectsList.locator('.c-recentobjects-listitem').count()).toBe(3);
|
||||
|
||||
@@ -331,24 +331,6 @@ test.describe('Recent Objects', () => {
|
||||
expect(await page.getByRole('button', { name: 'Clear Recently Viewed' }).isEnabled()).toBe(
|
||||
true
|
||||
);
|
||||
|
||||
// Assert initial state of pane and collapse the Recent Objects panel
|
||||
await expect(page.getByLabel('Expand Recently Viewed Pane')).toBeHidden();
|
||||
await expect(page.getByLabel('Collapse Recently Viewed Pane')).toBeVisible();
|
||||
await page.getByLabel('Collapse Recently Viewed Pane').click();
|
||||
|
||||
// Assert that the "Expand Recently Viewed Pane" button is visible
|
||||
// and that the "Collapse Recently Viewed Pane" button is hidden
|
||||
await expect(page.getByLabel('Expand Recently Viewed Pane')).toBeVisible();
|
||||
await expect(page.getByLabel('Collapse Recently Viewed Pane')).toBeHidden();
|
||||
|
||||
// Expand the Recent Objects panel by clicking on the "Expand Recently Viewed Pane" button
|
||||
await page.getByLabel('Expand Recently Viewed Pane').click();
|
||||
|
||||
// Assert that the "Expand Recently Viewed Pane" button is hidden
|
||||
// and that the "Collapse Recently Viewed Pane" button is visible
|
||||
await expect(page.getByLabel('Expand Recently Viewed Pane')).toBeHidden();
|
||||
await expect(page.getByLabel('Collapse Recently Viewed Pane')).toBeVisible();
|
||||
});
|
||||
|
||||
function assertInitialRecentObjectsListState() {
|
||||
|
||||
@@ -99,7 +99,7 @@ test.describe('Grand Search', () => {
|
||||
page.waitForNavigation(),
|
||||
page.getByLabel('OpenMCT Search').getByText('Clock A').click()
|
||||
]);
|
||||
await expect(page.getByRole('status', { name: 'Clock', exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('status', { name: 'Clock' })).toBeVisible();
|
||||
|
||||
await grandSearchInput.fill('Disp');
|
||||
await expect(page.getByLabel('Object Search Result').first()).toContainText(
|
||||
|
||||
@@ -48,7 +48,7 @@ test('Verify that the create button appears and that the Folder Domain Object is
|
||||
await expect(page.locator(':nth-match(:text("Folder"), 2)')).toBeEnabled();
|
||||
});
|
||||
|
||||
test('Verify that My Items Tree appears', async ({ page, openmctConfig }) => {
|
||||
test('Verify that My Items Tree appears @ipad', async ({ page, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
//Go to baseURL
|
||||
await page.goto('./');
|
||||
|
||||
@@ -359,11 +359,7 @@ test.describe('Verify tooltips', () => {
|
||||
expect(tooltipText).toBe(sineWaveObject3.path);
|
||||
});
|
||||
|
||||
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'
|
||||
});
|
||||
test('display tooltip path for telemetry table names', async ({ page }) => {
|
||||
// set endBound to 10 seconds after start bound
|
||||
const url = await page.url();
|
||||
const parsedUrl = new URL(url.replace('#', '!'));
|
||||
|
||||
@@ -34,13 +34,13 @@ test.describe('User Roles', () => {
|
||||
// we have multiple available roles, so it should prompt the user
|
||||
await expect(page.getByText('Select Role')).toBeVisible();
|
||||
await page.getByRole('combobox').selectOption('driver');
|
||||
await page.getByRole('button', { name: 'Select', exact: true }).click();
|
||||
await page.getByRole('button', { name: 'Select' }).click();
|
||||
await expect(page.getByLabel('User Role')).toContainText('driver');
|
||||
|
||||
// attempt changing the role to another valid available role
|
||||
await page.getByRole('button', { name: 'Change Role' }).click();
|
||||
await page.getByRole('combobox').selectOption('flight');
|
||||
await page.getByRole('button', { name: 'Select', exact: true }).click();
|
||||
await page.getByRole('button', { name: 'Select' }).click();
|
||||
await expect(page.getByLabel('User Role')).toContainText('flight');
|
||||
|
||||
// reload page
|
||||
@@ -63,7 +63,7 @@ test.describe('User Roles', () => {
|
||||
|
||||
// select real role of "driver"
|
||||
await page.getByRole('combobox').selectOption('driver');
|
||||
await page.getByRole('button', { name: 'Select', exact: true }).click();
|
||||
await page.getByRole('button', { name: 'Select' }).click();
|
||||
await expect(page.getByLabel('User Role')).toContainText('driver');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
This test suite is dedicated to tests which can quickly verify that any openmct installation is
|
||||
operable and that any type of testing can proceed.
|
||||
|
||||
Ideally, smoke tests should make zero assumptions about how and where they are run. This makes them
|
||||
more resilient to change and therefor a better indicator of failure. Smoke tests will also run quickly
|
||||
as they cover a very "thin surface" of functionality.
|
||||
|
||||
When deciding between authoring new smoke tests or functional tests, ask yourself "would I feel
|
||||
comfortable running this test during a live mission?" Avoid creating or deleting Domain Objects.
|
||||
Make no assumptions about the order that elements appear in the DOM.
|
||||
*/
|
||||
|
||||
import { expect, test } from '../../pluginFixtures.js';
|
||||
|
||||
test('Verify that My Items Tree appears @mobile', async ({ page, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
//Go to baseURL
|
||||
await page.goto('./');
|
||||
|
||||
//My Items to be visible
|
||||
await expect(page.getByRole('treeitem', { name: `${myItemsFolderName}` })).toBeVisible();
|
||||
});
|
||||
|
||||
test('Verify that user can search @mobile', async ({ page }) => {
|
||||
//For now, this test is going to be hardcoded against './test-data/display_layout_with_child_layouts.json'
|
||||
await page.goto('./');
|
||||
|
||||
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();
|
||||
});
|
||||
@@ -24,7 +24,7 @@ import { createDomainObjectWithDefaults, waitForPlotsToRender } from '../../appA
|
||||
import { expect, test } from '../../pluginFixtures.js';
|
||||
|
||||
test.describe('Tabs View', () => {
|
||||
test('Renders tabbed elements only when visible', async ({ page }) => {
|
||||
test('Renders tabbed elements nicely', 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`, { exact: true }).click();
|
||||
await page.getByLabel(`${table.name} tab`).click();
|
||||
// ensure table header visible
|
||||
await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible();
|
||||
|
||||
// select second tab
|
||||
await page.getByLabel(`${notebook.name} tab`, { exact: true }).click();
|
||||
await page.getByLabel(`${notebook.name} tab`).click();
|
||||
|
||||
// expect notebook visible
|
||||
await expect(page.locator('.c-notebook__drag-area')).toBeVisible();
|
||||
|
||||
// select third tab
|
||||
await page.getByLabel(`${sineWaveGenerator.name} tab`, { exact: true }).click();
|
||||
await page.getByLabel(`${sineWaveGenerator.name} tab`).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`, { exact: true }).click();
|
||||
await page.getByLabel(`${notebook.name} tab`).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`, { exact: true }).click();
|
||||
await page.getByLabel(`${sineWaveGenerator.name} tab`).click();
|
||||
|
||||
// ensure sine wave generator visible
|
||||
await waitForPlotsToRender(page);
|
||||
@@ -20,16 +20,14 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import { test } from '../../avpFixtures.js';
|
||||
import { scanForA11yViolations, test } from '../../avpFixtures.js';
|
||||
import { VISUAL_URL } from '../../constants.js';
|
||||
|
||||
test.describe('a11y - Default', () => {
|
||||
test.describe('a11y - Default @a11y', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
|
||||
});
|
||||
test('main view', async ({ page }, testInfo) => {
|
||||
await page.goto('./');
|
||||
//Skipping for https://github.com/nasa/openmct/issues/7421
|
||||
//await scanForA11yViolations(page, testInfo.title);
|
||||
test('main view @a11y', async ({ page }, testInfo) => {
|
||||
await scanForA11yViolations(page, testInfo.title);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,7 +26,7 @@ Tests the branding associated with the default deployment. At least the about mo
|
||||
|
||||
import percySnapshot from '@percy/playwright';
|
||||
|
||||
import { expect, test } from '../../../avpFixtures.js';
|
||||
import { scanForA11yViolations, test } from '../../../avpFixtures.js';
|
||||
import { VISUAL_URL } from '../../../constants.js';
|
||||
|
||||
//Declare the scope of the visual test
|
||||
@@ -36,22 +36,6 @@ test.describe('Visual - Header @a11y', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
//Go to baseURL and Hide Tree
|
||||
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
|
||||
// Wait for status bar to load
|
||||
await expect(
|
||||
page.getByRole('status', {
|
||||
name: 'Clock Indicator'
|
||||
})
|
||||
).toBeInViewport();
|
||||
await expect(
|
||||
page.getByRole('status', {
|
||||
name: 'Global Clear Indicator'
|
||||
})
|
||||
).toBeInViewport();
|
||||
await expect(
|
||||
page.getByRole('status', {
|
||||
name: 'Snapshot Indicator'
|
||||
})
|
||||
).toBeInViewport();
|
||||
});
|
||||
|
||||
test('header sizing', async ({ page, theme }) => {
|
||||
@@ -66,19 +50,7 @@ test.describe('Visual - Header @a11y', () => {
|
||||
scope: header
|
||||
});
|
||||
});
|
||||
|
||||
test('show snapshot button', async ({ page, theme }) => {
|
||||
await page.getByLabel('Take a Notebook Snapshot').click();
|
||||
|
||||
await page.getByRole('menuitem', { name: 'Save to Notebook Snapshots' }).click();
|
||||
|
||||
await percySnapshot(page, `Notebook Snapshot Show button (theme: '${theme}')`, {
|
||||
scope: header
|
||||
});
|
||||
await expect(await page.getByLabel('Show Snapshots')).toBeVisible();
|
||||
});
|
||||
});
|
||||
// Skipping for https://github.com/nasa/openmct/issues/7421
|
||||
// test.afterEach(async ({ page }, testInfo) => {
|
||||
// await scanForA11yViolations(page, testInfo.title);
|
||||
// });
|
||||
test.afterEach(async ({ page }, testInfo) => {
|
||||
await scanForA11yViolations(page, testInfo.title);
|
||||
});
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
import percySnapshot from '@percy/playwright';
|
||||
|
||||
import { test } from '../../../avpFixtures.js';
|
||||
import { scanForA11yViolations, test } from '../../../avpFixtures.js';
|
||||
import { MISSION_TIME, VISUAL_URL } from '../../../constants.js';
|
||||
|
||||
//Declare the scope of the visual test
|
||||
@@ -55,7 +55,6 @@ test.describe('Visual - Inspector @ally', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
// Skipping for https://github.com/nasa/openmct/issues/7421
|
||||
// test.afterEach(async ({ page }, testInfo) => {
|
||||
// await scanForA11yViolations(page, testInfo.title);
|
||||
// });
|
||||
test.afterEach(async ({ page }, testInfo) => {
|
||||
await scanForA11yViolations(page, testInfo.title);
|
||||
});
|
||||
|
||||
@@ -23,7 +23,7 @@ import percySnapshot from '@percy/playwright';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
import * as utils from '../../helper/faultUtils.js';
|
||||
import { expect, test } from '../../pluginFixtures.js';
|
||||
import { test } from '../../pluginFixtures.js';
|
||||
|
||||
test.describe('Fault Management Visual Tests', () => {
|
||||
test('icon test', async ({ page, theme }) => {
|
||||
@@ -32,23 +32,6 @@ test.describe('Fault Management Visual Tests', () => {
|
||||
});
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Wait for status bar to load
|
||||
await expect(
|
||||
page.getByRole('status', {
|
||||
name: 'Clock Indicator'
|
||||
})
|
||||
).toBeInViewport();
|
||||
await expect(
|
||||
page.getByRole('status', {
|
||||
name: 'Global Clear Indicator'
|
||||
})
|
||||
).toBeInViewport();
|
||||
await expect(
|
||||
page.getByRole('status', {
|
||||
name: 'Snapshot Indicator'
|
||||
})
|
||||
).toBeInViewport();
|
||||
|
||||
await percySnapshot(page, `Fault Management icon appears in tree (theme: '${theme}')`);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import 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})`);
|
||||
});
|
||||
});
|
||||
@@ -1,57 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import 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();
|
||||
// Description should be empty https://github.com/nasa/openmct/issues/6978
|
||||
await expect(page.locator('c-message__action-text')).toBeHidden();
|
||||
// set role
|
||||
await page.getByRole('button', { name: 'Select', exact: true }).click();
|
||||
// dismiss role confirmation popup
|
||||
await page.getByRole('button', { name: 'Dismiss' }).click();
|
||||
});
|
||||
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 { test } from '../../avpFixtures.js';
|
||||
import { scanForA11yViolations, test } from '../../avpFixtures.js';
|
||||
import { VISUAL_URL } from '../../constants.js';
|
||||
import { enterTextEntry, startAndAddRestrictedNotebookObject } from '../../helper/notebookUtils.js';
|
||||
|
||||
test.describe('Visual - Restricted Notebook @a11y', () => {
|
||||
test.describe('Visual - Restricted Notebook', () => {
|
||||
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 @a11y', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Visual - Notebook @a11y', () => {
|
||||
test.describe('Visual - Notebook', () => {
|
||||
let notebook;
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
|
||||
@@ -125,8 +125,7 @@ test.describe('Visual - Notebook @a11y', () => {
|
||||
// Take a snapshot
|
||||
await percySnapshot(page, `Notebook Selected Entry Text Area Active (theme: '${theme}')`);
|
||||
});
|
||||
// Skipping for https://github.com/nasa/openmct/issues/7421
|
||||
// test.afterEach(async ({ page }, testInfo) => {
|
||||
// await scanForA11yViolations(page, testInfo.title);
|
||||
// });
|
||||
test.afterEach(async ({ page }, testInfo) => {
|
||||
await scanForA11yViolations(page, testInfo.title);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,7 +24,7 @@ import percySnapshot from '@percy/playwright';
|
||||
import fs from 'fs';
|
||||
|
||||
import { createDomainObjectWithDefaults, createPlanFromJSON } from '../../appActions.js';
|
||||
import { test } from '../../avpFixtures.js';
|
||||
import { scanForA11yViolations, test } from '../../avpFixtures.js';
|
||||
import { VISUAL_URL } from '../../constants.js';
|
||||
import { setBoundsToSpanAllActivities, setDraftStatusForPlan } from '../../helper/planningUtils.js';
|
||||
|
||||
@@ -34,7 +34,7 @@ const examplePlanSmall = JSON.parse(
|
||||
|
||||
const snapshotScope = '.l-shell__pane-main .l-pane__contents';
|
||||
|
||||
test.describe('Visual - Planning', () => {
|
||||
test.describe('Visual - Planning @a11y', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(VISUAL_URL, { waitUntil: 'domcontentloaded' });
|
||||
});
|
||||
@@ -75,25 +75,7 @@ test.describe('Visual - Planning', () => {
|
||||
parent: ganttChart.uuid
|
||||
});
|
||||
await setBoundsToSpanAllActivities(page, examplePlanSmall, ganttChart.url);
|
||||
await percySnapshot(page, `Gantt Chart View (theme: ${theme}) - Clipped Activity Names`, {
|
||||
scope: snapshotScope
|
||||
});
|
||||
|
||||
// Expand the inspect pane and uncheck the 'Clip Activity Names' option
|
||||
await page.getByRole('button', { name: 'Expand Inspect Pane' }).click();
|
||||
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`, {
|
||||
await percySnapshot(page, `Gantt Chart View (theme: ${theme})`, {
|
||||
scope: snapshotScope
|
||||
});
|
||||
});
|
||||
@@ -116,31 +98,8 @@ test.describe('Visual - Planning', () => {
|
||||
await percySnapshot(page, `Gantt Chart View w/ draft status (theme: ${theme})`, {
|
||||
scope: snapshotScope
|
||||
});
|
||||
|
||||
// Expand the inspect pane and uncheck the 'Clip Activity Names' option
|
||||
await page.getByRole('button', { name: 'Expand Inspect Pane' }).click();
|
||||
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`,
|
||||
{
|
||||
scope: snapshotScope
|
||||
}
|
||||
);
|
||||
});
|
||||
// Skipping for https://github.com/nasa/openmct/issues/7421
|
||||
// test.afterEach(async ({ page }, testInfo) => {
|
||||
// await scanForA11yViolations(page, testInfo.title);
|
||||
// });
|
||||
test.afterEach(async ({ page }, testInfo) => {
|
||||
await scanForA11yViolations(page, testInfo.title);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -60,22 +60,10 @@ 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 } = {
|
||||
@@ -85,11 +73,6 @@ 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;
|
||||
@@ -127,11 +110,6 @@ export default class ExampleUserProvider extends EventEmitter {
|
||||
canSetPollQuestion() {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
canSetMissionStatus() {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
hasRole(roleId) {
|
||||
if (!this.loggedIn) {
|
||||
Promise.resolve(undefined);
|
||||
@@ -144,28 +122,6 @@ 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);
|
||||
}
|
||||
|
||||
@@ -92,8 +92,6 @@ 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,37 +130,48 @@
|
||||
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 size = request.size;
|
||||
var duration = end - start;
|
||||
var infinityValues = request.infinityValues;
|
||||
var exceedFloat32 = request.exceedFloat32;
|
||||
|
||||
var step = 1000 / dataRateInHz;
|
||||
var maxPoints = Math.floor(duration / step);
|
||||
var nextStep = start - (start % step) + step;
|
||||
|
||||
var data = [];
|
||||
|
||||
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);
|
||||
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 (loadDelay === 0) {
|
||||
@@ -170,35 +181,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
@@ -108,6 +108,7 @@
|
||||
|
||||
openmct.install(openmct.plugins.Espresso());
|
||||
openmct.install(openmct.plugins.MyItems());
|
||||
openmct.install(openmct.plugins.ActivityStates());
|
||||
openmct.install(
|
||||
openmct.plugins.PlanLayout({
|
||||
creatable: true
|
||||
|
||||
@@ -47,7 +47,7 @@ if (document.currentScript) {
|
||||
* @property {*} inspectorViews
|
||||
* @property {*} propertyEditors
|
||||
* @property {*} toolbars
|
||||
* @property {import('./src/api/types/TypeRegistry').default} types
|
||||
* @property {*} types
|
||||
* @property {import('./src/api/objects/ObjectAPI').default} objects
|
||||
* @property {import('./src/api/telemetry/TelemetryAPI').default} telemetry
|
||||
* @property {import('./src/api/indicators/IndicatorAPI').default} indicators
|
||||
@@ -67,7 +67,6 @@ if (document.currentScript) {
|
||||
* @property {import('./src/api/annotation/AnnotationAPI').default} annotation
|
||||
* @property {{(plugin: OpenMCTPlugin) => void}} install
|
||||
* @property {{() => string}} getAssetPath
|
||||
* @property {{(assetPath: string) => void}} setAssetPath
|
||||
* @property {{(domElement: HTMLElement, isHeadlessMode: boolean) => void}} start
|
||||
* @property {{() => void}} startHeadless
|
||||
* @property {{() => void}} destroy
|
||||
|
||||
21
package.json
21
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openmct",
|
||||
"version": "4.0.0-next",
|
||||
"version": "3.3.0-next",
|
||||
"description": "The Open MCT core platform",
|
||||
"type": "module",
|
||||
"main": "dist/openmct.js",
|
||||
@@ -28,12 +28,12 @@
|
||||
"d3-axis": "3.0.0",
|
||||
"d3-scale": "4.0.2",
|
||||
"d3-selection": "3.0.0",
|
||||
"eslint": "8.56.0",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"eslint": "8.54.0",
|
||||
"eslint-config-prettier": "9.0.0",
|
||||
"eslint-plugin-compat": "4.2.0",
|
||||
"eslint-plugin-no-unsanitized": "4.0.2",
|
||||
"eslint-plugin-playwright": "0.12.0",
|
||||
"eslint-plugin-prettier": "5.1.3",
|
||||
"eslint-plugin-prettier": "4.2.1",
|
||||
"eslint-plugin-simple-import-sort": "10.0.0",
|
||||
"eslint-plugin-unicorn": "49.0.0",
|
||||
"eslint-plugin-vue": "9.18.1",
|
||||
@@ -57,9 +57,9 @@
|
||||
"karma-webpack": "5.0.0",
|
||||
"location-bar": "3.0.1",
|
||||
"lodash": "4.17.21",
|
||||
"marked": "11.2.0",
|
||||
"marked": "11.1.0",
|
||||
"mini-css-extract-plugin": "2.7.6",
|
||||
"moment": "2.30.1",
|
||||
"moment": "2.29.4",
|
||||
"moment-duration-format": "2.3.2",
|
||||
"moment-timezone": "0.5.41",
|
||||
"npm-run-all2": "6.1.1",
|
||||
@@ -67,20 +67,19 @@
|
||||
"painterro": "1.2.87",
|
||||
"plotly.js-basic-dist-min": "2.20.0",
|
||||
"plotly.js-gl2d-dist-min": "2.20.0",
|
||||
"prettier": "3.2.5",
|
||||
"prettier-eslint": "16.3.0",
|
||||
"prettier": "2.8.7",
|
||||
"printj": "1.3.1",
|
||||
"resolve-url-loader": "5.0.0",
|
||||
"sanitize-html": "2.11.0",
|
||||
"sass": "1.68.0",
|
||||
"sass-loader": "14.0.0",
|
||||
"sass-loader": "13.3.2",
|
||||
"sinon": "17.0.0",
|
||||
"style-loader": "3.3.3",
|
||||
"terser-webpack-plugin": "5.3.9",
|
||||
"tiny-emitter": "2.1.0",
|
||||
"typescript": "5.3.3",
|
||||
"uuid": "9.0.1",
|
||||
"vue": "3.4.19",
|
||||
"vue": "3.3.8",
|
||||
"vue-eslint-parser": "9.3.2",
|
||||
"vue-loader": "16.8.3",
|
||||
"webpack": "5.89.0",
|
||||
@@ -107,13 +106,11 @@
|
||||
"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,7 +251,6 @@ 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 { makeKeyString, parseKeyString } from '../objects/object-utils.js';
|
||||
import objectUtils from '../objects/object-utils.js';
|
||||
|
||||
/**
|
||||
* @typedef {import('../objects/ObjectAPI').DomainObject} DomainObject
|
||||
@@ -223,18 +223,18 @@ export default class CompositionProvider {
|
||||
* @param {DomainObject} oldDomainObject
|
||||
*/
|
||||
#onMutation(newDomainObject, oldDomainObject) {
|
||||
const id = makeKeyString(oldDomainObject.identifier);
|
||||
const id = objectUtils.makeKeyString(oldDomainObject.identifier);
|
||||
const listeners = this.#listeningTo[id];
|
||||
|
||||
if (!listeners) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldComposition = oldDomainObject.composition.map(makeKeyString);
|
||||
const newComposition = newDomainObject.composition.map(makeKeyString);
|
||||
const oldComposition = oldDomainObject.composition.map(objectUtils.makeKeyString);
|
||||
const newComposition = newDomainObject.composition.map(objectUtils.makeKeyString);
|
||||
|
||||
const added = _.difference(newComposition, oldComposition).map(parseKeyString);
|
||||
const removed = _.difference(oldComposition, newComposition).map(parseKeyString);
|
||||
const added = _.difference(newComposition, oldComposition).map(objectUtils.parseKeyString);
|
||||
const removed = _.difference(oldComposition, newComposition).map(objectUtils.parseKeyString);
|
||||
|
||||
function notify(value) {
|
||||
return function (listener) {
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
*****************************************************************************/
|
||||
import { toRaw } from 'vue';
|
||||
|
||||
import { makeKeyString } from '../objects/object-utils.js';
|
||||
import objectUtils from '../objects/object-utils.js';
|
||||
import CompositionProvider from './CompositionProvider.js';
|
||||
|
||||
/**
|
||||
@@ -91,7 +91,7 @@ export default class DefaultCompositionProvider extends CompositionProvider {
|
||||
this.establishTopicListener();
|
||||
|
||||
/** @type {string} */
|
||||
const keyString = makeKeyString(domainObject.identifier);
|
||||
const keyString = objectUtils.makeKeyString(domainObject.identifier);
|
||||
let objectListeners = this.listeningTo[keyString];
|
||||
|
||||
if (!objectListeners) {
|
||||
@@ -120,7 +120,7 @@ export default class DefaultCompositionProvider extends CompositionProvider {
|
||||
*/
|
||||
off(domainObject, event, callback, context) {
|
||||
/** @type {string} */
|
||||
const keyString = makeKeyString(domainObject.identifier);
|
||||
const keyString = objectUtils.makeKeyString(domainObject.identifier);
|
||||
const objectListeners = this.listeningTo[keyString];
|
||||
|
||||
const index = objectListeners[event].findIndex((l) => {
|
||||
@@ -228,7 +228,7 @@ export default class DefaultCompositionProvider extends CompositionProvider {
|
||||
this.publicAPI.objects.mutate(domainObject, 'composition', newComposition);
|
||||
|
||||
/** @type {string} */
|
||||
let id = makeKeyString(domainObject.identifier);
|
||||
let id = objectUtils.makeKeyString(domainObject.identifier);
|
||||
const listeners = this.listeningTo[id];
|
||||
|
||||
if (!listeners) {
|
||||
|
||||
@@ -22,12 +22,9 @@
|
||||
|
||||
import EventEmitter from 'EventEmitter';
|
||||
|
||||
import vueWrapHtmlElement from '../../utils/vueWrapHtmlElement.js';
|
||||
import SimpleIndicator from './SimpleIndicator.js';
|
||||
|
||||
class IndicatorAPI extends EventEmitter {
|
||||
/** @type {import('../../../openmct.js').OpenMCT} */
|
||||
openmct;
|
||||
constructor(openmct) {
|
||||
super();
|
||||
|
||||
@@ -45,18 +42,6 @@ class IndicatorAPI extends EventEmitter {
|
||||
return new SimpleIndicator(this.openmct);
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {import('vue').Component} VueComponent
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} Indicator
|
||||
* @property {HTMLElement} [element]
|
||||
* @property {VueComponent|Promise<VueComponent>} [vueComponent]
|
||||
* @property {string} key
|
||||
* @property {number} priority
|
||||
*/
|
||||
|
||||
/**
|
||||
* Accepts an indicator object, which is a simple object
|
||||
* with a two attributes: 'element' which has an HTMLElement
|
||||
@@ -77,20 +62,11 @@ class IndicatorAPI extends EventEmitter {
|
||||
* myIndicator.text("Hello World!");
|
||||
* myIndicator.iconClass("icon-info");
|
||||
*
|
||||
* If you would like to use a Vue component, you can pass it in
|
||||
* directly as the 'vueComponent' attribute of the indicator object.
|
||||
* This accepts a Vue component or a promise that resolves to a Vue component (for asynchronous
|
||||
* rendering).
|
||||
*
|
||||
* @param {Indicator} indicator
|
||||
*/
|
||||
add(indicator) {
|
||||
if (!indicator.priority) {
|
||||
indicator.priority = this.openmct.priority.DEFAULT;
|
||||
}
|
||||
if (!indicator.vueComponent) {
|
||||
indicator.vueComponent = vueWrapHtmlElement(indicator.element);
|
||||
}
|
||||
|
||||
this.indicatorObjects.push(indicator);
|
||||
|
||||
|
||||
@@ -19,8 +19,6 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
import { createOpenMct, resetApplicationState } from '../../utils/testing.js';
|
||||
import SimpleIndicator from './SimpleIndicator.js';
|
||||
|
||||
@@ -35,7 +33,7 @@ describe('The Indicator API', () => {
|
||||
return resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
function generateHTMLIndicator(className, label, priority) {
|
||||
function generateIndicator(className, label, priority) {
|
||||
const element = document.createElement('div');
|
||||
element.classList.add(className);
|
||||
const textNode = document.createTextNode(label);
|
||||
@@ -48,25 +46,8 @@ describe('The Indicator API', () => {
|
||||
return testIndicator;
|
||||
}
|
||||
|
||||
function generateVueIndicator(priority) {
|
||||
return {
|
||||
vueComponent: defineComponent({
|
||||
template: '<div class="test-indicator">This is a test indicator</div>'
|
||||
}),
|
||||
priority
|
||||
};
|
||||
}
|
||||
|
||||
it('can register an HTML indicator', () => {
|
||||
const testIndicator = generateHTMLIndicator('test-indicator', 'This is a test indicator', 2);
|
||||
openmct.indicators.add(testIndicator);
|
||||
expect(openmct.indicators.indicatorObjects).toBeDefined();
|
||||
// notifier indicator is installed by default
|
||||
expect(openmct.indicators.indicatorObjects.length).toBe(2);
|
||||
});
|
||||
|
||||
it('can register a Vue indicator', () => {
|
||||
const testIndicator = generateVueIndicator(2);
|
||||
it('can register an indicator', () => {
|
||||
const testIndicator = generateIndicator('test-indicator', 'This is a test indicator', 2);
|
||||
openmct.indicators.add(testIndicator);
|
||||
expect(openmct.indicators.indicatorObjects).toBeDefined();
|
||||
// notifier indicator is installed by default
|
||||
@@ -74,40 +55,37 @@ describe('The Indicator API', () => {
|
||||
});
|
||||
|
||||
it('can order indicators based on priority', () => {
|
||||
const testIndicator1 = generateHTMLIndicator(
|
||||
const testIndicator1 = generateIndicator(
|
||||
'test-indicator-1',
|
||||
'This is a test indicator',
|
||||
openmct.priority.LOW
|
||||
);
|
||||
openmct.indicators.add(testIndicator1);
|
||||
|
||||
const testIndicator2 = generateHTMLIndicator(
|
||||
const testIndicator2 = generateIndicator(
|
||||
'test-indicator-2',
|
||||
'This is another test indicator',
|
||||
openmct.priority.DEFAULT
|
||||
);
|
||||
openmct.indicators.add(testIndicator2);
|
||||
|
||||
const testIndicator3 = generateHTMLIndicator(
|
||||
const testIndicator3 = generateIndicator(
|
||||
'test-indicator-3',
|
||||
'This is yet another test indicator',
|
||||
openmct.priority.LOW
|
||||
);
|
||||
openmct.indicators.add(testIndicator3);
|
||||
|
||||
const testIndicator4 = generateHTMLIndicator(
|
||||
const testIndicator4 = generateIndicator(
|
||||
'test-indicator-4',
|
||||
'This is yet another test indicator',
|
||||
openmct.priority.HIGH
|
||||
);
|
||||
openmct.indicators.add(testIndicator4);
|
||||
|
||||
const testIndicator5 = generateVueIndicator(openmct.priority.DEFAULT);
|
||||
openmct.indicators.add(testIndicator5);
|
||||
|
||||
expect(openmct.indicators.indicatorObjects.length).toBe(6);
|
||||
expect(openmct.indicators.indicatorObjects.length).toBe(5);
|
||||
const indicatorObjectsByPriority = openmct.indicators.getIndicatorObjectsByPriority();
|
||||
expect(indicatorObjectsByPriority.length).toBe(6);
|
||||
expect(indicatorObjectsByPriority.length).toBe(5);
|
||||
expect(indicatorObjectsByPriority[2].priority).toBe(openmct.priority.DEFAULT);
|
||||
});
|
||||
|
||||
|
||||
@@ -28,8 +28,7 @@
|
||||
v-for="action in actionGroups"
|
||||
:key="action.name"
|
||||
role="menuitem"
|
||||
:aria-disabled="action.isDisabled"
|
||||
:class="action.cssClass"
|
||||
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
|
||||
:aria-label="action.name"
|
||||
:title="action.description"
|
||||
@click="action.onItemClicked"
|
||||
@@ -52,8 +51,7 @@
|
||||
v-for="action in options.actions"
|
||||
:key="action.name"
|
||||
role="menuitem"
|
||||
:aria-disabled="action.isDisabled"
|
||||
:class="action.cssClass"
|
||||
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
|
||||
:aria-label="action.name"
|
||||
:title="action.description"
|
||||
@click="action.onItemClicked"
|
||||
|
||||
@@ -37,8 +37,7 @@
|
||||
v-for="action in actionGroups"
|
||||
:key="action.name"
|
||||
role="menuitem"
|
||||
:aria-disabled="action.isDisabled"
|
||||
:class="action.cssClass"
|
||||
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
|
||||
:title="action.description"
|
||||
@click="action.onItemClicked"
|
||||
@mouseover="toggleItemDescription(action)"
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
import EventEmitter from 'EventEmitter';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { makeKeyString, refresh } from './object-utils.js';
|
||||
import utils from './object-utils.js';
|
||||
|
||||
const ANY_OBJECT_EVENT = 'mutation';
|
||||
|
||||
@@ -152,7 +152,7 @@ class MutableDomainObject {
|
||||
|
||||
mutable.$observe('$_synchronize_model', (updatedObject) => {
|
||||
let clone = JSON.parse(JSON.stringify(updatedObject));
|
||||
refresh(mutable, clone);
|
||||
utils.refresh(mutable, clone);
|
||||
});
|
||||
|
||||
return mutable;
|
||||
@@ -168,7 +168,7 @@ class MutableDomainObject {
|
||||
}
|
||||
|
||||
function qualifiedEventName(object, eventName) {
|
||||
let keystring = makeKeyString(object.identifier);
|
||||
let keystring = utils.makeKeyString(object.identifier);
|
||||
|
||||
return [keystring, eventName].join(':');
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
*****************************************************************************/
|
||||
|
||||
import EventEmitter from 'EventEmitter';
|
||||
import { identifierEquals, makeKeyString, parseKeyString, refresh } from 'objectUtils';
|
||||
import utils from 'objectUtils';
|
||||
|
||||
import ConflictError from './ConflictError.js';
|
||||
import InMemorySearchProvider from './InMemorySearchProvider.js';
|
||||
@@ -82,19 +82,8 @@ import Transaction from './Transaction.js';
|
||||
* @memberof module:openmct
|
||||
*/
|
||||
export default class ObjectAPI {
|
||||
#makeKeyString;
|
||||
#parseKeyString;
|
||||
#identifierEquals;
|
||||
#refresh;
|
||||
#openmct;
|
||||
|
||||
constructor(typeRegistry, openmct) {
|
||||
this.#makeKeyString = makeKeyString;
|
||||
this.#parseKeyString = parseKeyString;
|
||||
this.#identifierEquals = identifierEquals;
|
||||
this.#refresh = refresh;
|
||||
this.#openmct = openmct;
|
||||
|
||||
this.openmct = openmct;
|
||||
this.typeRegistry = typeRegistry;
|
||||
this.SEARCH_TYPES = Object.freeze({
|
||||
OBJECTS: 'OBJECTS',
|
||||
@@ -217,14 +206,14 @@ export default class ObjectAPI {
|
||||
* has been saved, or be rejected if it cannot be saved
|
||||
*/
|
||||
get(identifier, abortSignal, forceRemote = false) {
|
||||
let keystring = this.#makeKeyString(identifier);
|
||||
let keystring = this.makeKeyString(identifier);
|
||||
|
||||
if (!forceRemote) {
|
||||
if (this.cache[keystring] !== undefined) {
|
||||
return this.cache[keystring];
|
||||
}
|
||||
|
||||
identifier = parseKeyString(identifier);
|
||||
identifier = utils.parseKeyString(identifier);
|
||||
|
||||
if (this.isTransactionActive()) {
|
||||
let dirtyObject = this.transaction.getDirtyObject(identifier);
|
||||
@@ -238,7 +227,7 @@ export default class ObjectAPI {
|
||||
const provider = this.getProvider(identifier);
|
||||
|
||||
if (!provider) {
|
||||
throw new Error(`No Provider Matched for keyString "${this.#makeKeyString(identifier)}"`);
|
||||
throw new Error(`No Provider Matched for keyString "${this.makeKeyString(identifier)}"`);
|
||||
}
|
||||
|
||||
if (!provider.get) {
|
||||
@@ -336,7 +325,7 @@ export default class ObjectAPI {
|
||||
*/
|
||||
getMutable(identifier) {
|
||||
if (!this.supportsMutation(identifier)) {
|
||||
throw new Error(`Object "${this.#makeKeyString(identifier)}" does not support mutation.`);
|
||||
throw new Error(`Object "${this.makeKeyString(identifier)}" does not support mutation.`);
|
||||
}
|
||||
|
||||
return this.get(identifier).then((object) => {
|
||||
@@ -363,17 +352,14 @@ export default class ObjectAPI {
|
||||
}
|
||||
|
||||
isPersistable(idOrKeyString) {
|
||||
let identifier = parseKeyString(idOrKeyString);
|
||||
let identifier = utils.parseKeyString(idOrKeyString);
|
||||
let provider = this.getProvider(identifier);
|
||||
if (provider?.isReadOnly) {
|
||||
return !provider.isReadOnly();
|
||||
}
|
||||
|
||||
return provider !== undefined && provider.create !== undefined && provider.update !== undefined;
|
||||
}
|
||||
|
||||
isMissing(domainObject) {
|
||||
let identifier = makeKeyString(domainObject.identifier);
|
||||
let identifier = utils.makeKeyString(domainObject.identifier);
|
||||
let missingName = 'Missing: ' + identifier;
|
||||
|
||||
return domainObject.name === missingName;
|
||||
@@ -453,21 +439,21 @@ export default class ObjectAPI {
|
||||
if (error instanceof this.errors.Conflict) {
|
||||
// Synchronized objects will resolve their own conflicts
|
||||
if (this.SYNCHRONIZED_OBJECT_TYPES.includes(domainObject.type)) {
|
||||
this.#openmct.notifications.info(
|
||||
`Conflict detected while saving "${this.#makeKeyString(
|
||||
this.openmct.notifications.info(
|
||||
`Conflict detected while saving "${this.makeKeyString(
|
||||
domainObject.name
|
||||
)}", attempting to resolve`
|
||||
);
|
||||
} else {
|
||||
this.#openmct.notifications.error(
|
||||
`Conflict detected while saving ${this.#makeKeyString(domainObject.identifier)}`
|
||||
this.openmct.notifications.error(
|
||||
`Conflict detected while saving ${this.makeKeyString(domainObject.identifier)}`
|
||||
);
|
||||
|
||||
if (this.isTransactionActive()) {
|
||||
this.endTransaction();
|
||||
}
|
||||
|
||||
await this.#refresh(domainObject);
|
||||
await this.refresh(domainObject);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -476,7 +462,7 @@ export default class ObjectAPI {
|
||||
}
|
||||
|
||||
async #getCurrentUsername() {
|
||||
const user = await this.#openmct.user.getCurrentUser();
|
||||
const user = await this.openmct.user.getCurrentUser();
|
||||
let username;
|
||||
|
||||
if (user !== undefined) {
|
||||
@@ -565,7 +551,7 @@ export default class ObjectAPI {
|
||||
*/
|
||||
getRelativePath(objectPath) {
|
||||
return objectPath
|
||||
.map((p) => this.#makeKeyString(p.identifier))
|
||||
.map((p) => this.makeKeyString(p.identifier))
|
||||
.reverse()
|
||||
.join('/');
|
||||
}
|
||||
@@ -585,13 +571,13 @@ export default class ObjectAPI {
|
||||
}
|
||||
|
||||
let sourceTelemetry = null;
|
||||
if (telemetryIdentifier && this.#identifierEquals(identifier, telemetryIdentifier)) {
|
||||
if (telemetryIdentifier && utils.identifierEquals(identifier, telemetryIdentifier)) {
|
||||
sourceTelemetry = identifier;
|
||||
} else if (objectDetails.composition) {
|
||||
sourceTelemetry = objectDetails.composition[0];
|
||||
if (telemetryIdentifier) {
|
||||
sourceTelemetry = objectDetails.composition.find((telemetrySource) =>
|
||||
this.#identifierEquals(telemetrySource, telemetryIdentifier)
|
||||
utils.identifierEquals(telemetrySource, telemetryIdentifier)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -677,7 +663,7 @@ export default class ObjectAPI {
|
||||
mutableObject = MutableDomainObject.createMutable(domainObject, this.eventEmitter);
|
||||
|
||||
// Check if provider supports realtime updates
|
||||
let identifier = parseKeyString(mutableObject.identifier);
|
||||
let identifier = utils.parseKeyString(mutableObject.identifier);
|
||||
let provider = this.getProvider(identifier);
|
||||
|
||||
if (
|
||||
@@ -707,17 +693,15 @@ 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, forceRemote = false) {
|
||||
const refreshedObject = await this.get(domainObject.identifier, null, forceRemote);
|
||||
async refresh(domainObject) {
|
||||
const refreshedObject = await this.get(domainObject.identifier);
|
||||
|
||||
if (domainObject.isMutable) {
|
||||
domainObject.$refresh(refreshedObject);
|
||||
} else {
|
||||
refresh(domainObject, refreshedObject);
|
||||
utils.refresh(domainObject, refreshedObject);
|
||||
}
|
||||
|
||||
return domainObject;
|
||||
@@ -756,7 +740,7 @@ export default class ObjectAPI {
|
||||
* @returns {string} A string representation of the given identifier, including namespace and key
|
||||
*/
|
||||
makeKeyString(identifier) {
|
||||
return makeKeyString(identifier);
|
||||
return utils.makeKeyString(identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -764,7 +748,7 @@ export default class ObjectAPI {
|
||||
* @returns {module:openmct.ObjectAPI~Identifier} An identifier object
|
||||
*/
|
||||
parseKeyString(keyString) {
|
||||
return parseKeyString(keyString);
|
||||
return utils.parseKeyString(keyString);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -772,9 +756,9 @@ export default class ObjectAPI {
|
||||
* @param {module:openmct.ObjectAPI~Identifier[]} identifiers
|
||||
*/
|
||||
areIdsEqual(...identifiers) {
|
||||
const firstIdentifier = this.#parseKeyString(identifiers[0]);
|
||||
const firstIdentifier = utils.parseKeyString(identifiers[0]);
|
||||
|
||||
return identifiers.map(this.#parseKeyString).every((identifier) => {
|
||||
return identifiers.map(utils.parseKeyString).every((identifier) => {
|
||||
return (
|
||||
identifier === firstIdentifier ||
|
||||
(identifier.namespace === firstIdentifier.namespace &&
|
||||
@@ -802,7 +786,7 @@ export default class ObjectAPI {
|
||||
}
|
||||
|
||||
return path.some((pathElement) => {
|
||||
const identifierToCheck = this.#parseKeyString(keyStringToCheck);
|
||||
const identifierToCheck = utils.parseKeyString(keyStringToCheck);
|
||||
|
||||
return this.areIdsEqual(identifierToCheck, pathElement.identifier);
|
||||
});
|
||||
@@ -825,7 +809,7 @@ export default class ObjectAPI {
|
||||
if (location && !this.#pathContainsDomainObject(location, path)) {
|
||||
// if we have a location, and we don't already have this in our constructed path,
|
||||
// then keep walking up the path
|
||||
return this.getOriginalPath(this.#parseKeyString(location), path, abortSignal);
|
||||
return this.getOriginalPath(utils.parseKeyString(location), path, abortSignal);
|
||||
} else {
|
||||
return path;
|
||||
}
|
||||
@@ -864,8 +848,8 @@ export default class ObjectAPI {
|
||||
await Promise.all(
|
||||
keyStrings.map((keyString) =>
|
||||
this.supportsMutation(keyString)
|
||||
? this.getMutable(this.#parseKeyString(keyString))
|
||||
: this.get(this.#parseKeyString(keyString))
|
||||
? this.getMutable(utils.parseKeyString(keyString))
|
||||
: this.get(utils.parseKeyString(keyString))
|
||||
)
|
||||
)
|
||||
).reverse();
|
||||
@@ -877,7 +861,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, null, false);
|
||||
expect(objectAPI.get).toHaveBeenCalledWith(testObject.identifier);
|
||||
|
||||
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 { isIdentifier } from './object-utils.js';
|
||||
import utils from './object-utils.js';
|
||||
|
||||
export default class RootRegistry {
|
||||
constructor(openmct) {
|
||||
@@ -47,12 +47,12 @@ export default class RootRegistry {
|
||||
}
|
||||
|
||||
_isValid(rootItem) {
|
||||
if (isIdentifier(rootItem) || typeof rootItem === 'function') {
|
||||
if (utils.isIdentifier(rootItem) || typeof rootItem === 'function') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Array.isArray(rootItem)) {
|
||||
return rootItem.every(isIdentifier);
|
||||
return rootItem.every(utils.isIdentifier);
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -47,9 +47,9 @@ export default class Transaction {
|
||||
return Promise.all(promiseArray);
|
||||
}
|
||||
|
||||
createDirtyObjectPromise(object, action, ...args) {
|
||||
createDirtyObjectPromise(object, action) {
|
||||
return new Promise((resolve, reject) => {
|
||||
action(object, ...args)
|
||||
action(object)
|
||||
.then((success) => {
|
||||
const key = this.objectAPI.makeKeyString(object.identifier);
|
||||
|
||||
@@ -75,10 +75,10 @@ export default class Transaction {
|
||||
|
||||
_clear() {
|
||||
const promiseArray = [];
|
||||
const action = (obj) => this.objectAPI.refresh(obj, true);
|
||||
const refresh = this.objectAPI.refresh.bind(this.objectAPI);
|
||||
|
||||
Object.values(this.dirtyObjects).forEach((object) => {
|
||||
promiseArray.push(this.createDirtyObjectPromise(object, action));
|
||||
promiseArray.push(this.createDirtyObjectPromise(object, refresh));
|
||||
});
|
||||
|
||||
return Promise.all(promiseArray);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { makeKeyString, parseKeyString } from 'objectUtils';
|
||||
import utils from 'objectUtils';
|
||||
|
||||
import Transaction from './Transaction.js';
|
||||
|
||||
@@ -9,7 +9,7 @@ let transaction;
|
||||
describe('Transaction Class', () => {
|
||||
beforeEach(() => {
|
||||
objectAPI = {
|
||||
makeKeyString: (identifier) => makeKeyString(identifier),
|
||||
makeKeyString: (identifier) => utils.makeKeyString(identifier),
|
||||
save: () => Promise.resolve(true),
|
||||
mutate: (object, prop, value) => {
|
||||
object[prop] = value;
|
||||
@@ -18,7 +18,7 @@ describe('Transaction Class', () => {
|
||||
},
|
||||
refresh: (object) => Promise.resolve(object),
|
||||
areIdsEqual: (...identifiers) => {
|
||||
return identifiers.map(parseKeyString).every((identifier) => {
|
||||
return identifiers.map(utils.parseKeyString).every((identifier) => {
|
||||
return (
|
||||
identifier === identifiers[0] ||
|
||||
(identifier.namespace === identifiers[0].namespace &&
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
* Utility for checking if a thing is an Open MCT Identifier.
|
||||
* @private
|
||||
*/
|
||||
export function isIdentifier(thing) {
|
||||
function isIdentifier(thing) {
|
||||
return (
|
||||
typeof thing === 'object' &&
|
||||
Object.prototype.hasOwnProperty.call(thing, 'key') &&
|
||||
@@ -36,7 +36,7 @@ export function isIdentifier(thing) {
|
||||
* Utility for checking if a thing is a key string. Not perfect.
|
||||
* @private
|
||||
*/
|
||||
export function isKeyString(thing) {
|
||||
function isKeyString(thing) {
|
||||
return typeof thing === 'string';
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ export function isKeyString(thing) {
|
||||
* @param keyString
|
||||
* @returns identifier
|
||||
*/
|
||||
export function parseKeyString(keyString) {
|
||||
function parseKeyString(keyString) {
|
||||
if (isIdentifier(keyString)) {
|
||||
return keyString;
|
||||
}
|
||||
@@ -86,7 +86,7 @@ export function parseKeyString(keyString) {
|
||||
* @param identifier
|
||||
* @returns keyString
|
||||
*/
|
||||
export function makeKeyString(identifier) {
|
||||
function makeKeyString(identifier) {
|
||||
if (!identifier) {
|
||||
throw new Error('Cannot make key string from null identifier');
|
||||
}
|
||||
@@ -112,7 +112,7 @@ export function makeKeyString(identifier) {
|
||||
* @param domainObject
|
||||
* @returns oldFormatModel
|
||||
*/
|
||||
export function toOldFormat(model) {
|
||||
function toOldFormat(model) {
|
||||
model = JSON.parse(JSON.stringify(model));
|
||||
delete model.identifier;
|
||||
if (model.composition) {
|
||||
@@ -131,7 +131,7 @@ export function toOldFormat(model) {
|
||||
* @param keyString
|
||||
* @returns domainObject
|
||||
*/
|
||||
export function toNewFormat(model, keyString) {
|
||||
function toNewFormat(model, keyString) {
|
||||
model = JSON.parse(JSON.stringify(model));
|
||||
model.identifier = parseKeyString(keyString);
|
||||
if (model.composition) {
|
||||
@@ -148,7 +148,7 @@ export function toNewFormat(model, keyString) {
|
||||
* @param otherIdentifier
|
||||
* @returns Boolean true if identifiers are equal.
|
||||
*/
|
||||
export function identifierEquals(a, b) {
|
||||
function identifierEquals(a, b) {
|
||||
return a.key === b.key && a.namespace === b.namespace;
|
||||
}
|
||||
|
||||
@@ -160,12 +160,23 @@ export function identifierEquals(a, b) {
|
||||
* @param otherDomainOBject
|
||||
* @returns Boolean true if objects are equal.
|
||||
*/
|
||||
export function objectEquals(a, b) {
|
||||
function objectEquals(a, b) {
|
||||
return identifierEquals(a.identifier, b.identifier);
|
||||
}
|
||||
|
||||
export function refresh(oldObject, newObject) {
|
||||
function refresh(oldObject, newObject) {
|
||||
let deleted = _.difference(Object.keys(oldObject), Object.keys(newObject));
|
||||
deleted.forEach((propertyName) => delete oldObject[propertyName]);
|
||||
Object.assign(oldObject, newObject);
|
||||
}
|
||||
|
||||
export default {
|
||||
isIdentifier: isIdentifier,
|
||||
toOldFormat: toOldFormat,
|
||||
toNewFormat: toNewFormat,
|
||||
makeKeyString: makeKeyString,
|
||||
parseKeyString: parseKeyString,
|
||||
equals: objectEquals,
|
||||
identifierEquals: identifierEquals,
|
||||
refresh: refresh
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { makeKeyString, parseKeyString, toNewFormat, toOldFormat } from 'objectUtils';
|
||||
import objectUtils from 'objectUtils';
|
||||
|
||||
describe('objectUtils', function () {
|
||||
describe('keyString util', function () {
|
||||
@@ -31,27 +31,27 @@ describe('objectUtils', function () {
|
||||
|
||||
Object.keys(EXPECTATIONS).forEach(function (keyString) {
|
||||
it('parses "' + keyString + '".', function () {
|
||||
expect(parseKeyString(keyString)).toEqual(EXPECTATIONS[keyString]);
|
||||
expect(objectUtils.parseKeyString(keyString)).toEqual(EXPECTATIONS[keyString]);
|
||||
});
|
||||
|
||||
it('parses and re-encodes "' + keyString + '"', function () {
|
||||
const identifier = parseKeyString(keyString);
|
||||
expect(makeKeyString(identifier)).toEqual(keyString);
|
||||
const identifier = objectUtils.parseKeyString(keyString);
|
||||
expect(objectUtils.makeKeyString(identifier)).toEqual(keyString);
|
||||
});
|
||||
|
||||
it('is idempotent for "' + keyString + '".', function () {
|
||||
const identifier = parseKeyString(keyString);
|
||||
let again = parseKeyString(identifier);
|
||||
const identifier = objectUtils.parseKeyString(keyString);
|
||||
let again = objectUtils.parseKeyString(identifier);
|
||||
expect(identifier).toEqual(again);
|
||||
again = parseKeyString(again);
|
||||
again = parseKeyString(again);
|
||||
again = objectUtils.parseKeyString(again);
|
||||
again = objectUtils.parseKeyString(again);
|
||||
expect(identifier).toEqual(again);
|
||||
|
||||
let againKeyString = makeKeyString(again);
|
||||
let againKeyString = objectUtils.makeKeyString(again);
|
||||
expect(againKeyString).toEqual(keyString);
|
||||
againKeyString = makeKeyString(againKeyString);
|
||||
againKeyString = makeKeyString(againKeyString);
|
||||
againKeyString = makeKeyString(againKeyString);
|
||||
againKeyString = objectUtils.makeKeyString(againKeyString);
|
||||
againKeyString = objectUtils.makeKeyString(againKeyString);
|
||||
againKeyString = objectUtils.makeKeyString(againKeyString);
|
||||
expect(againKeyString).toEqual(keyString);
|
||||
});
|
||||
});
|
||||
@@ -60,7 +60,7 @@ describe('objectUtils', function () {
|
||||
describe('old object conversions', function () {
|
||||
it('translate ids', function () {
|
||||
expect(
|
||||
toNewFormat(
|
||||
objectUtils.toNewFormat(
|
||||
{
|
||||
prop: 'someValue'
|
||||
},
|
||||
@@ -77,7 +77,7 @@ describe('objectUtils', function () {
|
||||
|
||||
it('translates composition', function () {
|
||||
expect(
|
||||
toNewFormat(
|
||||
objectUtils.toNewFormat(
|
||||
{
|
||||
prop: 'someValue',
|
||||
composition: ['anotherObjectId', 'scratch:anotherObjectId']
|
||||
@@ -107,7 +107,7 @@ describe('objectUtils', function () {
|
||||
describe('new object conversions', function () {
|
||||
it('removes ids', function () {
|
||||
expect(
|
||||
toOldFormat({
|
||||
objectUtils.toOldFormat({
|
||||
prop: 'someValue',
|
||||
identifier: {
|
||||
namespace: '',
|
||||
@@ -121,7 +121,7 @@ describe('objectUtils', function () {
|
||||
|
||||
it('translates composition', function () {
|
||||
expect(
|
||||
toOldFormat({
|
||||
objectUtils.toOldFormat({
|
||||
prop: 'someValue',
|
||||
composition: [
|
||||
{
|
||||
|
||||
@@ -61,7 +61,6 @@ class Overlay extends EventEmitter {
|
||||
dismiss() {
|
||||
this.emit('destroy');
|
||||
this.destroy();
|
||||
this.container.remove();
|
||||
}
|
||||
|
||||
//Ensures that any callers are notified that the overlay is dismissed
|
||||
|
||||
@@ -1,194 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* 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,10 +20,9 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import { makeKeyString } from 'objectUtils';
|
||||
import objectUtils from 'objectUtils';
|
||||
|
||||
import CustomStringFormatter from '../../plugins/displayLayout/CustomStringFormatter.js';
|
||||
import BatchingWebSocket from './BatchingWebSocket.js';
|
||||
import DefaultMetadataProvider from './DefaultMetadataProvider.js';
|
||||
import TelemetryCollection from './TelemetryCollection.js';
|
||||
import TelemetryMetadataManager from './TelemetryMetadataManager.js';
|
||||
@@ -55,28 +54,6 @@ 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
|
||||
@@ -84,11 +61,6 @@ const SUBSCRIBE_STRATEGY = {
|
||||
*/
|
||||
export default class TelemetryAPI {
|
||||
#isGreedyLAD;
|
||||
#subscribeCache;
|
||||
|
||||
get SUBSCRIBE_STRATEGY() {
|
||||
return SUBSCRIBE_STRATEGY;
|
||||
}
|
||||
|
||||
constructor(openmct) {
|
||||
this.openmct = openmct;
|
||||
@@ -106,8 +78,6 @@ export default class TelemetryAPI {
|
||||
this.valueFormatterCache = new WeakMap();
|
||||
this.requestInterceptorRegistry = new TelemetryRequestInterceptorRegistry();
|
||||
this.#isGreedyLAD = true;
|
||||
this.BatchingWebSocket = BatchingWebSocket;
|
||||
this.#subscribeCache = {};
|
||||
}
|
||||
|
||||
abortAllRequests() {
|
||||
@@ -408,111 +378,54 @@ export default class TelemetryAPI {
|
||||
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
|
||||
* @param {module:openmct.DomainObject} domainObject the object
|
||||
* which has associated telemetry
|
||||
* @param {TelemetrySubscriptionOptions} options configuration items for subscription
|
||||
* @param {TelemetryRequestOptions} 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 = { strategy: SUBSCRIBE_STRATEGY.LATEST }) {
|
||||
const requestedStrategy = options.strategy || SUBSCRIBE_STRATEGY.LATEST;
|
||||
|
||||
subscribe(domainObject, callback, options) {
|
||||
if (domainObject.type === 'unknown') {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
const provider = this.findSubscriptionProvider(domainObject, options);
|
||||
const supportsBatching =
|
||||
Boolean(provider?.supportsBatching) && provider?.supportsBatching(domainObject, options);
|
||||
const provider = this.findSubscriptionProvider(domainObject);
|
||||
|
||||
if (!this.#subscribeCache) {
|
||||
this.#subscribeCache = {};
|
||||
if (!this.subscribeCache) {
|
||||
this.subscribeCache = {};
|
||||
}
|
||||
|
||||
const keyString = makeKeyString(domainObject.identifier);
|
||||
const supportedStrategy = supportsBatching ? requestedStrategy : SUBSCRIBE_STRATEGY.LATEST;
|
||||
// Override the requested strategy with the strategy supported by the provider
|
||||
const optionsWithSupportedStrategy = {
|
||||
...options,
|
||||
strategy: supportedStrategy
|
||||
};
|
||||
// If batching is supported, we need to cache a subscription for each strategy -
|
||||
// latest and batched.
|
||||
const cacheKey = `${keyString}:${supportedStrategy}`;
|
||||
let subscriber = this.#subscribeCache[cacheKey];
|
||||
const keyString = objectUtils.makeKeyString(domainObject.identifier);
|
||||
let subscriber = this.subscribeCache[keyString];
|
||||
|
||||
if (!subscriber) {
|
||||
subscriber = this.#subscribeCache[cacheKey] = {
|
||||
latestCallbacks: [],
|
||||
batchCallbacks: []
|
||||
subscriber = this.subscribeCache[keyString] = {
|
||||
callbacks: [callback]
|
||||
};
|
||||
if (provider) {
|
||||
subscriber.unsubscribe = provider.subscribe(
|
||||
domainObject,
|
||||
invokeCallbackWithRequestedStrategy,
|
||||
optionsWithSupportedStrategy
|
||||
function (value) {
|
||||
subscriber.callbacks.forEach(function (cb) {
|
||||
cb(value);
|
||||
});
|
||||
},
|
||||
options
|
||||
);
|
||||
} else {
|
||||
subscriber.unsubscribe = function () {};
|
||||
}
|
||||
}
|
||||
|
||||
if (requestedStrategy === SUBSCRIBE_STRATEGY.BATCH) {
|
||||
subscriber.batchCallbacks.push(callback);
|
||||
} else {
|
||||
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);
|
||||
});
|
||||
subscriber.callbacks.push(callback);
|
||||
}
|
||||
|
||||
return function unsubscribe() {
|
||||
subscriber.latestCallbacks = subscriber.latestCallbacks.filter(function (cb) {
|
||||
subscriber.callbacks = subscriber.callbacks.filter(function (cb) {
|
||||
return cb !== callback;
|
||||
});
|
||||
subscriber.batchCallbacks = subscriber.batchCallbacks.filter(function (cb) {
|
||||
return cb !== callback;
|
||||
});
|
||||
|
||||
if (subscriber.latestCallbacks.length === 0 && subscriber.batchCallbacks.length === 0) {
|
||||
if (subscriber.callbacks.length === 0) {
|
||||
subscriber.unsubscribe();
|
||||
delete this.#subscribeCache[cacheKey];
|
||||
delete this.subscribeCache[keyString];
|
||||
}
|
||||
}.bind(this);
|
||||
}
|
||||
@@ -541,7 +454,7 @@ export default class TelemetryAPI {
|
||||
this.stalenessSubscriberCache = {};
|
||||
}
|
||||
|
||||
const keyString = makeKeyString(domainObject.identifier);
|
||||
const keyString = objectUtils.makeKeyString(domainObject.identifier);
|
||||
let stalenessSubscriber = this.stalenessSubscriberCache[keyString];
|
||||
|
||||
if (!stalenessSubscriber) {
|
||||
@@ -600,7 +513,7 @@ export default class TelemetryAPI {
|
||||
this.limitsSubscribeCache = {};
|
||||
}
|
||||
|
||||
const keyString = makeKeyString(domainObject.identifier);
|
||||
const keyString = objectUtils.makeKeyString(domainObject.identifier);
|
||||
let subscriber = this.limitsSubscribeCache[keyString];
|
||||
|
||||
if (!subscriber) {
|
||||
|
||||
@@ -90,9 +90,7 @@ describe('Telemetry API', () => {
|
||||
|
||||
const callback = jasmine.createSpy('callback');
|
||||
const unsubscribe = telemetryAPI.subscribe(domainObject, callback);
|
||||
expect(telemetryProvider.supportsSubscribe).toHaveBeenCalledWith(domainObject, {
|
||||
strategy: 'latest'
|
||||
});
|
||||
expect(telemetryProvider.supportsSubscribe).toHaveBeenCalledWith(domainObject);
|
||||
expect(telemetryProvider.subscribe).not.toHaveBeenCalled();
|
||||
expect(unsubscribe).toEqual(jasmine.any(Function));
|
||||
|
||||
@@ -113,16 +111,12 @@ 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, {
|
||||
strategy: 'latest'
|
||||
});
|
||||
expect(telemetryProvider.supportsSubscribe).toHaveBeenCalledWith(domainObject);
|
||||
expect(telemetryProvider.subscribe.calls.count()).toBe(1);
|
||||
expect(telemetryProvider.subscribe).toHaveBeenCalledWith(
|
||||
domainObject,
|
||||
jasmine.any(Function),
|
||||
{
|
||||
strategy: 'latest'
|
||||
}
|
||||
undefined
|
||||
);
|
||||
|
||||
const notify = telemetryProvider.subscribe.calls.mostRecent().args[1];
|
||||
@@ -327,126 +321,6 @@ 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,14 +180,11 @@ 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),
|
||||
options
|
||||
this.options
|
||||
);
|
||||
}
|
||||
|
||||
@@ -212,8 +209,6 @@ 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) {
|
||||
@@ -276,13 +271,6 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,366 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
/* eslint-disable max-classes-per-file */
|
||||
export default function installWorker() {
|
||||
const FALLBACK_AND_WAIT_MS = [1000, 5000, 5000, 10000, 10000, 30000];
|
||||
|
||||
/**
|
||||
* @typedef {import('./BatchingWebSocket').BatchingStrategy} BatchingStrategy
|
||||
*/
|
||||
|
||||
/**
|
||||
* Provides a WebSocket connection that is resilient to errors and dropouts.
|
||||
* On an error or dropout, will automatically reconnect.
|
||||
*
|
||||
* Additionally, messages will be queued and sent only when WebSocket is
|
||||
* connected meaning that client code does not need to check the state of
|
||||
* the socket before sending.
|
||||
*/
|
||||
class ResilientWebSocket extends EventTarget {
|
||||
#webSocket;
|
||||
#isConnected = false;
|
||||
#isConnecting = false;
|
||||
#messageQueue = [];
|
||||
#reconnectTimeoutHandle;
|
||||
#currentWaitIndex = 0;
|
||||
#messageCallbacks = [];
|
||||
#wsUrl;
|
||||
|
||||
/**
|
||||
* Establish a new WebSocket connection to the given URL
|
||||
* @param {String} url
|
||||
*/
|
||||
connect(url) {
|
||||
this.#wsUrl = url;
|
||||
if (this.#isConnected) {
|
||||
throw new Error('WebSocket already connected');
|
||||
}
|
||||
|
||||
if (this.#isConnecting) {
|
||||
throw new Error('WebSocket connection in progress');
|
||||
}
|
||||
|
||||
this.#isConnecting = true;
|
||||
|
||||
this.#webSocket = new WebSocket(url);
|
||||
|
||||
const boundConnected = this.#connected.bind(this);
|
||||
this.#webSocket.addEventListener('open', boundConnected);
|
||||
|
||||
const boundCleanUpAndReconnect = this.#cleanUpAndReconnect.bind(this);
|
||||
this.#webSocket.addEventListener('error', boundCleanUpAndReconnect);
|
||||
this.#webSocket.addEventListener('close', boundCleanUpAndReconnect);
|
||||
|
||||
const boundMessage = this.#message.bind(this);
|
||||
this.#webSocket.addEventListener('message', boundMessage);
|
||||
|
||||
this.addEventListener(
|
||||
'disconnected',
|
||||
() => {
|
||||
this.#webSocket.removeEventListener('open', boundConnected);
|
||||
this.#webSocket.removeEventListener('error', boundCleanUpAndReconnect);
|
||||
this.#webSocket.removeEventListener('close', boundCleanUpAndReconnect);
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a callback to be invoked when a message is received on the WebSocket.
|
||||
* This paradigm is used instead of the standard EventTarget or EventEmitter approach
|
||||
* for performance reasons.
|
||||
* @param {Function} callback The function to be invoked when a message is received
|
||||
* @returns an unregister function
|
||||
*/
|
||||
registerMessageCallback(callback) {
|
||||
this.#messageCallbacks.push(callback);
|
||||
|
||||
return () => {
|
||||
this.#messageCallbacks = this.#messageCallbacks.filter((cb) => cb !== callback);
|
||||
};
|
||||
}
|
||||
|
||||
#connected() {
|
||||
console.debug('Websocket connected.');
|
||||
this.#isConnected = true;
|
||||
this.#isConnecting = false;
|
||||
this.#currentWaitIndex = 0;
|
||||
|
||||
this.dispatchEvent(new Event('connected'));
|
||||
|
||||
this.#flushQueue();
|
||||
}
|
||||
|
||||
#cleanUpAndReconnect() {
|
||||
console.warn('Websocket closed. Attempting to reconnect...');
|
||||
this.disconnect();
|
||||
this.#reconnect();
|
||||
}
|
||||
|
||||
#message(event) {
|
||||
this.#messageCallbacks.forEach((callback) => callback(event.data));
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.#isConnected = false;
|
||||
this.#isConnecting = false;
|
||||
|
||||
// On WebSocket error, both error callback and close callback are invoked, resulting in
|
||||
// this function being called twice, and websocket being destroyed and deallocated.
|
||||
if (this.#webSocket !== undefined && this.#webSocket !== null) {
|
||||
this.#webSocket.close();
|
||||
}
|
||||
|
||||
this.dispatchEvent(new Event('disconnected'));
|
||||
this.#webSocket = undefined;
|
||||
}
|
||||
|
||||
#reconnect() {
|
||||
if (this.#reconnectTimeoutHandle) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#reconnectTimeoutHandle = setTimeout(() => {
|
||||
this.connect(this.#wsUrl);
|
||||
|
||||
this.#reconnectTimeoutHandle = undefined;
|
||||
}, FALLBACK_AND_WAIT_MS[this.#currentWaitIndex]);
|
||||
|
||||
if (this.#currentWaitIndex < FALLBACK_AND_WAIT_MS.length - 1) {
|
||||
this.#currentWaitIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
enqueueMessage(message) {
|
||||
this.#messageQueue.push(message);
|
||||
this.#flushQueueIfReady();
|
||||
}
|
||||
|
||||
#flushQueueIfReady() {
|
||||
if (this.#isConnected) {
|
||||
this.#flushQueue();
|
||||
}
|
||||
}
|
||||
|
||||
#flushQueue() {
|
||||
while (this.#messageQueue.length > 0) {
|
||||
if (!this.#isConnected) {
|
||||
break;
|
||||
}
|
||||
|
||||
const message = this.#messageQueue.shift();
|
||||
this.#webSocket.send(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles messages over the worker interface, and
|
||||
* sends corresponding WebSocket messages.
|
||||
*/
|
||||
class WorkerToWebSocketMessageBroker {
|
||||
#websocket;
|
||||
#messageBatcher;
|
||||
|
||||
constructor(websocket, messageBatcher) {
|
||||
this.#websocket = websocket;
|
||||
this.#messageBatcher = messageBatcher;
|
||||
}
|
||||
|
||||
routeMessageToHandler(message) {
|
||||
const { type } = message.data;
|
||||
switch (type) {
|
||||
case 'connect':
|
||||
this.connect(message);
|
||||
break;
|
||||
case 'disconnect':
|
||||
this.disconnect(message);
|
||||
break;
|
||||
case 'message':
|
||||
this.#websocket.enqueueMessage(message.data.message);
|
||||
break;
|
||||
case 'setBatchingStrategy':
|
||||
this.setBatchingStrategy(message);
|
||||
break;
|
||||
case 'readyForNextBatch':
|
||||
this.#messageBatcher.readyForNextBatch();
|
||||
break;
|
||||
case 'setMaxBatchSize':
|
||||
this.#messageBatcher.setMaxBatchSize(message.data.maxBatchSize);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown message type: ${type}`);
|
||||
}
|
||||
}
|
||||
connect(message) {
|
||||
const { url } = message.data;
|
||||
this.#websocket.connect(url);
|
||||
}
|
||||
disconnect() {
|
||||
this.#websocket.disconnect();
|
||||
}
|
||||
setBatchingStrategy(message) {
|
||||
const { serializedStrategy } = message.data;
|
||||
const batchingStrategy = {
|
||||
// eslint-disable-next-line no-new-func
|
||||
shouldBatchMessage: new Function(`return ${serializedStrategy.shouldBatchMessage}`)(),
|
||||
// eslint-disable-next-line no-new-func
|
||||
getBatchIdFromMessage: new Function(`return ${serializedStrategy.getBatchIdFromMessage}`)()
|
||||
// Will also include maximum batch length here
|
||||
};
|
||||
this.#messageBatcher.setBatchingStrategy(batchingStrategy);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Received messages from the WebSocket, and passes them along to the
|
||||
* Worker interface and back to the main thread.
|
||||
*/
|
||||
class WebSocketToWorkerMessageBroker {
|
||||
#worker;
|
||||
#messageBatcher;
|
||||
|
||||
constructor(messageBatcher, worker) {
|
||||
this.#messageBatcher = messageBatcher;
|
||||
this.#worker = worker;
|
||||
}
|
||||
|
||||
routeMessageToHandler(data) {
|
||||
//Implement batching here
|
||||
if (this.#messageBatcher.shouldBatchMessage(data)) {
|
||||
this.#messageBatcher.addMessageToBatch(data);
|
||||
} else {
|
||||
this.#worker.postMessage({
|
||||
type: 'message',
|
||||
message: data
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Responsible for batching messages according to the defined batching strategy.
|
||||
*/
|
||||
class MessageBatcher {
|
||||
#batch;
|
||||
#batchingStrategy;
|
||||
#hasBatch = false;
|
||||
#maxBatchSize;
|
||||
#readyForNextBatch;
|
||||
#worker;
|
||||
|
||||
constructor(worker) {
|
||||
this.#maxBatchSize = 10;
|
||||
this.#readyForNextBatch = false;
|
||||
this.#worker = worker;
|
||||
this.#resetBatch();
|
||||
}
|
||||
#resetBatch() {
|
||||
this.#batch = {};
|
||||
this.#hasBatch = false;
|
||||
}
|
||||
/**
|
||||
* @param {BatchingStrategy} strategy
|
||||
*/
|
||||
setBatchingStrategy(strategy) {
|
||||
this.#batchingStrategy = strategy;
|
||||
}
|
||||
/**
|
||||
* Applies the `shouldBatchMessage` function from the supplied batching strategy
|
||||
* to each message to determine if it should be added to a batch. If not batched,
|
||||
* the message is immediately sent over the worker to the main thread.
|
||||
* @param {any} message the message received from the WebSocket. See the WebSocket
|
||||
* documentation for more details -
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent/data
|
||||
* @returns
|
||||
*/
|
||||
shouldBatchMessage(message) {
|
||||
return (
|
||||
this.#batchingStrategy.shouldBatchMessage &&
|
||||
this.#batchingStrategy.shouldBatchMessage(message)
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Adds the given message to a batch. The batch group that the message is added
|
||||
* to will be determined by the value returned by `getBatchIdFromMessage`.
|
||||
* @param {any} message the message received from the WebSocket. See the WebSocket
|
||||
* documentation for more details -
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent/data
|
||||
*/
|
||||
addMessageToBatch(message) {
|
||||
const batchId = this.#batchingStrategy.getBatchIdFromMessage(message);
|
||||
let batch = this.#batch[batchId];
|
||||
if (batch === undefined) {
|
||||
batch = this.#batch[batchId] = [message];
|
||||
} else {
|
||||
batch.push(message);
|
||||
}
|
||||
if (batch.length > this.#maxBatchSize) {
|
||||
batch.shift();
|
||||
this.#batch.dropped = this.#batch.dropped || true;
|
||||
}
|
||||
if (this.#readyForNextBatch) {
|
||||
this.#sendNextBatch();
|
||||
} else {
|
||||
this.#hasBatch = true;
|
||||
}
|
||||
}
|
||||
setMaxBatchSize(maxBatchSize) {
|
||||
this.#maxBatchSize = maxBatchSize;
|
||||
}
|
||||
/**
|
||||
* Indicates that client code is ready to receive the next batch of
|
||||
* messages. If a batch is available, it will be immediately sent.
|
||||
* Otherwise a flag will be set to send the next batch as soon as
|
||||
* any new data is available.
|
||||
*/
|
||||
readyForNextBatch() {
|
||||
if (this.#hasBatch) {
|
||||
this.#sendNextBatch();
|
||||
} else {
|
||||
this.#readyForNextBatch = true;
|
||||
}
|
||||
}
|
||||
#sendNextBatch() {
|
||||
const batch = this.#batch;
|
||||
this.#resetBatch();
|
||||
this.#worker.postMessage({
|
||||
type: 'batch',
|
||||
batch
|
||||
});
|
||||
this.#readyForNextBatch = false;
|
||||
this.#hasBatch = false;
|
||||
}
|
||||
}
|
||||
|
||||
const websocket = new ResilientWebSocket();
|
||||
const messageBatcher = new MessageBatcher(self);
|
||||
const workerBroker = new WorkerToWebSocketMessageBroker(websocket, messageBatcher);
|
||||
const websocketBroker = new WebSocketToWorkerMessageBroker(messageBatcher, self);
|
||||
|
||||
self.addEventListener('message', (message) => {
|
||||
workerBroker.routeMessageToHandler(message);
|
||||
});
|
||||
websocket.registerMessageCallback((data) => {
|
||||
websocketBroker.routeMessageToHandler(data);
|
||||
});
|
||||
}
|
||||
@@ -32,7 +32,6 @@ export default class StatusAPI extends EventEmitter {
|
||||
|
||||
this.onProviderStatusChange = this.onProviderStatusChange.bind(this);
|
||||
this.onProviderPollQuestionChange = this.onProviderPollQuestionChange.bind(this);
|
||||
this.onMissionActionStatusChange = this.onMissionActionStatusChange.bind(this);
|
||||
this.listenToStatusEvents = this.listenToStatusEvents.bind(this);
|
||||
|
||||
this.#openmct.once('destroy', () => {
|
||||
@@ -41,7 +40,6 @@ export default class StatusAPI extends EventEmitter {
|
||||
if (typeof provider?.off === 'function') {
|
||||
provider.off('statusChange', this.onProviderStatusChange);
|
||||
provider.off('pollQuestionChange', this.onProviderPollQuestionChange);
|
||||
provider.off('missionActionStatusChange', this.onMissionActionStatusChange);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -102,67 +100,6 @@ export default class StatusAPI extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Can the currently logged in user set the mission status.
|
||||
* @returns {Promise<Boolean>} true if the currently logged in user can set the mission status, false otherwise.
|
||||
*/
|
||||
canSetMissionStatus() {
|
||||
const provider = this.#userAPI.getProvider();
|
||||
|
||||
if (provider.canSetMissionStatus) {
|
||||
return provider.canSetMissionStatus();
|
||||
} else {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the current status for the given mission action
|
||||
* @param {MissionAction} action
|
||||
* @returns {string}
|
||||
*/
|
||||
getStatusForMissionAction(action) {
|
||||
const provider = this.#userAPI.getProvider();
|
||||
|
||||
if (provider.getStatusForMissionAction) {
|
||||
return provider.getStatusForMissionAction(action);
|
||||
} else {
|
||||
this.#userAPI.error('User provider does not support getting mission action status');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the list of possible mission status options (GO, NO-GO, etc.)
|
||||
* @returns {Promise<MissionStatusOption[]>} the complete list of possible mission statuses
|
||||
*/
|
||||
async getPossibleMissionActionStatuses() {
|
||||
const provider = this.#userAPI.getProvider();
|
||||
|
||||
if (provider.getPossibleMissionActionStatuses) {
|
||||
const possibleOptions = await provider.getPossibleMissionActionStatuses();
|
||||
|
||||
return possibleOptions;
|
||||
} else {
|
||||
this.#userAPI.error('User provider does not support mission status options');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the list of possible mission actions
|
||||
* @returns {Promise<string[]>} the list of possible mission actions
|
||||
*/
|
||||
async getPossibleMissionActions() {
|
||||
const provider = this.#userAPI.getProvider();
|
||||
|
||||
if (provider.getPossibleMissionActions) {
|
||||
const possibleActions = await provider.getPossibleMissionActions();
|
||||
|
||||
return possibleActions;
|
||||
} else {
|
||||
this.#userAPI.error('User provider does not support mission statuses');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<Array<Status>>} the complete list of possible states that an operator can reply to a poll question with.
|
||||
*/
|
||||
@@ -229,21 +166,6 @@ export default class StatusAPI extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MissionAction} action
|
||||
* @param {MissionStatusOption} status
|
||||
* @returns {Promise<Boolean>} true if operation was successful, otherwise false.
|
||||
*/
|
||||
setStatusForMissionAction(action, status) {
|
||||
const provider = this.#userAPI.getProvider();
|
||||
|
||||
if (provider.setStatusForMissionAction) {
|
||||
return provider.setStatusForMissionAction(action, status);
|
||||
} else {
|
||||
this.#userAPI.error('User provider does not support setting mission role status');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the status of the provided role back to its default status.
|
||||
* @param {import("./UserAPI").Role} role The role to set the status for.
|
||||
@@ -323,7 +245,6 @@ export default class StatusAPI extends EventEmitter {
|
||||
if (typeof provider.on === 'function') {
|
||||
provider.on('statusChange', this.onProviderStatusChange);
|
||||
provider.on('pollQuestionChange', this.onProviderPollQuestionChange);
|
||||
provider.on('missionActionStatusChange', this.onMissionActionStatusChange);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,23 +261,14 @@ export default class StatusAPI extends EventEmitter {
|
||||
onProviderPollQuestionChange(pollQuestion) {
|
||||
this.emit('pollQuestionChange', pollQuestion);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
onMissionActionStatusChange({ action, status }) {
|
||||
this.emit('missionActionStatusChange', { action, status });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {import('./UserProvider')} UserProvider
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('./StatusUserProvider')} StatusUserProvider
|
||||
*/
|
||||
|
||||
/**
|
||||
* The PollQuestion type
|
||||
* @typedef {Object} PollQuestion
|
||||
@@ -364,19 +276,6 @@ export default class StatusAPI extends EventEmitter {
|
||||
* @property {Number} timestamp - The time that the poll question was set.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The MissionStatus type
|
||||
* @typedef {Object} MissionStatusOption
|
||||
* @extends {Status}
|
||||
* @property {String} color A color to be used when displaying the mission status
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} MissionAction
|
||||
* @property {String} key A unique identifier for this action
|
||||
* @property {String} label A human readable label for this action
|
||||
*/
|
||||
|
||||
/**
|
||||
* The Status type
|
||||
* @typedef {Object} Status
|
||||
|
||||
@@ -23,12 +23,12 @@ import UserProvider from './UserProvider.js';
|
||||
|
||||
export default class StatusUserProvider extends UserProvider {
|
||||
/**
|
||||
* @param {('statusChange'|'pollQuestionChange'|'missionActionStatusChange')} event the name of the event to listen to
|
||||
* @param {('statusChange'|'pollQuestionChange')} event the name of the event to listen to
|
||||
* @param {Function} callback a function to invoke when this event occurs
|
||||
*/
|
||||
on(event, callback) {}
|
||||
/**
|
||||
* @param {('statusChange'|'pollQuestionChange'|'missionActionStatusChange')} event the name of the event to stop listen to
|
||||
* @param {('statusChange'|'pollQuestionChange')} event the name of the event to stop listen to
|
||||
* @param {Function} callback the callback function used to register the listener
|
||||
*/
|
||||
off(event, callback) {}
|
||||
|
||||
@@ -24,6 +24,9 @@ import ExampleUserProvider from '../../../example/exampleUser/ExampleUserProvide
|
||||
import { createOpenMct, resetApplicationState } from '../../utils/testing.js';
|
||||
import { MULTIPLE_PROVIDER_ERROR } from './constants.js';
|
||||
|
||||
const USERNAME = 'Test User';
|
||||
const EXAMPLE_ROLE = 'flight';
|
||||
|
||||
describe('The User API', () => {
|
||||
let openmct;
|
||||
|
||||
@@ -62,4 +65,48 @@ describe('The User API', () => {
|
||||
expect(openmct.user.hasProvider()).toBeTrue();
|
||||
});
|
||||
});
|
||||
|
||||
describe('provides the ability', () => {
|
||||
let provider;
|
||||
|
||||
beforeEach(() => {
|
||||
provider = new ExampleUserProvider(openmct);
|
||||
provider.autoLogin(USERNAME);
|
||||
});
|
||||
|
||||
it('to check if a user (not specific) is logged in', (done) => {
|
||||
expect(openmct.user.isLoggedIn()).toBeFalse();
|
||||
|
||||
openmct.user.on('providerAdded', () => {
|
||||
expect(openmct.user.isLoggedIn()).toBeTrue();
|
||||
done();
|
||||
});
|
||||
|
||||
// this will trigger the user indicator plugin,
|
||||
// which will in turn login the user
|
||||
openmct.user.setProvider(provider);
|
||||
});
|
||||
|
||||
it('to get the current user', (done) => {
|
||||
openmct.user.setProvider(provider);
|
||||
openmct.user
|
||||
.getCurrentUser()
|
||||
.then((apiUser) => {
|
||||
expect(apiUser.name).toEqual(USERNAME);
|
||||
})
|
||||
.finally(done);
|
||||
});
|
||||
|
||||
it('to check if a user has a specific role (by id)', (done) => {
|
||||
openmct.user.setProvider(provider);
|
||||
let junkIdCheckPromise = openmct.user.hasRole('junk-id').then((hasRole) => {
|
||||
expect(hasRole).toBeFalse();
|
||||
});
|
||||
let realIdCheckPromise = openmct.user.hasRole(EXAMPLE_ROLE).then((hasRole) => {
|
||||
expect(hasRole).toBeTrue();
|
||||
});
|
||||
|
||||
Promise.all([junkIdCheckPromise, realIdCheckPromise]).finally(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -38,6 +38,7 @@ describe('the plugin', function () {
|
||||
|
||||
let couchPlugin = openmct.plugins.CouchDB(testPath);
|
||||
openmct.install(couchPlugin);
|
||||
|
||||
openmct.install(
|
||||
new CouchDBSearchFolderPlugin('CouchDB Documents', couchPlugin, {
|
||||
selector: {
|
||||
|
||||
@@ -46,14 +46,14 @@ describe('DeviceMatchers', function () {
|
||||
return 'is' + deviceType[0].toUpperCase() + deviceType.slice(1);
|
||||
}
|
||||
|
||||
['mobile', 'phone', 'tablet', 'landscape', 'portrait', 'landscape', 'touch'].forEach(
|
||||
function (deviceType) {
|
||||
it('detects when a device is a ' + deviceType + ' device', function () {
|
||||
mockAgent[method(deviceType)].and.returnValue(true);
|
||||
expect(DeviceMatchers[deviceType](mockAgent)).toBe(true);
|
||||
mockAgent[method(deviceType)].and.returnValue(false);
|
||||
expect(DeviceMatchers[deviceType](mockAgent)).toBe(false);
|
||||
});
|
||||
}
|
||||
);
|
||||
['mobile', 'phone', 'tablet', 'landscape', 'portrait', 'landscape', 'touch'].forEach(function (
|
||||
deviceType
|
||||
) {
|
||||
it('detects when a device is a ' + deviceType + ' device', function () {
|
||||
mockAgent[method(deviceType)].and.returnValue(true);
|
||||
expect(DeviceMatchers[deviceType](mockAgent)).toBe(true);
|
||||
mockAgent[method(deviceType)].and.returnValue(false);
|
||||
expect(DeviceMatchers[deviceType](mockAgent)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -79,6 +79,5 @@ export default class LADTableView {
|
||||
if (this._destroy) {
|
||||
this._destroy();
|
||||
}
|
||||
this.component = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ export default class LadTableSetView {
|
||||
this.component = null;
|
||||
}
|
||||
|
||||
show(element, isEditing, { renderWhenVisible }) {
|
||||
show(element) {
|
||||
let ladTableConfiguration = new LADTableConfiguration(this.domainObject, this.openmct);
|
||||
|
||||
const { vNode, destroy } = mount(
|
||||
@@ -47,8 +47,7 @@ export default class LadTableSetView {
|
||||
openmct: this.openmct,
|
||||
objectPath: this.objectPath,
|
||||
currentView: this,
|
||||
ladTableConfiguration,
|
||||
renderWhenVisible
|
||||
ladTableConfiguration
|
||||
},
|
||||
data: () => {
|
||||
return {
|
||||
|
||||
@@ -35,20 +35,13 @@
|
||||
>
|
||||
{{ domainObject.name }}
|
||||
</td>
|
||||
<td v-if="showTimestamp" aria-label="lad timestamp" class="js-second-data">
|
||||
{{ formattedTimestamp }}
|
||||
</td>
|
||||
<td aria-label="lad value" class="js-third-data" :class="valueClasses">{{ value }}</td>
|
||||
<td v-if="showTimestamp" class="js-second-data">{{ formattedTimestamp }}</td>
|
||||
<td class="js-third-data" :class="valueClasses">{{ value }}</td>
|
||||
<td v-if="hasUnits" class="js-units">
|
||||
{{ unit }}
|
||||
</td>
|
||||
<td v-if="showType" aria-label="lad type" class="js-type-data">{{ typeLabel }}</td>
|
||||
<td
|
||||
v-for="limit in formattedLimitValues"
|
||||
:key="limit.key"
|
||||
aria-label="lad limit value"
|
||||
class="js-limit-data"
|
||||
>
|
||||
<td v-if="showType" class="js-type-data">{{ typeLabel }}</td>
|
||||
<td v-for="limit in formattedLimitValues" :key="limit.key" class="js-limit-data">
|
||||
{{ limit.value }}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -143,9 +143,6 @@ export default {
|
||||
ladTable.domainObject = domainObject;
|
||||
ladTable.key = this.openmct.objects.makeKeyString(domainObject.identifier);
|
||||
|
||||
if (!this.ladTelemetryObjects) {
|
||||
this.ladTelemetryObjects = {};
|
||||
}
|
||||
this.ladTelemetryObjects[ladTable.key] = [];
|
||||
this.ladTableObjects.push(ladTable);
|
||||
|
||||
|
||||
@@ -423,7 +423,7 @@ describe('The LAD Table Set', () => {
|
||||
(viewProvider) => viewProvider.key === ladTableSetKey
|
||||
);
|
||||
ladTableSetView = ladTableSetViewProvider.view(mockObj.ladTableSet, [mockObj.ladTableSet]);
|
||||
ladTableSetView.show(child, false, { renderWhenVisible });
|
||||
ladTableSetView.show(child);
|
||||
|
||||
return nextTick();
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
* Open MCT, Copyright (c) 2014-2023, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
@@ -20,39 +20,22 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import { ACTIVITY_STATES_KEY } from './createActivityStatesIdentifier.js';
|
||||
import { ACTIVITYSTATES_KEY, ACTIVITYSTATES_TYPE } from './createActivityStatesIdentifier.js';
|
||||
|
||||
/**
|
||||
* @typedef {object} ActivityStatesInterceptorOptions
|
||||
* @property {import('../../api/objects/ObjectAPI').Identifier} identifier the {namespace, key} to use for the activity states object.
|
||||
* @property {string} name The name of the activity states model.
|
||||
* @property {number} priority the priority of the interceptor. By default, it is low.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates an activity states object in the persistence store. This is used to save plan activity states.
|
||||
* This will only get invoked when an attempt is made to save the state for an activity and no activity states object exists in the store.
|
||||
* @param {import('../../../openmct').OpenMCT} openmct
|
||||
* @param {ActivityStatesInterceptorOptions} options
|
||||
* @returns {object}
|
||||
*/
|
||||
const ACTIVITY_STATES_TYPE = 'activity-states';
|
||||
|
||||
function activityStatesInterceptor(openmct, options) {
|
||||
const { identifier, name, priority = openmct.priority.LOW } = options;
|
||||
function activityStatesInterceptor(openmct, identifierObject, name) {
|
||||
const activityStatesModel = {
|
||||
identifier,
|
||||
identifier: identifierObject,
|
||||
name,
|
||||
type: ACTIVITY_STATES_TYPE,
|
||||
type: ACTIVITYSTATES_TYPE,
|
||||
activities: {},
|
||||
location: null
|
||||
};
|
||||
|
||||
return {
|
||||
appliesTo: (identifierObject) => {
|
||||
return identifierObject.key === ACTIVITY_STATES_KEY;
|
||||
appliesTo: (identifier) => {
|
||||
return identifier.key === ACTIVITYSTATES_KEY;
|
||||
},
|
||||
invoke: (identifierObject, object) => {
|
||||
invoke: (identifier, object) => {
|
||||
if (!object || openmct.objects.isMissing(object)) {
|
||||
openmct.objects.save(activityStatesModel);
|
||||
|
||||
@@ -61,7 +44,7 @@ function activityStatesInterceptor(openmct, options) {
|
||||
|
||||
return object;
|
||||
},
|
||||
priority
|
||||
priority: openmct.priority.HIGH
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,30 +1,9 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
export const ACTIVITY_STATES_KEY = 'activity-states';
|
||||
export const ACTIVITYSTATES_KEY = 'activity-states';
|
||||
export const ACTIVITYSTATES_TYPE = 'activity-states';
|
||||
|
||||
export function createActivityStatesIdentifier(namespace = '') {
|
||||
return {
|
||||
key: ACTIVITY_STATES_KEY,
|
||||
key: ACTIVITYSTATES_KEY,
|
||||
namespace
|
||||
};
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user