Compare commits

..

11 Commits

249 changed files with 2348 additions and 6975 deletions

View File

@@ -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:

View File

@@ -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/**",

View File

@@ -8,7 +8,7 @@ Closes <!--- Insert Issue Number(s) this PR addresses. Start by typing # will op
* [ ] Have you followed the guidelines in our [Contributing document](https://github.com/nasa/openmct/blob/master/CONTRIBUTING.md)?
* [ ] Have you checked to ensure there aren't other open [Pull Requests](https://github.com/nasa/openmct/pulls) for the same update/change?
* [ ] Is this a notable change 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
View File

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

View File

@@ -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}`);
}

View File

@@ -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}`);
}

View File

@@ -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"]
}

View File

@@ -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.

View File

@@ -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:**

View File

@@ -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'
});

View File

@@ -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

View File

@@ -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);

View File

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

View File

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

View File

@@ -1,69 +0,0 @@
// playwright.config.js
// @ts-check
import { devices } from '@playwright/test';
const MAX_FAILURES = 5;
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;

View File

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

View File

@@ -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"
}
]
}

View File

@@ -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"
}
]
}

View File

@@ -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();
});
});

View File

@@ -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();
}

View File

@@ -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'
);
});
});

View File

@@ -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);

View File

@@ -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;

View File

@@ -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

View File

@@ -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'

View File

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

View File

@@ -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];
}

View File

@@ -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');
});
});

View File

@@ -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();

View File

@@ -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();
});
});

View File

@@ -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();
});
});

View File

@@ -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();
});
});

View File

@@ -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

View File

@@ -33,15 +33,15 @@ import {
import { expect, test } from '../../../../pluginFixtures.js';
test.describe('Overlay Plot', () => {
let overlayPlot;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot'
});
});
test('Plot legend color is in sync with plot series color', async ({ page }) => {
const overlayPlot = await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot'
});
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
parent: overlayPlot.uuid
@@ -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();
}

View File

@@ -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);

View File

@@ -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();
});
});

View File

@@ -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);
});
});
/**

View File

@@ -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);
});
});

View File

@@ -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')
);
});
});

View File

@@ -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';

View File

@@ -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();
});
});

View File

@@ -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();
});
});

View File

@@ -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

View File

@@ -298,7 +298,7 @@ test.describe('Recent Objects', () => {
// Assert that the list is empty
expect(await recentObjectsList.locator('.c-recentobjects-listitem').count()).toBe(0);
});
test('Verify functionality of "clear" and "collapse pane" buttons', async ({ page }) => {
test('Ensure clear recent objects button is active or inactive', async ({ page }) => {
// Assert that the list initially contains 3 objects (clock, folder, my items)
expect(await recentObjectsList.locator('.c-recentobjects-listitem').count()).toBe(3);
@@ -331,24 +331,6 @@ test.describe('Recent Objects', () => {
expect(await page.getByRole('button', { name: 'Clear Recently Viewed' }).isEnabled()).toBe(
true
);
// Assert initial state of pane and collapse the Recent Objects panel
await expect(page.getByLabel('Expand Recently Viewed Pane')).toBeHidden();
await expect(page.getByLabel('Collapse Recently Viewed Pane')).toBeVisible();
await page.getByLabel('Collapse Recently Viewed Pane').click();
// Assert that the "Expand Recently Viewed Pane" button is visible
// and that the "Collapse Recently Viewed Pane" button is hidden
await expect(page.getByLabel('Expand Recently Viewed Pane')).toBeVisible();
await expect(page.getByLabel('Collapse Recently Viewed Pane')).toBeHidden();
// Expand the Recent Objects panel by clicking on the "Expand Recently Viewed Pane" button
await page.getByLabel('Expand Recently Viewed Pane').click();
// Assert that the "Expand Recently Viewed Pane" button is hidden
// and that the "Collapse Recently Viewed Pane" button is visible
await expect(page.getByLabel('Expand Recently Viewed Pane')).toBeHidden();
await expect(page.getByLabel('Collapse Recently Viewed Pane')).toBeVisible();
});
function assertInitialRecentObjectsListState() {

View File

@@ -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(

View File

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

View File

@@ -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('#', '!'));

View File

@@ -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');
});
});

View File

@@ -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();
});

View File

@@ -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);

View File

@@ -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);
});
});

View File

@@ -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);
});

View File

@@ -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);
});

View File

@@ -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}')`);
});

View File

@@ -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})`);
});
});

View File

@@ -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);
});
});

View File

@@ -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);
});
});

View File

@@ -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);
});
});

View File

@@ -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);
}

View File

@@ -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);
};

View File

@@ -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,

View File

@@ -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

View File

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

View File

@@ -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",

View File

@@ -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());

View File

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

View File

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

View File

@@ -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);

View File

@@ -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);
});

View File

@@ -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"

View File

@@ -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)"

View File

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

View File

@@ -21,7 +21,7 @@
*****************************************************************************/
import EventEmitter from 'EventEmitter';
import { identifierEquals, makeKeyString, parseKeyString, refresh } from 'objectUtils';
import utils from 'objectUtils';
import ConflictError from './ConflictError.js';
import InMemorySearchProvider from './InMemorySearchProvider.js';
@@ -82,19 +82,8 @@ import Transaction from './Transaction.js';
* @memberof module:openmct
*/
export default class ObjectAPI {
#makeKeyString;
#parseKeyString;
#identifierEquals;
#refresh;
#openmct;
constructor(typeRegistry, openmct) {
this.#makeKeyString = makeKeyString;
this.#parseKeyString = parseKeyString;
this.#identifierEquals = identifierEquals;
this.#refresh = refresh;
this.#openmct = openmct;
this.openmct = openmct;
this.typeRegistry = typeRegistry;
this.SEARCH_TYPES = Object.freeze({
OBJECTS: 'OBJECTS',
@@ -217,14 +206,14 @@ export default class ObjectAPI {
* has been saved, or be rejected if it cannot be saved
*/
get(identifier, abortSignal, forceRemote = false) {
let keystring = this.#makeKeyString(identifier);
let keystring = this.makeKeyString(identifier);
if (!forceRemote) {
if (this.cache[keystring] !== undefined) {
return this.cache[keystring];
}
identifier = parseKeyString(identifier);
identifier = utils.parseKeyString(identifier);
if (this.isTransactionActive()) {
let dirtyObject = this.transaction.getDirtyObject(identifier);
@@ -238,7 +227,7 @@ export default class ObjectAPI {
const provider = this.getProvider(identifier);
if (!provider) {
throw new Error(`No Provider Matched for keyString "${this.#makeKeyString(identifier)}"`);
throw new Error(`No Provider Matched for keyString "${this.makeKeyString(identifier)}"`);
}
if (!provider.get) {
@@ -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)
);
}

View File

@@ -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);

View File

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

View File

@@ -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);

View File

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

View File

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

View File

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

View File

@@ -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

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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', () => {

View File

@@ -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);
}
}
}
}

View File

@@ -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);
});
}

View File

@@ -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

View File

@@ -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) {}

View File

@@ -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);
});
});
});

View File

@@ -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: {

View File

@@ -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);
});
});
});

View File

@@ -79,6 +79,5 @@ export default class LADTableView {
if (this._destroy) {
this._destroy();
}
this.component = null;
}
}

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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);

View File

@@ -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();
});

View File

@@ -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
};
}

View File

@@ -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