mirror of
https://github.com/nasa/openmct.git
synced 2025-06-25 02:29:24 +00:00
Compare commits
99 Commits
v4.0.0-pre
...
master
Author | SHA1 | Date | |
---|---|---|---|
f4637b8ac7 | |||
826c7134b5 | |||
fa1a45b6cd | |||
10bc8eb55d | |||
2e7fb94dd5 | |||
573bbb041e | |||
6a450a0e89 | |||
e5631c9f6c | |||
15b5d1405d | |||
0377788533 | |||
28dcff7e89 | |||
b191eb9d64 | |||
f8e4aba922 | |||
28f6987dd7 | |||
f3047093d6 | |||
34e57ef300 | |||
88e6557782 | |||
dbd4abebae | |||
50ca27e54f | |||
2955092c86 | |||
28b5d7c41c | |||
ecd120387c | |||
a6517bb33e | |||
1fde0d9e38 | |||
5be103ea72 | |||
d74e1b19b6 | |||
5bb6a18cd4 | |||
14b947c101 | |||
61b982ab99 | |||
ba4d8a428b | |||
ea9947cab5 | |||
2010f2e377 | |||
3241e9ba57 | |||
057a5f997c | |||
078cd341a5 | |||
518b55cf0f | |||
3e23dceb64 | |||
7f8b5e09e5 | |||
7c2bb16bfd | |||
890ddcac4e | |||
d8c5095ebb | |||
ccf7ed91af | |||
2b8673941a | |||
703186adf1 | |||
c43ef64733 | |||
f4cf9c756b | |||
4415fe7952 | |||
83e4a124e2 | |||
47f0b66c7e | |||
55c023d1eb | |||
37b2660f27 | |||
43cc963328 | |||
ad30a0e2d0 | |||
29f1956d1a | |||
c498f7d20c | |||
a8fbabe695 | |||
e792403788 | |||
de122b91c2 | |||
fa8efa858b | |||
26578e849d | |||
fccae3bd49 | |||
440474b2e3 | |||
21a4335c4e | |||
0413e77d8a | |||
4ee68cccd6 | |||
e3fcbe1a35 | |||
689f7cc815 | |||
1fae0a6ad5 | |||
762762945d | |||
c763937455 | |||
db808b4d54 | |||
6983148aba | |||
92e5cba6d3 | |||
0215a5b693 | |||
291c0997a6 | |||
393da7dc62 | |||
448750ca59 | |||
554f77c42f | |||
a5770817cc | |||
34b4091204 | |||
6360bc4b6c | |||
c354e1c2f1 | |||
eba6f0f505 | |||
017380bb6a | |||
810d580b18 | |||
977792fae8 | |||
a69e300f1c | |||
17bc6cb722 | |||
b3d3465734 | |||
fb0d74e87f | |||
a961d7e3bf | |||
5a06b51c5a | |||
ef8b353d01 | |||
6c5b925454 | |||
e91aba2e37 | |||
b18aa48141 | |||
d33da65dae | |||
e3adeb6a75 | |||
de3dad02b5 |
@ -5,7 +5,7 @@ orbs:
|
||||
executors:
|
||||
pw-focal-development:
|
||||
docker:
|
||||
- image: mcr.microsoft.com/playwright:v1.42.1-focal
|
||||
- image: mcr.microsoft.com/playwright:v1.48.1-focal
|
||||
environment:
|
||||
NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed
|
||||
PERCY_POSTINSTALL_BROWSER: 'true' # Needed to store the percy browser in cache deps
|
||||
@ -37,6 +37,29 @@ commands:
|
||||
ls -latR >> /tmp/artifacts/dir.txt
|
||||
- store_artifacts:
|
||||
path: /tmp/artifacts/
|
||||
download_verify_codecov_cli:
|
||||
description: 'Download and verify Codecov CLI'
|
||||
steps:
|
||||
- run:
|
||||
name: Download and verify Codecov CLI
|
||||
command: |
|
||||
# Download Codecov CLI
|
||||
curl -Os https://cli.codecov.io/latest/linux/codecov
|
||||
|
||||
# Import Codecov's GPG key
|
||||
curl https://keybase.io/codecovsecurity/pgp_keys.asc | gpg --no-default-keyring --keyring trustedkeys.gpg --import
|
||||
|
||||
# Download and verify the SHA256SUM and its signature
|
||||
curl -Os https://cli.codecov.io/latest/linux/codecov.SHA256SUM
|
||||
curl -Os https://cli.codecov.io/latest/linux/codecov.SHA256SUM.sig
|
||||
gpgv codecov.SHA256SUM.sig codecov.SHA256SUM
|
||||
|
||||
# Verify the checksum
|
||||
shasum -a 256 -c codecov.SHA256SUM
|
||||
|
||||
# Make the codecov executable
|
||||
[[ $EUID -ne 0 ]] && sudo chmod +x codecov || chmod +x codecov
|
||||
./codecov --help
|
||||
generate_e2e_code_cov_report:
|
||||
description: 'Generate e2e code coverage artifacts and publish to codecov.io. Needed to that we can ignore the exit code status of the npm run test'
|
||||
parameters:
|
||||
@ -44,7 +67,15 @@ commands:
|
||||
type: string
|
||||
steps:
|
||||
- run: npm run cov:e2e:report || true
|
||||
- run: npm run cov:e2e:<<parameters.suite>>:publish
|
||||
- download_verify_codecov_cli
|
||||
- run:
|
||||
name: Upload coverage report to Codecov
|
||||
command: |
|
||||
./codecov --verbose upload-process --disable-search \
|
||||
-t $CODECOV_TOKEN \
|
||||
-n 'e2e-<<parameters.suite>>'-${CIRCLE_WORKFLOW_ID} \
|
||||
-F e2e-<<parameters.suite>> \
|
||||
-f ./coverage/e2e/lcov.info
|
||||
jobs:
|
||||
npm-audit:
|
||||
parameters:
|
||||
@ -81,7 +112,15 @@ jobs:
|
||||
mkdir -p dist/reports/tests/
|
||||
TESTFILES=$(circleci tests glob "src/**/*Spec.js")
|
||||
echo "$TESTFILES" | circleci tests run --command="xargs npm run test" --verbose
|
||||
- run: npm run cov:unit:publish
|
||||
- download_verify_codecov_cli
|
||||
- run:
|
||||
name: Upload coverage report to Codecov
|
||||
command: |
|
||||
./codecov --verbose upload-process --disable-search \
|
||||
-t $CODECOV_TOKEN \
|
||||
-n 'unit-test'-${CIRCLE_WORKFLOW_ID} \
|
||||
-F unit \
|
||||
-f ./coverage/unit/lcov.info
|
||||
- store_test_results:
|
||||
path: dist/reports/tests/
|
||||
- store_artifacts:
|
||||
@ -93,10 +132,10 @@ jobs:
|
||||
- generate_and_store_version_and_filesystem_artifacts
|
||||
e2e-test:
|
||||
parameters:
|
||||
suite: #stable or full
|
||||
suite: #ci or full
|
||||
type: string
|
||||
executor: pw-focal-development
|
||||
parallelism: 7
|
||||
parallelism: 8
|
||||
steps:
|
||||
- build_and_install:
|
||||
node-version: lts/hydrogen
|
||||
@ -159,10 +198,10 @@ jobs:
|
||||
steps:
|
||||
- build_and_install:
|
||||
node-version: lts/hydrogen
|
||||
- run: npx playwright@1.42.1 install #Necessary for bare ubuntu machine
|
||||
- run: npx playwright@1.48.1 install #Necessary for bare ubuntu machine
|
||||
- run: |
|
||||
export $(cat src/plugins/persistence/couch/.env.ci | xargs)
|
||||
docker-compose -f src/plugins/persistence/couch/couchdb-compose.yaml up --detach
|
||||
docker compose -f src/plugins/persistence/couch/couchdb-compose.yaml up --detach
|
||||
sleep 3
|
||||
bash src/plugins/persistence/couch/setup-couchdb.sh
|
||||
- run: sh src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh #Replace LocalStorage Plugin with CouchDB
|
||||
@ -230,7 +269,7 @@ jobs:
|
||||
steps:
|
||||
- build_and_install:
|
||||
node-version: lts/iron
|
||||
- run: npm run test:e2e:visual:<<parameters.suite>>
|
||||
- run: SHARD="$((${CIRCLE_NODE_INDEX}+1))"; npm run test:e2e:visual:<<parameters.suite>> -- --shard=${SHARD}/${CIRCLE_NODE_TOTAL}
|
||||
- store_test_results:
|
||||
path: test-results/results.xml
|
||||
- store_artifacts:
|
||||
@ -247,24 +286,26 @@ workflows:
|
||||
overall-circleci-commit-status: #These jobs run on every commit
|
||||
jobs:
|
||||
- lint:
|
||||
name: node20-lint
|
||||
node-version: lts/iron
|
||||
name: node22-lint
|
||||
node-version: '22'
|
||||
- unit-test:
|
||||
name: node18-chrome
|
||||
node-version: lts/hydrogen
|
||||
- e2e-test:
|
||||
name: e2e-stable
|
||||
suite: stable
|
||||
name: e2e-ci
|
||||
suite: ci
|
||||
- e2e-mobile
|
||||
- visual-a11y:
|
||||
name: visual-a11y-ci
|
||||
suite: ci
|
||||
- perf-test
|
||||
- mem-test
|
||||
|
||||
the-nightly: #These jobs do not run on PRs, but against master at night
|
||||
jobs:
|
||||
- unit-test:
|
||||
name: node20-chrome-nightly
|
||||
node-version: lts/iron
|
||||
name: node22-chrome-nightly
|
||||
node-version: '22'
|
||||
- unit-test:
|
||||
name: node18-chrome
|
||||
node-version: lts/hydrogen
|
||||
|
24
.cspell.json
24
.cspell.json
@ -7,24 +7,18 @@
|
||||
"minmax",
|
||||
"openmct",
|
||||
"datasources",
|
||||
"recieved",
|
||||
"evalute",
|
||||
"Sinewave",
|
||||
"deregistration",
|
||||
"unregisters",
|
||||
"configutation",
|
||||
"configuation",
|
||||
"codecov",
|
||||
"carryforward",
|
||||
"Chacon",
|
||||
"Straub",
|
||||
"OWASP",
|
||||
"Testathon",
|
||||
"exploratorily",
|
||||
"Testathons",
|
||||
"testathon",
|
||||
"npmjs",
|
||||
"publishj",
|
||||
"treeitem",
|
||||
"timespan",
|
||||
"Timespan",
|
||||
@ -41,14 +35,10 @@
|
||||
"faultname",
|
||||
"gantt",
|
||||
"sharded",
|
||||
"perfromance",
|
||||
"MMOC",
|
||||
"codegen",
|
||||
"Unfortuantely",
|
||||
"viewports",
|
||||
"updatesnapshots",
|
||||
"excercised",
|
||||
"Circel",
|
||||
"browsercontexts",
|
||||
"miminum",
|
||||
"testcase",
|
||||
@ -135,9 +125,7 @@
|
||||
"tortor",
|
||||
"faucibus",
|
||||
"euismod",
|
||||
"pratices",
|
||||
"pathing",
|
||||
"pases",
|
||||
"testcases",
|
||||
"Noneditable",
|
||||
"listitem",
|
||||
@ -206,16 +194,12 @@
|
||||
"unlisten",
|
||||
"symbolsfont",
|
||||
"ellipsize",
|
||||
"dismissable",
|
||||
"TIMESYSTEM",
|
||||
"Metadatas",
|
||||
"stalenes",
|
||||
"receieves",
|
||||
"unsub",
|
||||
"callbacktwo",
|
||||
"unsubscribetwo",
|
||||
"telem",
|
||||
"Telemetery",
|
||||
"unemitted",
|
||||
"granually",
|
||||
"timesystem",
|
||||
@ -457,7 +441,6 @@
|
||||
"Userand",
|
||||
"Userbefore",
|
||||
"brdr",
|
||||
"pushs",
|
||||
"ALPH",
|
||||
"Recents",
|
||||
"Qbert",
|
||||
@ -497,7 +480,12 @@
|
||||
"checksnapshots",
|
||||
"specced",
|
||||
"composables",
|
||||
"countup"
|
||||
"countup",
|
||||
"darkmatter",
|
||||
"Undeletes",
|
||||
"SSSZ",
|
||||
"pageerror",
|
||||
"annotatable"
|
||||
],
|
||||
"dictionaries": ["npm", "softwareTerms", "node", "html", "css", "bash", "en_US", "en-gb", "misc"],
|
||||
"ignorePaths": [
|
||||
|
@ -5,12 +5,16 @@ const config = {
|
||||
browser: true,
|
||||
es2024: true,
|
||||
jasmine: true,
|
||||
node: true,
|
||||
worker: true,
|
||||
serviceworker: true
|
||||
amd: true,
|
||||
node: true
|
||||
},
|
||||
globals: {
|
||||
_: 'readonly'
|
||||
_: 'readonly',
|
||||
__webpack_public_path__: 'writeable',
|
||||
__OPENMCT_VERSION__: 'readonly',
|
||||
__OPENMCT_BUILD_DATE__: 'readonly',
|
||||
__OPENMCT_REVISION__: 'readonly',
|
||||
__OPENMCT_BUILD_BRANCH__: 'readonly'
|
||||
},
|
||||
plugins: ['prettier', 'unicorn', 'simple-import-sort'],
|
||||
extends: [
|
||||
@ -39,6 +43,7 @@ const config = {
|
||||
'vue/no-deprecated-events-api': 'warn',
|
||||
'vue/no-v-for-template-key': 'off',
|
||||
'vue/no-v-for-template-key-on-child': 'error',
|
||||
'vue/component-name-in-template-casing': ['error', 'PascalCase'],
|
||||
'prettier/prettier': 'error',
|
||||
'you-dont-need-lodash-underscore/omit': 'off',
|
||||
'you-dont-need-lodash-underscore/throttle': 'off',
|
||||
|
4
.github/codeql/codeql-config.yml
vendored
4
.github/codeql/codeql-config.yml
vendored
@ -1 +1,5 @@
|
||||
name: 'Custom CodeQL config'
|
||||
|
||||
paths-ignore:
|
||||
# Ignore e2e tests and framework
|
||||
- e2e
|
||||
|
25
.github/workflows/e2e-couchdb.yml
vendored
25
.github/workflows/e2e-couchdb.yml
vendored
@ -37,12 +37,12 @@ jobs:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- run: npx playwright@1.42.1 install
|
||||
- run: npx playwright@1.48.1 install
|
||||
|
||||
- name: Start CouchDB Docker Container and Init with Setup Scripts
|
||||
run: |
|
||||
export $(cat src/plugins/persistence/couch/.env.ci | xargs)
|
||||
docker-compose -f src/plugins/persistence/couch/couchdb-compose.yaml up --detach
|
||||
docker compose -f src/plugins/persistence/couch/couchdb-compose.yaml up --detach
|
||||
sleep 3
|
||||
bash src/plugins/persistence/couch/setup-couchdb.sh
|
||||
bash src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh
|
||||
@ -52,22 +52,33 @@ jobs:
|
||||
COMMIT_INFO_SHA: ${{github.event.pull_request.head.sha }}
|
||||
run: npm run test:e2e:couchdb
|
||||
|
||||
- name: Generate Code Coverage Report
|
||||
run: npm run cov:e2e:report
|
||||
|
||||
- name: Publish Results to Codecov.io
|
||||
env:
|
||||
SUPER_SECRET: ${{ secrets.CODECOV_TOKEN }}
|
||||
run: npm run cov:e2e:full:publish
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./coverage/e2e/lcov.info
|
||||
flags: e2e-full
|
||||
fail_ci_if_error: true
|
||||
verbose: true
|
||||
|
||||
- name: Archive test results
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: e2e-couchdb-test-results
|
||||
path: test-results
|
||||
overwrite: true
|
||||
|
||||
- name: Archive html test results
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: e2e-couchdb-html-test-results
|
||||
path: html-test-results
|
||||
overwrite: true
|
||||
|
||||
- name: Remove pr:e2e:couchdb label (if present)
|
||||
if: always()
|
||||
|
8
.github/workflows/e2e-flakefinder.yml
vendored
8
.github/workflows/e2e-flakefinder.yml
vendored
@ -30,17 +30,19 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
|
||||
- run: npx playwright@1.42.1 install
|
||||
- run: npx playwright@1.48.1 install
|
||||
- run: npm ci --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
|
||||
run: npm run test:e2e:ci -- --retries=0 --repeat-each=10 --max-failures=50
|
||||
|
||||
- name: Archive test results
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: e2e-flakefinder-test-results
|
||||
path: test-results
|
||||
overwrite: true
|
||||
|
||||
- name: Remove pr:e2e:flakefinder label (if present)
|
||||
if: always()
|
||||
|
6
.github/workflows/e2e-perf.yml
vendored
6
.github/workflows/e2e-perf.yml
vendored
@ -28,16 +28,18 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
|
||||
- run: npx playwright@1.42.1 install
|
||||
- run: npx playwright@1.48.1 install
|
||||
- run: npm ci --no-audit --progress=false
|
||||
- run: npm run test:perf:localhost
|
||||
- run: npm run test:perf:contract
|
||||
- run: npm run test:perf:memory
|
||||
- name: Archive test results
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: e2e-perf-test-results
|
||||
path: test-results
|
||||
overwrite: true
|
||||
|
||||
- name: Remove pr:e2e:perf label (if present)
|
||||
if: always()
|
||||
|
6
.github/workflows/e2e-pr.yml
vendored
6
.github/workflows/e2e-pr.yml
vendored
@ -33,7 +33,7 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
|
||||
- run: npx playwright@1.42.1 install
|
||||
- run: npx playwright@1.47.2 install
|
||||
- run: npx playwright install chrome-beta
|
||||
- run: npm ci --no-audit --progress=false
|
||||
- run: npm run test:e2e:full -- --max-failures=40
|
||||
@ -45,9 +45,11 @@ jobs:
|
||||
npm run cov:e2e:full:publish
|
||||
- name: Archive test results
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: e2e-pr-test-results
|
||||
path: test-results
|
||||
overwrite: true
|
||||
|
||||
- name: Remove pr:e2e label (if present)
|
||||
if: always()
|
||||
|
@ -22,3 +22,6 @@
|
||||
!index.html
|
||||
!openmct.js
|
||||
!SECURITY.md
|
||||
|
||||
# Dont include the example html
|
||||
dist/index.html
|
@ -19,7 +19,7 @@ import { merge } from 'webpack-merge';
|
||||
let gitRevision = 'error-retrieving-revision';
|
||||
let gitBranch = 'error-retrieving-branch';
|
||||
|
||||
const packageDefinition = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url)));
|
||||
const { version } = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url)));
|
||||
|
||||
try {
|
||||
gitRevision = execSync('git rev-parse HEAD').toString().trim();
|
||||
@ -49,7 +49,8 @@ const config = {
|
||||
couchDBChangesFeed: './src/plugins/persistence/couch/CouchChangesFeed.js',
|
||||
inMemorySearchWorker: './src/api/objects/InMemorySearchWorker.js',
|
||||
espressoTheme: './src/plugins/themes/espresso-theme.scss',
|
||||
snowTheme: './src/plugins/themes/snow-theme.scss'
|
||||
snowTheme: './src/plugins/themes/snow-theme.scss',
|
||||
darkmatterTheme: './src/plugins/themes/darkmatter-theme.scss'
|
||||
},
|
||||
output: {
|
||||
globalObject: 'this',
|
||||
@ -69,7 +70,6 @@ const config = {
|
||||
'@': path.join(projectRootDir, 'src'),
|
||||
legacyRegistry: path.join(projectRootDir, 'src/legacyRegistry'),
|
||||
csv: 'comma-separated-values',
|
||||
EventEmitter: 'eventemitter3',
|
||||
bourbon: 'bourbon.scss',
|
||||
'plotly-basic': 'plotly.js-basic-dist-min',
|
||||
'plotly-gl2d': 'plotly.js-gl2d-dist-min',
|
||||
@ -84,7 +84,7 @@ const config = {
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
__OPENMCT_VERSION__: `'${packageDefinition.version}'`,
|
||||
__OPENMCT_VERSION__: `'${version}'`,
|
||||
__OPENMCT_BUILD_DATE__: `'${new Date()}'`,
|
||||
__OPENMCT_REVISION__: `'${gitRevision}'`,
|
||||
__OPENMCT_BUILD_BRANCH__: `'${gitBranch}'`,
|
||||
|
@ -6,7 +6,7 @@ information to pull requests.
|
||||
|
||||
import config from './webpack.dev.mjs';
|
||||
|
||||
config.devtool = 'source-map';
|
||||
config.devtool = 'inline-source-map';
|
||||
config.devServer.hot = false;
|
||||
|
||||
config.module.rules.push({
|
||||
|
@ -39,7 +39,7 @@ export default merge(common, {
|
||||
return shouldWrite;
|
||||
}
|
||||
},
|
||||
watchFiles: ['**/*.css'],
|
||||
watchFiles: ['src/**/*.css', 'example/**/*.css'],
|
||||
static: {
|
||||
directory: fileURLToPath(new URL('../dist', import.meta.url)),
|
||||
publicPath: '/dist',
|
||||
|
81
API.md
81
API.md
@ -31,6 +31,10 @@
|
||||
- [`latest` request strategy](#latest-request-strategy)
|
||||
- [`minmax` request strategy](#minmax-request-strategy)
|
||||
- [Telemetry Formats](#telemetry-formats)
|
||||
- [Built-in Formats](#built-in-formats)
|
||||
- [**Number Format (default):**](#number-format-default)
|
||||
- [**String Format**](#string-format)
|
||||
- [**Enum Format**](#enum-format)
|
||||
- [Registering Formats](#registering-formats)
|
||||
- [Telemetry Data](#telemetry-data)
|
||||
- [Telemetry Datums](#telemetry-datums)
|
||||
@ -59,6 +63,12 @@
|
||||
- [Custom Indicators](#custom-indicators)
|
||||
- [Priority API](#priority-api)
|
||||
- [Priority Types](#priority-types)
|
||||
- [User API](#user-api)
|
||||
- [Example](#example)
|
||||
- [Visibility-Based Rendering in View Providers](#visibility-based-rendering-in-view-providers)
|
||||
- [Overview](#overview)
|
||||
- [Implementing Visibility-Based Rendering](#implementing-visibility-based-rendering)
|
||||
- [Example](#example-1)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
@ -381,6 +391,7 @@ openmct.composition.addProvider({
|
||||
|
||||
The `addProvider` function accepts a Composition Provider object as its sole
|
||||
argument. A Composition Provider is a javascript object exposing two functions:
|
||||
|
||||
- `appliesTo`: A `function` that accepts a `domainObject` argument, and returns
|
||||
a `boolean` value indicating whether this composition provider applies to the
|
||||
given object.
|
||||
@ -618,9 +629,10 @@ interface Formatter {
|
||||
|
||||
Open MCT on its own defines a handful of built-in formats:
|
||||
|
||||
###### **Number Format (default):**
|
||||
###### **Number Format (default):**
|
||||
|
||||
Applied to data with `format: 'number'`
|
||||
|
||||
```js
|
||||
valueMetadata = {
|
||||
format: 'number'
|
||||
@ -635,15 +647,18 @@ interface NumberFormatter extends Formatter {
|
||||
validate: (value: any) => boolean;
|
||||
}
|
||||
```
|
||||
###### **String Format**:
|
||||
|
||||
###### **String Format**
|
||||
|
||||
Applied to data with `format: 'string'`
|
||||
|
||||
```js
|
||||
valueMetadata = {
|
||||
format: 'string'
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
```ts
|
||||
interface StringFormatter extends Formatter {
|
||||
parse: (value: any) => string;
|
||||
@ -652,8 +667,10 @@ interface StringFormatter extends Formatter {
|
||||
}
|
||||
```
|
||||
|
||||
###### **Enum Format**:
|
||||
###### **Enum Format**
|
||||
|
||||
Applied to data with `format: 'enum'`
|
||||
|
||||
```js
|
||||
valueMetadata = {
|
||||
format: 'enum',
|
||||
@ -676,6 +693,7 @@ valueMetadata = {
|
||||
|
||||
Creates a two-way mapping between enum string and value to be used in the `parse` and `format` methods.
|
||||
Ex:
|
||||
|
||||
- `formatter.parse('APPLE') === 1;`
|
||||
- `formatter.format(1) === 'APPLE';`
|
||||
|
||||
@ -691,7 +709,6 @@ interface EnumFormatter extends Formatter {
|
||||
|
||||
Formats implement the following interface (provided here as TypeScript for simplicity):
|
||||
|
||||
|
||||
Formats are registered with the Telemetry API using the `addFormat` function. eg.
|
||||
|
||||
```javascript
|
||||
@ -713,7 +730,7 @@ openmct.telemetry.addFormat({
|
||||
|
||||
A single telemetry point is considered a Datum, and is represented by a standard
|
||||
javascript object. Realtime subscriptions (obtained via **subscribe**) will
|
||||
invoke the supplied callback once for each telemetry datum recieved. Telemetry
|
||||
invoke the supplied callback once for each telemetry datum received. Telemetry
|
||||
requests (obtained via **request**) will return a promise for an array of
|
||||
telemetry datums.
|
||||
|
||||
@ -738,7 +755,7 @@ section.
|
||||
Limit evaluators allow a telemetry integrator to define which limits exist for a
|
||||
telemetry endpoint and how limits should be applied to telemetry from a given domain object.
|
||||
|
||||
A limit evaluator can implement the `evalute` method which is used to define how limits
|
||||
A limit evaluator can implement the `evaluate` method which is used to define how limits
|
||||
should be applied to telemetry and the `getLimits` method which is used to specify
|
||||
what the limit values are for different limit levels.
|
||||
|
||||
@ -763,8 +780,8 @@ state of the application, and emits events to inform listeners when the state ch
|
||||
|
||||
Because the data displayed tends to be time domain data, Open MCT must always
|
||||
have at least one time system installed and activated. When you download Open
|
||||
MCT, it will be pre-configured to use the UTC time system, which is installed and activated,
|
||||
along with other default plugins, in `index.html`. Installing and activating a time system
|
||||
MCT, it will be pre-configured to use the UTC time system, which is installed and activated,
|
||||
along with other default plugins, in `index.html`. Installing and activating a time system
|
||||
is simple, and is covered [in the next section](#defining-and-registering-time-systems).
|
||||
|
||||
### Time Systems and Bounds
|
||||
@ -817,7 +834,7 @@ numbers in UTC terrestrial time.
|
||||
#### Getting and Setting the Active Time System
|
||||
|
||||
Once registered, a time system can be activated by calling `setTimeSystem` with
|
||||
the timeSystem `key` or an instance of the time system. You can also specify
|
||||
the timeSystem `key` or an instance of the time system. You can also specify
|
||||
valid [bounds](#time-bounds) for the timeSystem.
|
||||
|
||||
```javascript
|
||||
@ -841,10 +858,9 @@ Setting the active time system will trigger a [`'timeSystemChanged'`](#time-even
|
||||
event. If you supplied bounds, a [`'boundsChanged'`](#time-events) event will be triggered afterwards with your newly supplied bounds.
|
||||
|
||||
> ⚠️ **Deprecated**
|
||||
>
|
||||
> - The method `timeSystem()` is deprecated. Please use `getTimeSystem()` and `setTimeSystem()` as a replacement.
|
||||
|
||||
|
||||
|
||||
#### Time Bounds
|
||||
|
||||
The TimeAPI provides a getter and setter for querying and setting time bounds. Time
|
||||
@ -875,15 +891,16 @@ To respond to bounds change events, listen for the [`'boundsChanged'`](#time-eve
|
||||
event.
|
||||
|
||||
> ⚠️ **Deprecated**
|
||||
>
|
||||
> - The method `bounds()` is deprecated and will be removed in a future release. Please use `getBounds()` and `setBounds()` as a replacement.
|
||||
|
||||
### Clocks
|
||||
|
||||
The Time API requires a clock source which will cause the bounds to be updated
|
||||
automatically whenever the clock source "ticks". A clock is simply an object that
|
||||
supports registration of listeners and periodically invokes its listeners with a
|
||||
number. Open MCT supports registration of new clock sources that tick on almost
|
||||
anything. A tick occurs when the clock invokes callback functions registered by its
|
||||
The Time API requires a clock source which will cause the bounds to be updated
|
||||
automatically whenever the clock source "ticks". A clock is simply an object that
|
||||
supports registration of listeners and periodically invokes its listeners with a
|
||||
number. Open MCT supports registration of new clock sources that tick on almost
|
||||
anything. A tick occurs when the clock invokes callback functions registered by its
|
||||
listeners with a new time value.
|
||||
|
||||
An example of a clock source is the [LocalClock](https://github.com/nasa/openmct/blob/master/src/plugins/utcTimeSystem/LocalClock.js)
|
||||
@ -972,6 +989,7 @@ openmct.time.getClock();
|
||||
```
|
||||
|
||||
> ⚠️ **Deprecated**
|
||||
>
|
||||
> - The method `clock()` is deprecated and will be removed in a future release. Please use `getClock()` and `setClock()` as a replacement.
|
||||
|
||||
#### ⚠️ [DEPRECATED] Stopping an active clock
|
||||
@ -986,12 +1004,13 @@ openmct.time.stopClock();
|
||||
```
|
||||
|
||||
> ⚠️ **Deprecated**
|
||||
>
|
||||
> - The method `stopClock()` is deprecated and will be removed in a future release.
|
||||
|
||||
#### Clock Offsets
|
||||
|
||||
When in Real-time [mode](#time-modes), the time bounds of the application will be updated automatically each time the
|
||||
clock "ticks". The bounds are calculated based on the current value provided by
|
||||
When in Real-time [mode](#time-modes), the time bounds of the application will be updated automatically each time the
|
||||
clock "ticks". The bounds are calculated based on the current value provided by
|
||||
the active clock (via its `tick` event, or its `currentValue()` method).
|
||||
|
||||
Unlike bounds, which represent absolute time values, clock offsets represent
|
||||
@ -1026,13 +1045,14 @@ new bounds will be calculated based on the `currentValue()` of the active clock.
|
||||
Clock offsets are only relevant when in Real-time [mode](#time-modes).
|
||||
|
||||
> ⚠️ **Deprecated**
|
||||
>
|
||||
> - The method `clockOffsets()` is deprecated and will be removed in a future release. Please use `getClockOffsets()` and `setClockOffsets()` as a replacement.
|
||||
|
||||
### Time Modes
|
||||
|
||||
There are two time modes in Open MCT, "Fixed" and "Real-time". In Real-time mode the
|
||||
There are two time modes in Open MCT, "Fixed" and "Real-time". In Real-time mode the
|
||||
time bounds of the application will be updated automatically each time the clock "ticks".
|
||||
The bounds are calculated based on the current value provided by the active clock. In
|
||||
The bounds are calculated based on the current value provided by the active clock. In
|
||||
Fixed mode, the time bounds are set for a specified time range. When Open MCT is first
|
||||
initialized, it will be in Real-time mode.
|
||||
|
||||
@ -1120,6 +1140,7 @@ The events emitted by the Time API are:
|
||||
- `mode`: A string representation of the current time mode, either `'realtime'` or `'fixed'`.
|
||||
|
||||
> ⚠️ **Deprecated Events** (These will be removed in a future release):
|
||||
>
|
||||
> - `bounds` → `boundsChanged`
|
||||
> - `timeSystem` → `timeSystemChanged`
|
||||
> - `clock` → `clockChanged`
|
||||
@ -1180,7 +1201,7 @@ An example time conductor configuration is provided below. It sets up some
|
||||
default options for the [UTCTimeSystem](https://github.com/nasa/openmct/blob/master/src/plugins/utcTimeSystem/UTCTimeSystem.js)
|
||||
and [LocalTimeSystem](https://github.com/nasa/openmct/blob/master/src/plugins/localTimeSystem/LocalTimeSystem.js),
|
||||
in both fixed mode, and for the [LocalClock](https://github.com/nasa/openmct/blob/master/src/plugins/utcTimeSystem/LocalClock.js)
|
||||
source. In this configutation, the local clock supports both the UTCTimeSystem
|
||||
source. In this configuration, the local clock supports both the UTCTimeSystem
|
||||
and LocalTimeSystem. Configuration for fixed bounds mode is specified by omitting
|
||||
a clock key.
|
||||
|
||||
@ -1190,7 +1211,7 @@ const ONE_MINUTE = 60 * 1000;
|
||||
|
||||
openmct.install(openmct.plugins.Conductor({
|
||||
menuOptions: [
|
||||
// 'Fixed' bounds mode configuation for the UTCTimeSystem
|
||||
// 'Fixed' bounds mode configuration for the UTCTimeSystem
|
||||
{
|
||||
timeSystem: 'utc',
|
||||
bounds: {start: Date.now() - 30 * ONE_MINUTE, end: Date.now()},
|
||||
@ -1262,7 +1283,7 @@ Returns the currently set text as a `string`.
|
||||
[the built-in glyphs](https://nasa.github.io/openmct/style-guide/#/browse/styleguide:home/glyphs?view=styleguide.glyphs)
|
||||
may be used here, or a custom CSS class can be provided. Returns the currently defined CSS
|
||||
class as a `string`.
|
||||
- `.statusClass([className])`: Gets or sets the CSS class used to determine status. Accepts an __optional__
|
||||
- `.statusClass([className])`: Gets or sets the CSS class used to determine status. Accepts an **optional**
|
||||
`string` parameter to be used to set a status class applied to the indicator. May be used to apply
|
||||
different colors to indicate status.
|
||||
|
||||
@ -1290,9 +1311,11 @@ Open MCT provides some built-in priority values that can be used in the applicat
|
||||
|
||||
Currently, the Open MCT Priority API provides (type: numeric value):
|
||||
|
||||
- HIGHEST: Infinity
|
||||
- HIGH: 1000
|
||||
- Default: 0
|
||||
- LOW: -1000
|
||||
- LOWEST: -Infinity
|
||||
|
||||
View provider Example:
|
||||
|
||||
@ -1312,7 +1335,7 @@ can be used to manage user information and roles.
|
||||
|
||||
### Example
|
||||
|
||||
Open MCT provides an example [user](example/exampleUser/exampleUserCreator.js) and [user provider](example/exampleUser/ExampleUserProvider.js) which
|
||||
Open MCT provides an example [user](example/exampleUser/exampleUserCreator.js) and [user provider](example/exampleUser/ExampleUserProvider.js) which
|
||||
can be used as a starting point for creating a custom user provider.
|
||||
|
||||
## Visibility-Based Rendering in View Providers
|
||||
@ -1335,10 +1358,10 @@ Here’s the signature for the show function:
|
||||
|
||||
`show(element, isEditing, viewOptions)`
|
||||
|
||||
* `element` (HTMLElement) - The DOM element where the view should be rendered.
|
||||
* `isEditing` (boolean) - Indicates whether the view is in editing mode.
|
||||
* `viewOptions` (Object) - An object with configuration options for the view, including:
|
||||
* `renderWhenVisible` (Function) - This function wraps the `requestAnimationFrame` and only triggers the provided render logic when the view is visible in the viewport.
|
||||
- `element` (HTMLElement) - The DOM element where the view should be rendered.
|
||||
- `isEditing` (boolean) - Indicates whether the view is in editing mode.
|
||||
- `viewOptions` (Object) - An object with configuration options for the view, including:
|
||||
- `renderWhenVisible` (Function) - This function wraps the `requestAnimationFrame` and only triggers the provided render logic when the view is visible in the viewport.
|
||||
|
||||
### Example
|
||||
|
||||
@ -1367,8 +1390,6 @@ const myViewProvider = {
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
Note that `renderWhenVisible` defers rendering while the view is not visible and caters to the latest execution call. This provides responsiveness for dynamic content while ensuring performance optimizations.
|
||||
|
||||
Ensure your view logic is prepared to handle potentially multiple deferrals if using this API, as only the last call to renderWhenVisible will be queued for execution upon the view becoming visible.
|
||||
|
||||
|
112
README.md
112
README.md
@ -5,12 +5,10 @@ Open MCT (Open Mission Control Technologies) is a next-generation mission contro
|
||||
> [!NOTE]
|
||||
> Please visit our [Official Site](https://nasa.github.io/openmct/) and [Getting Started Guide](https://nasa.github.io/openmct/getting-started/)
|
||||
|
||||
|
||||
Once you've created something amazing with Open MCT, showcase your work in our GitHub Discussions [Show and Tell](https://github.com/nasa/openmct/discussions/categories/show-and-tell) section. We love seeing unique and wonderful implementations of Open MCT!
|
||||
|
||||

|
||||
|
||||
|
||||
## Building and Running Open MCT Locally
|
||||
|
||||
Building and running Open MCT in your local dev environment is very easy. Be sure you have [Git](https://git-scm.com/downloads) and [Node.js](https://nodejs.org/) installed, then follow the directions below. Need additional information? Check out the [Getting Started](https://nasa.github.io/openmct/getting-started/) page on our website.
|
||||
@ -18,19 +16,19 @@ Building and running Open MCT in your local dev environment is very easy. Be sur
|
||||
|
||||
1. Clone the source code:
|
||||
|
||||
```
|
||||
```sh
|
||||
git clone https://github.com/nasa/openmct.git
|
||||
```
|
||||
|
||||
2. (Optional) Install the correct node version using [nvm](https://github.com/nvm-sh/nvm):
|
||||
|
||||
```
|
||||
```sh
|
||||
nvm install
|
||||
```
|
||||
|
||||
3. Install development dependencies (Note: Check the `package.json` engine for our tested and supported node versions):
|
||||
3. Install development dependencies (Note: Check the `package.json` engine for our tested and supported node versions):
|
||||
|
||||
```
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
@ -57,9 +55,9 @@ our documentation.
|
||||
|
||||
> [!NOTE]
|
||||
> We want Open MCT to be as easy to use, install, run, and develop for as
|
||||
> possible, and your feedback will help us get there!
|
||||
> Feedback can be provided via [GitHub issues](https://github.com/nasa/openmct/issues/new/choose),
|
||||
> [Starting a GitHub Discussion](https://github.com/nasa/openmct/discussions),
|
||||
> possible, and your feedback will help us get there!
|
||||
> Feedback can be provided via [GitHub issues](https://github.com/nasa/openmct/issues/new/choose),
|
||||
> [Starting a GitHub Discussion](https://github.com/nasa/openmct/discussions),
|
||||
> or by emailing us at [arc-dl-openmct@mail.nasa.gov](mailto:arc-dl-openmct@mail.nasa.gov).
|
||||
|
||||
## Developing Applications With Open MCT
|
||||
@ -68,28 +66,29 @@ For more on developing with Open MCT, see our documentation for a guide on [Deve
|
||||
|
||||
## Compatibility
|
||||
|
||||
This is a fast moving project and we do our best to test and support the widest possible range of browsers, operating systems, and nodejs APIs. We have a published list of support available in our package.json's `browserslist` key.
|
||||
This is a fast moving project and we do our best to test and support the widest possible range of browsers, operating systems, and NodeJS APIs. We have a published list of support available in our package.json's `browserslist` key.
|
||||
|
||||
The project uses `nvm` to ensure the node and npm version used, is coherent in all projects. Install nvm (non-windows), [here](https://github.com/nvm-sh/nvm) or the windows equivalent [here](https://github.com/coreybutler/nvm-windows)
|
||||
The project utilizes `nvm` to maintain consistent node and npm versions across all projects. For UNIX, MacOS, Windows WSL, and other POSIX-compliant shell environments, click [here](https://github.com/nvm-sh/nvm). For Windows, check out [nvm-windows](https://github.com/coreybutler/nvm-windows).
|
||||
|
||||
If you encounter an issue with a particular browser, OS, or nodejs API, please file a [GitHub issue](https://github.com/nasa/openmct/issues/new/choose)
|
||||
If you encounter an issue with a particular browser, OS, or NodeJS API, please [file an issue](https://github.com/nasa/openmct/issues/new/choose).
|
||||
|
||||
## Plugins
|
||||
|
||||
Open MCT can be extended via plugins that make calls to the Open MCT API. A plugin is a group
|
||||
Open MCT can be extended via plugins that make calls to the Open MCT API. A plugin is a group
|
||||
of software components (including source code and resources such as images and HTML templates)
|
||||
that is intended to be added or removed as a single unit.
|
||||
|
||||
As well as providing an extension mechanism, most of the core Open MCT codebase is also
|
||||
As well as providing an extension mechanism, most of the core Open MCT codebase is also
|
||||
written as plugins.
|
||||
|
||||
For information on writing plugins, please see [our API documentation](./API.md#plugins).
|
||||
|
||||
## Tests
|
||||
|
||||
Our automated test coverage comes in the form of unit, e2e, visual, performance, and security tests.
|
||||
Our automated test coverage comes in the form of unit, e2e, visual, performance, and security tests.
|
||||
|
||||
### Unit Tests
|
||||
|
||||
Unit Tests are written for [Jasmine](https://jasmine.github.io/api/edge/global)
|
||||
and run by [Karma](http://karma-runner.github.io). To run:
|
||||
|
||||
@ -101,24 +100,34 @@ in the `src` hierarchy. Full configuration details are found in
|
||||
alongside the units that they test; for example, `src/foo/Bar.js` would be
|
||||
tested by `src/foo/BarSpec.js`.
|
||||
|
||||
### e2e, Visual, and Performance tests
|
||||
The e2e, Visual, and Performance tests are written for playwright and run by playwright's new test runner [@playwright/test](https://playwright.dev/).
|
||||
### e2e, Visual, and Performance Testing
|
||||
|
||||
To run the e2e tests which are part of every commit:
|
||||
Our e2e (end-to-end), Visual, and Performance tests leverage the Playwright framework and are executed using Playwright's test runner, [@playwright/test](https://playwright.dev/).
|
||||
|
||||
`npm run test:e2e:stable`
|
||||
#### How to Run Tests
|
||||
|
||||
To run the visual test suite:
|
||||
- **e2e Tests**: These tests are run on every commit. To run the tests locally, use:
|
||||
|
||||
`npm run test:e2e:visual`
|
||||
```sh
|
||||
npm run test:e2e:ci
|
||||
```
|
||||
|
||||
To run the performance tests:
|
||||
- **Visual Tests**: For running the visual test suite, use:
|
||||
|
||||
`npm run test:perf`
|
||||
```sh
|
||||
npm run test:e2e:visual
|
||||
```
|
||||
|
||||
The test suite is configured to all tests located in `e2e/tests/` ending in `*.e2e.spec.js`. For more about the e2e test suite, please see the [README](./e2e/README.md)
|
||||
- **Performance Tests**: To initiate the performance tests, enter:
|
||||
|
||||
```sh
|
||||
npm run test:perf
|
||||
```
|
||||
|
||||
All tests are located within the `e2e/tests/` directory and are identified by the `*.e2e.spec.js` filename pattern. For more information about the e2e test suite, refer to the [README](./e2e/README.md).
|
||||
|
||||
### Security Tests
|
||||
|
||||
Each commit is analyzed for known security vulnerabilities using [CodeQL](https://codeql.github.com/docs/codeql-language-guides/codeql-library-for-javascript/). The list of CWE coverage items is available in the [CodeQL docs](https://codeql.github.com/codeql-query-help/javascript-cwe/). The CodeQL workflow is specified in the [CodeQL analysis file](./.github/workflows/codeql-analysis.yml) and the custom [CodeQL config](./.github/codeql/codeql-config.yml).
|
||||
|
||||
### Test Reporting and Code Coverage
|
||||
@ -129,60 +138,41 @@ Our code coverage is generated during the runtime of our unit, e2e, and visual t
|
||||
|
||||
For more on the specifics of our code coverage setup, [see](TESTING.md#code-coverage)
|
||||
|
||||
# Glossary
|
||||
## Glossary
|
||||
|
||||
Certain terms are used throughout Open MCT with consistent meanings
|
||||
or conventions. Any deviations from the below are issues and should be
|
||||
addressed (either by updating this glossary or changing code to reflect
|
||||
correct usage.) Other developer documentation, particularly in-line
|
||||
documentation, may presume an understanding of these terms.
|
||||
|
||||
* _plugin_: A plugin is a removable, reusable grouping of software elements.
|
||||
The application is composed of plugins.
|
||||
* _composition_: In the context of a domain object, this refers to the set of
|
||||
other domain objects that compose or are contained by that object. A domain
|
||||
object's composition is the set of domain objects that should appear
|
||||
immediately beneath it in a tree hierarchy. A domain object's composition is
|
||||
described in its model as an array of id's; its composition capability
|
||||
provides a means to retrieve the actual domain object instances associated
|
||||
with these identifiers asynchronously.
|
||||
* _description_: When used as an object property, this refers to the human-readable
|
||||
description of a thing; usually a single sentence or short paragraph.
|
||||
(Most often used in the context of extensions, domain
|
||||
object models, or other similar application-specific objects.)
|
||||
* _domain object_: A meaningful object to the user; a distinct thing in
|
||||
the work support by Open MCT. Anything that appears in the left-hand
|
||||
tree is a domain object.
|
||||
* _identifier_: A tuple consisting of a namespace and a key, which together uniquely
|
||||
identifies a domain object.
|
||||
* _model_: The persistent state associated with a domain object. A domain
|
||||
object's model is a JavaScript object which can be converted to JSON
|
||||
without losing information (that is, it contains no methods.)
|
||||
* _name_: When used as an object property, this refers to the human-readable
|
||||
name for a thing. (Most often used in the context of extensions, domain
|
||||
object models, or other similar application-specific objects.)
|
||||
* _navigation_: Refers to the current state of the application with respect
|
||||
to the user's expressed interest in a specific domain object; e.g. when
|
||||
a user clicks on a domain object in the tree, they are _navigating_ to
|
||||
it, and it is thereafter considered the _navigated_ object (until the
|
||||
user makes another such choice.)
|
||||
* _namespace_: A name used to identify a persistence store. A running open MCT
|
||||
application could potentially use multiple persistence stores, with the
|
||||
| Term | Definition |
|
||||
|---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| _plugin_ | A removable, reusable grouping of software elements. The application is composed of plugins. |
|
||||
| _composition_ | In the context of a domain object, this term refers to the set of other domain objects that compose or are contained by that object. A domain object's composition is the set of domain objects that should appear immediately beneath it in a tree hierarchy. It is described in its model as an array of ids, providing a means to asynchronously retrieve the actual domain object instances associated with these identifiers. |
|
||||
| _description_ | When used as an object property, this term refers to the human-readable description of a thing, usually a single sentence or short paragraph. It is most often used in the context of extensions, domain object models, or other similar application-specific objects. |
|
||||
| _domain object_ | A meaningful object to the user and a distinct thing in the work supported by Open MCT. Anything that appears in the left-hand tree is a domain object. |
|
||||
| _identifier_ | A tuple consisting of a namespace and a key, which together uniquely identifies a domain object. |
|
||||
| _model_ | The persistent state associated with a domain object. A domain object's model is a JavaScript object that can be converted to JSON without losing information, meaning it contains no methods. |
|
||||
| _name_ | When used as an object property, this term refers to the human-readable name for a thing. It is most often used in the context of extensions, domain object models, or other similar application-specific objects. |
|
||||
| _navigation_ | This term refers to the current state of the application with respect to the user's expressed interest in a specific domain object. For example, when a user clicks on a domain object in the tree, they are navigating to it, and it is thereafter considered the navigated object until the user makes another such choice. |
|
||||
| _namespace_ | A name used to identify a persistence store. A running Open MCT application could potentially use multiple persistence stores. |
|
||||
|
||||
## Open MCT v2.0.0
|
||||
|
||||
Support for our legacy bundle-based API, and the libraries that it was built on (like Angular 1.x), have now been removed entirely from this repository.
|
||||
|
||||
For now if you have an Open MCT application that makes use of the legacy API, [a plugin](https://github.com/nasa/openmct-legacy-plugin) is provided that bootstraps the legacy bundling mechanism and API. This plugin will not be maintained over the long term however, and the legacy support plugin will not be tested for compatibility with future versions of Open MCT. It is provided for convenience only.
|
||||
|
||||
### How do I know if I am using legacy API?
|
||||
|
||||
You might still be using legacy API if your source code
|
||||
|
||||
* Contains files named bundle.js, or bundle.json,
|
||||
* Makes calls to `openmct.$injector()`, or `openmct.$angular`,
|
||||
* Makes calls to `openmct.legacyRegistry`, `openmct.legacyExtension`, or `openmct.legacyBundle`.
|
||||
|
||||
- Contains files named bundle.js, or bundle.json,
|
||||
- Makes calls to `openmct.$injector()`, or `openmct.$angular`,
|
||||
- Makes calls to `openmct.legacyRegistry`, `openmct.legacyExtension`, or `openmct.legacyBundle`.
|
||||
|
||||
### What should I do if I am using legacy API?
|
||||
|
||||
Please refer to [the modern Open MCT API](https://nasa.github.io/openmct/documentation/). Post any questions to the [Discussions section](https://github.com/nasa/openmct/discussions) of the Open MCT GitHub repository.
|
||||
|
||||
## Related Repos
|
||||
|
@ -66,8 +66,8 @@ The e2e line coverage is a bit more complex than the karma implementation. This
|
||||
1. Each e2e suite will start webpack with the ```npm run start:coverage``` command with config `webpack.coverage.mjs` and the `babel-plugin-istanbul` plugin to generate code coverage during e2e test execution using our custom [baseFixture](./baseFixtures.js).
|
||||
1. During testcase execution, each e2e shard will generate its piece of the larger coverage suite. **This coverage file is not merged**. The raw coverage file is stored in a `.nyc_report` directory.
|
||||
1. [nyc](https://github.com/istanbuljs/nyc) converts this directory into a `lcov` file with the following command `npm run cov:e2e:report`
|
||||
1. Most of the tests are run in the '@stable' configuration and focus on chrome/ubuntu at a single resolution. This coverage is published to codecov with `npm run cov:e2e:stable:publish`.
|
||||
1. The rest of our coverage only appears when run against `@unstable` tests, persistent datastore (couchdb), non-ubuntu machines, and non-chrome browsers with the `npm run cov:e2e:full:publish` flag. Since this happens about once a day, we have leveraged codecov.io's carryforward flag to report on lines covered outside of each commit on an individual PR.
|
||||
1. Most of the tests focus on chrome/ubuntu at a single resolution. This coverage is published to codecov with `npm run cov:e2e:ci:publish`.
|
||||
1. The rest of our coverage only appears when run against persistent datastore (couchdb), non-ubuntu machines, and non-chrome browsers with the `npm run cov:e2e:full:publish` flag. Since this happens about once a day, we have leveraged codecov.io's carryforward flag to report on lines covered outside of each commit on an individual PR.
|
||||
|
||||
|
||||
### Limitations in our code coverage reporting
|
||||
|
10
codecov.yml
10
codecov.yml
@ -1,6 +1,10 @@
|
||||
codecov:
|
||||
require_ci_to_pass: false #This setting will update the bot regardless of whether or not tests pass
|
||||
|
||||
# Disabling annotations for now. They are incorrectly labelling lines as lacking coverage when they are in fact covered by tests.
|
||||
github_checks:
|
||||
annotations: false
|
||||
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
@ -11,18 +15,18 @@ coverage:
|
||||
informational: true
|
||||
precision: 2
|
||||
round: down
|
||||
range: '66...100'
|
||||
range: "66...100"
|
||||
|
||||
flags:
|
||||
unit:
|
||||
carryforward: false
|
||||
e2e-stable:
|
||||
e2e-ci:
|
||||
carryforward: false
|
||||
e2e-full:
|
||||
carryforward: true
|
||||
|
||||
comment:
|
||||
layout: 'diff,flags,files,footer'
|
||||
layout: "diff,flags,files,footer"
|
||||
behavior: default
|
||||
require_changes: false
|
||||
show_carryforward_flags: true
|
||||
|
@ -14,13 +14,13 @@
|
||||
extend the platform are provided in the following documentation.
|
||||
|
||||
## Sections
|
||||
|
||||
* The [API](api/) uses inline documentation
|
||||
|
||||
* The [API](api/) uses inline documentation.
|
||||
using [TypeScript](https://www.typescriptlang.org) and some legacy [JSDoc](https://jsdoc.app/). It describes the JavaScript objects and
|
||||
functions that make up the software platform.
|
||||
|
||||
* The [Development Process](process/) document describes the
|
||||
* The [Development Process](process/) document describes the
|
||||
Open MCT software development cycle.
|
||||
|
||||
* The [Tutorials](https://github.com/nasa/openmct-tutorial) give examples of extending the platform to add
|
||||
functionality, and integrate with data sources.
|
||||
* The [tutorial](https://github.com/nasa/openmct-tutorial) and [plugin template](https://github.com/nasa/openmct-hello) give examples of extending the platform to add
|
||||
functionality and integrate with data sources.
|
||||
|
@ -133,7 +133,7 @@ emphasis on testing.
|
||||
Multi-user testing, involving as many users as
|
||||
is feasible, plus development team. Open-ended; should verify
|
||||
completed work from this sprint using the sprint branch, test
|
||||
exploratorily for regressions, et cetera.
|
||||
exploratory for regressions, et cetera.
|
||||
* [__Long-Duration Test.__](testing/plan.md#long-duration-testing) A
|
||||
test to verify that the software remains
|
||||
stable after running for longer durations. May include some
|
||||
|
@ -132,7 +132,7 @@ numbers by the following process:
|
||||
4. Test the package before publishing by doing `npm publish --dry-run`
|
||||
if necessary.
|
||||
5. Publish the package to the npmjs registry (e.g. `npm publish --access public`)
|
||||
NOTE: Use the `--tag unstable` flag to the npm publishj if this is a prerelease.
|
||||
NOTE: Use the `--tag unstable` flag to the npm publish if this is a prerelease.
|
||||
6. Confirm the package has been published (e.g. `https://www.npmjs.com/package/openmct`)
|
||||
5. Update snapshot status in `package.json`
|
||||
1. Create a new branch off the `master` branch.
|
||||
|
@ -1,14 +1,36 @@
|
||||
/* eslint-disable no-undef */
|
||||
module.exports = {
|
||||
extends: ['plugin:playwright/playwright-test'],
|
||||
extends: ['plugin:playwright/recommended'],
|
||||
rules: {
|
||||
'playwright/max-nested-describe': ['error', { max: 1 }]
|
||||
'playwright/expect-expect': 'off'
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['tests/visual/*.spec.js'],
|
||||
//Apply Best Practices to externalFixtures and exampleTemplate.e2e.spec.js
|
||||
files: [
|
||||
'appActions.js',
|
||||
'baseFixtures.js',
|
||||
'pluginFixtures.js',
|
||||
'**/exampleTemplate.e2e.spec.js'
|
||||
],
|
||||
rules: {
|
||||
'playwright/no-wait-for-timeout': 'off'
|
||||
'playwright/no-raw-locators': 'error',
|
||||
'playwright/no-nth-methods': 'error',
|
||||
'playwright/no-get-by-title': 'error',
|
||||
'playwright/prefer-comparison-matcher': 'error'
|
||||
}
|
||||
},
|
||||
{
|
||||
// Disable no-raw-locators for .contract.perf.spec.js files until https://github.com/grafana/xk6-browser/issues/1226
|
||||
files: ['**/*.contract.perf.spec.js'],
|
||||
rules: {
|
||||
'playwright/no-raw-locators': 'off'
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['**/*.visual.spec.js'],
|
||||
rules: {
|
||||
'playwright/no-networkidle': 'off' //https://github.com/nasa/openmct/issues/7549
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -30,4 +30,15 @@ snapshot:
|
||||
.gl-plot-chart-area{
|
||||
opacity: 0 !important;
|
||||
}
|
||||
|
||||
/* SWG Time values on plot */
|
||||
.gl-plot-x{
|
||||
opacity: 0 !important;
|
||||
}
|
||||
/* Notification Time in modal */
|
||||
.c-ne__time{
|
||||
opacity: 0 !important;
|
||||
}
|
||||
/* Snapshot name with embedded time */
|
||||
.l-browse-bar__snapshot-datetime{
|
||||
opacity: 0 !important;
|
||||
}
|
||||
|
@ -29,4 +29,16 @@ snapshot:
|
||||
/* Chart Area for Plots */
|
||||
.gl-plot-chart-area{
|
||||
opacity: 0 !important;
|
||||
}
|
||||
}
|
||||
/* SWG Time values on plot */
|
||||
.gl-plot-x{
|
||||
opacity: 0 !important;
|
||||
}
|
||||
/* Notification Time in modal */
|
||||
.c-ne__time{
|
||||
opacity: 0 !important;
|
||||
}
|
||||
/* Snapshot name with embedded time */
|
||||
.l-browse-bar__snapshot-datetime{
|
||||
opacity: 0 !important;
|
||||
}
|
||||
|
@ -181,7 +181,7 @@ In addition to the explicit definition of performance tests, we also ensure that
|
||||
|
||||
### File Structure
|
||||
|
||||
Our file structure follows the type of type of testing being excercised at the e2e layer and files containing test suites which matcher application behavior or our `src` and `example` layout. This area is not well refined as we figure out what works best for closed source and downstream projects. This may change altogether if we move `e2e` to it's own npm package.
|
||||
Our file structure follows the type of type of testing being exercised at the e2e layer and files containing test suites which matcher application behavior or our `src` and `example` layout. This area is not well refined as we figure out what works best for closed source and downstream projects. This may change altogether if we move `e2e` to it's own npm package.
|
||||
|
||||
|File Path|Description|
|
||||
|:-:|-|
|
||||
@ -225,18 +225,17 @@ Current list of test tags:
|
||||
|:-:|-|
|
||||
|`@mobile` | Test case or test suite is compatible with Playwright's iPad support and Open MCT's read-only mobile view (i.e. no create button).|
|
||||
|`@a11y` | Test case or test suite to execute playwright-axe accessibility checks and generate a11y reports.|
|
||||
|`@gds` | Denotes a GDS Test Case used in the VIPER Mission.|
|
||||
|`@addInit` | Initializes the browser with an injected and artificial state. Useful for loading non-default plugins. Likely will not work outside of `npm start`.|
|
||||
|`@localStorage` | Captures or generates session storage to manipulate browser state. Useful for excluding in tests which require a persistent backend (i.e. CouchDB). See [note](#utilizing-localstorage)|
|
||||
|`@snapshot` | Uses Playwright's snapshot functionality to record a copy of the DOM for direct comparison. Must be run inside of the playwright container.|
|
||||
|`@unstable` | A new test or test which is known to be flaky.|
|
||||
|`@2p` | Indicates that multiple users are involved, or multiple tabs/pages are used. Useful for testing multi-user interactivity.|
|
||||
|`@generatedata` | Indicates that a test is used to generate testdata or test the generated test data. Usually to be associated with localstorage, but this may grow over time.|
|
||||
|`@clock` | A test which modifies the clock. These have expanded out of the visual tests and into the functional tests.
|
||||
|`@framework` | A test for open mct e2e capabilities. This is primarily to ensure we don't break projects which depend on sourcing this project's fixtures like appActions.js.
|
||||
|
||||
### Continuous Integration
|
||||
|
||||
The cheapest time to catch a bug is pre-merge. Unfortuantely, this is the most expensive time to run all of the tests since each merge event can consist of hundreds of commits. For this reason, we're selective in _what we run_ as much as _when we run it_.
|
||||
The cheapest time to catch a bug is pre-merge. Unfortunately, this is the most expensive time to run all of the tests since each merge event can consist of hundreds of commits. For this reason, we're selective in _what we run_ as much as _when we run it_.
|
||||
|
||||
We leverage CircleCI to run tests against each commit and inject the Test Reports which are generated by Playwright so that they team can keep track of flaky and [historical test trends](https://app.circleci.com/insights/github/nasa/openmct/workflows/overall-circleci-commit-status/tests?branch=master&reporting-window=last-30-days)
|
||||
|
||||
@ -248,7 +247,7 @@ Our CI environment consists of 3 main modes of operation:
|
||||
|
||||
CircleCI
|
||||
|
||||
- Stable e2e tests against ubuntu and chrome
|
||||
- e2e tests against ubuntu and chrome
|
||||
- Performance tests against ubuntu and chrome
|
||||
- e2e tests are linted
|
||||
- Visual and a11y tests are run in a single resolution on the default `espresso` theme
|
||||
@ -281,24 +280,12 @@ Playwright has native support for semi-intelligent sharding. Read about it [here
|
||||
|
||||
We will be adjusting the parallelization of the Per-Commit tests to keep below the 5 minute total runtime threshold.
|
||||
|
||||
In addition to the Parallelization of Test Runners (Sharding), we're also running two concurrent threads on every Shard. This is the functional limit of what CircelCI Agents can support from a memory and CPU resource constraint.
|
||||
In addition to the Parallelization of Test Runners (Sharding), we're also running two concurrent threads on every Shard. This is the functional limit of what CircleCI Agents can support from a memory and CPU resource constraint.
|
||||
|
||||
So for every commit, Playwright is effectively running 4 x 2 concurrent browsercontexts to keep the overall runtime to a miminum.
|
||||
|
||||
At the same time, we don't want to waste CI resources on parallel runs, so we've configured each shard to fail after 5 test failures. Test failure logs are recorded and stored to allow fast triage.
|
||||
|
||||
#### Test Promotion
|
||||
|
||||
In order to maintain fast and reliable feedback, tests go through a promotion process. All new test cases or test suites must be labeled with the `@unstable` annotation. The Open MCT dev team runs these unstable tests in our private repos to ensure they work downstream and are reliable.
|
||||
|
||||
- To run the stable tests, use the `npm run test:e2e:stable` command.
|
||||
- To run the new and flaky tests, use the `npm run test:e2e:unstable` command.
|
||||
|
||||
A testcase and testsuite are to be unmarked as @unstable when:
|
||||
|
||||
1. They run as part of "full" run 5 times without failure.
|
||||
2. They've been by a Open MCT Developer 5 times in the closed source repo without failure.
|
||||
|
||||
### Cross-browser and Cross-operating system
|
||||
|
||||
#### **What's supported:**
|
||||
@ -380,8 +367,7 @@ By adhering to this principle, we can create tests that are both robust and refl
|
||||
1. Avoid creating locator aliases. This likely means that you're compensating for a bad locator. Improve the application instead.
|
||||
1. Leverage `await page.goto('./', { waitUntil: 'domcontentloaded' });` instead of `{ waitUntil: 'networkidle' }`. Tests run against deployments with websockets often have issues with the networkidle detection.
|
||||
|
||||
#### How to make tests faster and more resilient
|
||||
|
||||
#### How to make tests faster and more resilient to application changes
|
||||
1. Avoid app interaction when possible. The best way of doing this is to navigate directly by URL:
|
||||
|
||||
```js
|
||||
@ -396,6 +382,16 @@ By adhering to this principle, we can create tests that are both robust and refl
|
||||
- Initial navigation should _almost_ always use the `{ waitUntil: 'domcontentloaded' }` option.
|
||||
1. Avoid repeated setup to test a single assertion. Write longer tests with multiple soft assertions.
|
||||
This ensures that your changes will be picked up with large refactors.
|
||||
1. Use [user-facing locators](https://playwright.dev/docs/best-practices#use-locators) (Now a eslint rule!)
|
||||
|
||||
```js
|
||||
page.getByRole('button', { name: 'Create' } )
|
||||
```
|
||||
Instead of
|
||||
```js
|
||||
page.locator('.c-create-button')
|
||||
```
|
||||
Note: `page.locator()` can be used in performance tests as xk6-browser does not yet support the new `page.getBy` pattern and css lookups can be [1.5x faster](https://serpapi.com/blog/css-selectors-faster-than-getbyrole-playwright/)
|
||||
|
||||
##### Utilizing LocalStorage
|
||||
|
||||
@ -448,6 +444,7 @@ By adhering to this principle, we can create tests that are both robust and refl
|
||||
- Use Open MCT's fixed-time mode unless explicitly testing realtime clock
|
||||
- Employ the `createExampleTelemetryObject` appAction to source telemetry and specify a `name` to avoid autogenerated names.
|
||||
- Avoid creating objects with a time component like timers and clocks.
|
||||
- Utilize the playwright clock() API. See @clock Annotations for examples.
|
||||
|
||||
5. **Hide the Tree and Inspector**: Generally, your test will not require comparisons involving the tree and inspector. These aspects are covered in component-specific tests (explained below). To exclude them from the comparison by default, navigate to the root of the main view with the tree and inspector hidden:
|
||||
- `await page.goto('./#/browse/mine?hideTree=true&hideInspector=true')`
|
||||
@ -472,6 +469,7 @@ By adhering to this principle, we can create tests that are both robust and refl
|
||||
//Select from object
|
||||
await percySnapshot(page, `object selected (theme: ${theme})`)
|
||||
```
|
||||
8. **Use `networkidle` to wait for network requests to complete**: This is necessary to ensure that all network requests have completed before taking a snapshot. This ensures that icons are loaded and other assets are available. https://github.com/nasa/openmct/issues/7549
|
||||
|
||||
#### How to write a great network test
|
||||
|
||||
@ -493,29 +491,25 @@ For best practices with regards to mocking network responses, see our [couchdb.e
|
||||
The following contains a list of tips and tricks which don't exactly fit into a FAQ or Best Practices doc.
|
||||
|
||||
- (Advanced) Overriding the Browser's Clock
|
||||
It is possible to override the browser's clock in order to control time-based elements. Since this can cause unwanted behavior (i.e. Tree not rendering), only use this sparingly. To do this, use the `overrideClock` fixture as such:
|
||||
It is possible to override the browser's clock in order to control time-based elements. Since this can cause unwanted behavior -- i.e. Tree not rendering -- only use this sparingly. Use the `page.clock()` API as such:
|
||||
|
||||
```js
|
||||
import { test, expect } from '../../pluginFixtures.js';
|
||||
|
||||
test.describe('foo test suite', () => {
|
||||
|
||||
// All subsequent tests in this suite will override the clock
|
||||
test.use({
|
||||
clockOptions: {
|
||||
now: 1732413600000, // A timestamp given as milliseconds since the epoch
|
||||
shouldAdvanceTime: true // Should the clock tick?
|
||||
}
|
||||
test.describe('foo test suite @clock', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
//Set clock time
|
||||
await page.clock.install({ time: MISSION_TIME });
|
||||
await page.clock.resume();
|
||||
//Navigate to page with new clock
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
});
|
||||
|
||||
test('bar test', async ({ page }) => {
|
||||
// ...
|
||||
test('bar here', async ({ page }) => {
|
||||
/// ...
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
More info and options for `overrideClock` can be found in [baseFixtures.js](baseFixtures.js)
|
||||
|
||||
- Working with multiple pages
|
||||
There are instances where multiple browser pages will needed to verify multi-page or multi-tab application behavior. Make sure to use the `@2p` annotation as well as name each page appropriately: i.e. `page1` and `page2` or `tab1` and `tab2` depending on the intended use case. Generally pages should be used unless testing `sharedWorker` code, specifically.
|
||||
|
||||
|
@ -35,7 +35,6 @@
|
||||
* @property {string} type the type of domain object to create (e.g.: "Sine Wave Generator").
|
||||
* @property {string} [name] the desired name of the created domain object.
|
||||
* @property {string | import('../src/api/objects/ObjectAPI').Identifier} [parent] the Identifier or uuid of the parent object.
|
||||
* @property {Object<string, string>} [customParameters] any additional parameters to be passed to the domain object's form. E.g. '[aria-label="Data Rate (hz)"]': {'0.1'}
|
||||
*/
|
||||
|
||||
/**
|
||||
@ -62,13 +61,17 @@ import { v4 as genUuid } from 'uuid';
|
||||
* This common function creates a domain object with the default options. It is the preferred way of creating objects
|
||||
* in the e2e suite when uninterested in properties of the objects themselves.
|
||||
*
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {CreateObjectOptions} options
|
||||
* @param {import('@playwright/test').Page} page - The Playwright page object.
|
||||
* @param {Object} options - Options for creating the domain object.
|
||||
* @param {string} options.type - The type of domain object to create (e.g., "Sine Wave Generator").
|
||||
* @param {string} [options.name] - The desired name of the created domain object.
|
||||
* @param {string | import('../src/api/objects/ObjectAPI').Identifier} [options.parent='mine'] - The Identifier or uuid of the parent object. Defaults to 'mine' folder
|
||||
* @returns {Promise<CreatedObjectInfo>} An object containing information about the newly created domain object.
|
||||
*/
|
||||
async function createDomainObjectWithDefaults(
|
||||
page,
|
||||
{ type, name, parent = 'mine', customParameters = {} }
|
||||
{ type, name, parent = 'mine' },
|
||||
additionalOptions = {}
|
||||
) {
|
||||
if (!name) {
|
||||
name = `${type}:${genUuid()}`;
|
||||
@ -78,40 +81,33 @@ async function createDomainObjectWithDefaults(
|
||||
|
||||
// Navigate to the parent object. This is necessary to create the object
|
||||
// in the correct location, such as a folder, layout, or plot.
|
||||
await page.goto(`${parentUrl}`);
|
||||
await page.goto(parentUrl);
|
||||
|
||||
//Click the Create button
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
// Click the Create button
|
||||
await page.getByRole('button', { name: 'Create', exact: true }).click();
|
||||
|
||||
// Click the object specified by 'type'
|
||||
await page.click(`li[role='menuitem']:text("${type}")`);
|
||||
// Click the object specified by 'type'-- case insensitive
|
||||
await page.getByRole('menuitem', { name: new RegExp(`^${type}$`, 'i') }).click();
|
||||
|
||||
// Modify the name input field of the domain object to accept 'name'
|
||||
const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]');
|
||||
await nameInput.fill('');
|
||||
await nameInput.fill(name);
|
||||
// Fill in the name of the object
|
||||
await page.getByLabel('Title', { exact: true }).fill('');
|
||||
await page.getByLabel('Title', { exact: true }).fill(name);
|
||||
|
||||
if (additionalOptions) {
|
||||
for (const [key, value] of Object.entries(additionalOptions)) {
|
||||
// eslint-disable-next-line playwright/no-raw-locators
|
||||
await page.locator(`#form-${key}`).fill(value);
|
||||
}
|
||||
}
|
||||
|
||||
if (page.testNotes) {
|
||||
// Fill the "Notes" section with information about the
|
||||
// currently running test and its project.
|
||||
const notesInput = page.locator('form[name="mctForm"] #notes-textarea');
|
||||
await notesInput.fill(page.testNotes);
|
||||
// eslint-disable-next-line playwright/no-raw-locators
|
||||
await page.locator('#notes-textarea').fill(page.testNotes);
|
||||
}
|
||||
|
||||
// If there are any further parameters, fill them in
|
||||
for (const [key, value] of Object.entries(customParameters)) {
|
||||
const input = page.locator(`form[name="mctForm"] ${key}`);
|
||||
await input.fill('');
|
||||
await input.fill(value);
|
||||
}
|
||||
|
||||
// Click OK button and wait for Navigate event
|
||||
await Promise.all([
|
||||
page.waitForLoadState(),
|
||||
await page.getByRole('button', { name: 'Save' }).click(),
|
||||
// Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
// Wait until the URL is updated
|
||||
await page.waitForURL(`**/${parent}/*`);
|
||||
@ -120,7 +116,7 @@ async function createDomainObjectWithDefaults(
|
||||
|
||||
if (await _isInEditMode(page, uuid)) {
|
||||
// Save (exit edit mode)
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.getByRole('button', { name: 'Save', exact: true }).click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
}
|
||||
|
||||
@ -151,61 +147,41 @@ async function createNotification(page, createNotificationOptions) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand an item in the tree by a given object name.
|
||||
* Create a Plan object from JSON with the provided options. Must be used with a json based plan.
|
||||
* Please check appActions.e2e.spec.js for an example of how to use this function.
|
||||
*
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} name
|
||||
*/
|
||||
async function expandTreePaneItemByName(page, name) {
|
||||
const treePane = page.getByRole('tree', {
|
||||
name: 'Main Tree'
|
||||
});
|
||||
const treeItem = treePane.locator(`role=treeitem[expanded=false][name=/${name}/]`);
|
||||
const expandTriangle = treeItem.locator('.c-disclosure-triangle');
|
||||
await expandTriangle.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Plan object from JSON with the provided options.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {*} options
|
||||
* @param {Object} json
|
||||
* @param {string | import('../src/api/objects/ObjectAPI').Identifier} [parent] the uuid or identifier of the parent object. Defaults to 'mine'
|
||||
* @returns {Promise<CreatedObjectInfo>} An object containing information about the newly created domain object.
|
||||
*/
|
||||
async function createPlanFromJSON(page, { name, json, parent = 'mine' }) {
|
||||
if (!name) {
|
||||
name = `Plan:${genUuid()}`;
|
||||
}
|
||||
|
||||
const parentUrl = await getHashUrlToDomainObject(page, parent);
|
||||
|
||||
// Navigate to the parent object. This is necessary to create the object
|
||||
// in the correct location, such as a folder, layout, or plot.
|
||||
await page.goto(`${parentUrl}`);
|
||||
|
||||
// Click the Create button
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
|
||||
// Click 'Plan' menu option
|
||||
await page.click(`li:text("Plan")`);
|
||||
await page.getByRole('menuitem', { name: 'Plan' }).click();
|
||||
|
||||
// Modify the name input field of the domain object to accept 'name'
|
||||
const nameInput = page.getByLabel('Title', { exact: true });
|
||||
await nameInput.fill('');
|
||||
await nameInput.fill(name);
|
||||
// Fill in the name of the object or generate a random one
|
||||
if (!name) {
|
||||
name = `Plan:${genUuid()}`;
|
||||
}
|
||||
await page.getByLabel('Title', { exact: true }).fill('');
|
||||
await page.getByLabel('Title', { exact: true }).fill(name);
|
||||
|
||||
// Upload buffer from memory
|
||||
await page.locator('input#fileElem').setInputFiles({
|
||||
await page.getByLabel('Select File...').setInputFiles({
|
||||
name: 'plan.txt',
|
||||
mimeType: 'text/plain',
|
||||
buffer: Buffer.from(JSON.stringify(json))
|
||||
});
|
||||
|
||||
// Click OK button and wait for Navigate event
|
||||
await Promise.all([
|
||||
page.waitForLoadState(),
|
||||
page.click('[aria-label="Save"]'),
|
||||
// Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
await page.getByLabel('Save').click();
|
||||
|
||||
// Wait until the URL is updated
|
||||
await page.waitForURL(`**/${parent}/*`);
|
||||
@ -233,10 +209,10 @@ async function createExampleTelemetryObject(page, parent = 'mine') {
|
||||
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
|
||||
await page.locator('li:has-text("Sine Wave Generator")').click();
|
||||
await page.getByRole('menuitem', { name: 'Sine Wave Generator' }).click();
|
||||
|
||||
const name = 'VIPER Rover Heading';
|
||||
await page.getByRole('dialog').locator('input[type="text"]').fill(name);
|
||||
await page.getByLabel('Title', { exact: true }).fill(name);
|
||||
|
||||
// Fill out the fields with default values
|
||||
await page.getByRole('spinbutton', { name: 'Period' }).fill('10');
|
||||
@ -263,7 +239,40 @@ async function createExampleTelemetryObject(page, parent = 'mine') {
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates directly to a given object url, in fixed time mode, with the given start and end bounds.
|
||||
* Create a Stable State Telemetry Object (State Generator) for use in visual tests
|
||||
* and tests against plotting telemetry (e.g. logPlot tests). This will change state every 2 seconds.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string | import('../src/api/objects/ObjectAPI').Identifier} [parent] the uuid or identifier of the parent object. Defaults to 'mine'
|
||||
* @returns {Promise<CreatedObjectInfo>} An object containing information about the telemetry object.
|
||||
*/
|
||||
async function createStableStateTelemetry(page, parent = 'mine') {
|
||||
const parentUrl = await getHashUrlToDomainObject(page, parent);
|
||||
|
||||
await page.goto(`${parentUrl}`);
|
||||
const createdObject = await createDomainObjectWithDefaults(page, {
|
||||
type: 'State Generator',
|
||||
name: 'Stable State Generator'
|
||||
});
|
||||
// edit the state generator to have a 1 second update rate
|
||||
await page.getByLabel('More actions').click();
|
||||
await page.getByRole('menuitem', { name: 'Edit Properties...' }).click();
|
||||
await page.getByLabel('State Duration (seconds)', { exact: true }).fill('2');
|
||||
await page.getByLabel('Save').click();
|
||||
// Wait until the URL is updated
|
||||
const uuid = await getFocusedObjectUuid(page);
|
||||
const url = await getHashUrlToDomainObject(page, uuid);
|
||||
|
||||
return {
|
||||
name: createdObject.name,
|
||||
uuid,
|
||||
url
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates directly to a given object url, in fixed time mode, with the given start and end bounds. Note: does not set
|
||||
* default view type.
|
||||
*
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} url The url to the domainObject
|
||||
* @param {string | number} start The starting time bound in milliseconds since epoch
|
||||
@ -276,22 +285,25 @@ async function navigateToObjectWithFixedTimeBounds(page, url, start, end) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the given `domainObject`'s context menu from the object tree.
|
||||
* Expands the path to the object and scrolls to it if necessary.
|
||||
* Navigates directly to a given object url, in real-time mode. Note: does not set
|
||||
* default view type.
|
||||
*
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} url the url to the object
|
||||
* @param {string} url The url to the domainObject
|
||||
* @param {string | number} start The start offset in milliseconds
|
||||
* @param {string | number} end The end offset in milliseconds
|
||||
*/
|
||||
async function openObjectTreeContextMenu(page, url) {
|
||||
await page.goto(url);
|
||||
await page.getByLabel('Show selected item in tree').click();
|
||||
await page.locator('.is-navigated-object').click({
|
||||
button: 'right'
|
||||
});
|
||||
async function navigateToObjectWithRealTime(page, url, start = '1800000', end = '30000') {
|
||||
await page.goto(
|
||||
`${url}?tc.mode=local&tc.startDelta=${start}&tc.endDelta=${end}&tc.timeSystem=utc`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Expands the entire object tree (every expandable tree item).
|
||||
* Expands the entire object tree (every expandable tree item). Can be used to
|
||||
* ensure that the tree is fully expanded before performing actions on objects.
|
||||
* Can be applied to either the main tree or the create modal tree.
|
||||
*
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {"Main Tree" | "Create Modal Tree"} [treeName="Main Tree"]
|
||||
*/
|
||||
@ -303,9 +315,10 @@ async function expandEntireTree(page, treeName = 'Main Tree') {
|
||||
.getByRole('treeitem', {
|
||||
expanded: false
|
||||
})
|
||||
.locator('span.c-disclosure-triangle.is-enabled');
|
||||
.getByLabel(/Expand/);
|
||||
|
||||
while ((await collapsedTreeItems.count()) > 0) {
|
||||
//eslint-disable-next-line playwright/no-nth-methods
|
||||
await collapsedTreeItems.nth(0).click();
|
||||
|
||||
// FIXME: Replace hard wait with something event-driven.
|
||||
@ -342,7 +355,7 @@ async function getFocusedObjectUuid(page) {
|
||||
* @returns {Promise<string>} the url of the object
|
||||
*/
|
||||
async function getHashUrlToDomainObject(page, identifier) {
|
||||
await page.waitForLoadState('load');
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
const hashUrl = await page.evaluate(async (objectIdentifier) => {
|
||||
const path = await window.openmct.objects.getOriginalPath(objectIdentifier);
|
||||
let url =
|
||||
@ -377,10 +390,11 @@ async function _isInEditMode(page, identifier) {
|
||||
|
||||
/**
|
||||
* Set the time conductor mode to either fixed timespan or realtime mode.
|
||||
* @private
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {boolean} [isFixedTimespan=true] true for fixed timespan mode, false for realtime mode; default is true
|
||||
*/
|
||||
async function setTimeConductorMode(page, isFixedTimespan = true) {
|
||||
async function _setTimeConductorMode(page, isFixedTimespan = true) {
|
||||
// Click 'mode' button
|
||||
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
|
||||
await page.getByRole('button', { name: 'Time Conductor Mode Menu' }).click();
|
||||
@ -401,7 +415,7 @@ async function setTimeConductorMode(page, isFixedTimespan = true) {
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function setFixedTimeMode(page) {
|
||||
await setTimeConductorMode(page, true);
|
||||
await _setTimeConductorMode(page, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -409,7 +423,7 @@ async function setFixedTimeMode(page) {
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function setRealTimeMode(page) {
|
||||
await setTimeConductorMode(page, false);
|
||||
await _setTimeConductorMode(page, false);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -425,61 +439,67 @@ async function setRealTimeMode(page) {
|
||||
/**
|
||||
* Set the values (hours, mins, secs) for the TimeConductor offsets when in realtime mode
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {OffsetValues} offset
|
||||
* @param {import('@playwright/test').Locator} offsetButton
|
||||
* @param {OffsetValues} offset - Object containing offset values
|
||||
* @param {boolean} [offset.submitChanges=true] - If true, submit the offset changes; otherwise, discard them
|
||||
*/
|
||||
async function setTimeConductorOffset(
|
||||
page,
|
||||
{ startHours, startMins, startSecs, endHours, endMins, endSecs }
|
||||
{ startHours, startMins, startSecs, endHours, endMins, endSecs, submitChanges = true }
|
||||
) {
|
||||
if (startHours) {
|
||||
await page.getByRole('spinbutton', { name: 'Start offset hours' }).fill(startHours);
|
||||
await page.getByLabel('Start offset hours').fill(startHours);
|
||||
}
|
||||
|
||||
if (startMins) {
|
||||
await page.getByRole('spinbutton', { name: 'Start offset minutes' }).fill(startMins);
|
||||
await page.getByLabel('Start offset minutes').fill(startMins);
|
||||
}
|
||||
|
||||
if (startSecs) {
|
||||
await page.getByRole('spinbutton', { name: 'Start offset seconds' }).fill(startSecs);
|
||||
await page.getByLabel('Start offset seconds').fill(startSecs);
|
||||
}
|
||||
|
||||
if (endHours) {
|
||||
await page.getByRole('spinbutton', { name: 'End offset hours' }).fill(endHours);
|
||||
await page.getByLabel('End offset hours').fill(endHours);
|
||||
}
|
||||
|
||||
if (endMins) {
|
||||
await page.getByRole('spinbutton', { name: 'End offset minutes' }).fill(endMins);
|
||||
await page.getByLabel('End offset minutes').fill(endMins);
|
||||
}
|
||||
|
||||
if (endSecs) {
|
||||
await page.getByRole('spinbutton', { name: 'End offset seconds' }).fill(endSecs);
|
||||
await page.getByLabel('End offset seconds').fill(endSecs);
|
||||
}
|
||||
|
||||
// Click the check button
|
||||
await page.locator('.pr-time-input--buttons .icon-check').click();
|
||||
if (submitChanges) {
|
||||
await page.getByLabel('Submit time offsets').click();
|
||||
} else {
|
||||
await page.getByLabel('Discard changes and close time popup').click();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the values (hours, mins, secs) for the start time offset when in realtime mode
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {OffsetValues} offset
|
||||
* @param {boolean} [submit=true] If true, submit the offset changes; otherwise, discard them
|
||||
*/
|
||||
async function setStartOffset(page, offset) {
|
||||
async function setStartOffset(page, { submitChanges = true, ...offset }) {
|
||||
// Click 'mode' button
|
||||
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
|
||||
await setTimeConductorOffset(page, offset);
|
||||
await setTimeConductorOffset(page, { submitChanges, ...offset });
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the values (hours, mins, secs) for the end time offset when in realtime mode
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {OffsetValues} offset
|
||||
* @param {boolean} [submit=true] If true, submit the offset changes; otherwise, discard them
|
||||
*/
|
||||
async function setEndOffset(page, offset) {
|
||||
async function setEndOffset(page, { submitChanges = true, ...offset }) {
|
||||
// Click 'mode' button
|
||||
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
|
||||
await setTimeConductorOffset(page, offset);
|
||||
await setTimeConductorOffset(page, { submitChanges, ...offset });
|
||||
}
|
||||
|
||||
/**
|
||||
@ -488,33 +508,61 @@ async function setEndOffset(page, offset) {
|
||||
* NOTE: Unless explicitly testing the Time Conductor itself, it is advised to instead
|
||||
* navigate directly to the object with the desired time bounds using `navigateToObjectWithFixedTimeBounds()`.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} startDate
|
||||
* @param {string} endDate
|
||||
* @param {Object} bounds - The time conductor bounds
|
||||
* @param {string} [bounds.startDate] - The start date in YYYY-MM-DD format
|
||||
* @param {string} [bounds.startTime] - The start time in HH:mm:ss format
|
||||
* @param {string} [bounds.endDate] - The end date in YYYY-MM-DD format
|
||||
* @param {string} [bounds.endTime] - The end time in HH:mm:ss format
|
||||
* @param {boolean} [bounds.submitChanges=true] - If true, submit the changes; otherwise, discard them.
|
||||
*/
|
||||
async function setTimeConductorBounds(page, startDate, endDate) {
|
||||
// Bring up the time conductor popup
|
||||
expect(await page.locator('.l-shell__time-conductor.c-compact-tc').count()).toBe(1);
|
||||
await page.click('.l-shell__time-conductor.c-compact-tc');
|
||||
async function setTimeConductorBounds(page, { submitChanges = true, ...bounds }) {
|
||||
const { startDate, endDate, startTime, endTime } = bounds;
|
||||
|
||||
await setTimeBounds(page, startDate, endDate);
|
||||
// Open the time conductor popup
|
||||
await page.getByRole('button', { name: 'Time Conductor Mode', exact: true }).click();
|
||||
|
||||
await page.keyboard.press('Enter');
|
||||
// FIXME: https://github.com/nasa/openmct/pull/7818
|
||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
if (startDate) {
|
||||
await page.getByLabel('Start date').fill(startDate);
|
||||
}
|
||||
|
||||
if (startTime) {
|
||||
await page.getByLabel('Start time').fill(startTime);
|
||||
}
|
||||
|
||||
if (endDate) {
|
||||
await page.getByLabel('End date').fill(endDate);
|
||||
}
|
||||
|
||||
if (endTime) {
|
||||
await page.getByLabel('End time').fill(endTime);
|
||||
}
|
||||
|
||||
if (submitChanges) {
|
||||
await page.getByLabel('Submit time bounds').click();
|
||||
} else {
|
||||
await page.getByLabel('Discard changes and close time popup').click();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the independent time conductor bounds in fixed time mode
|
||||
* Set the bounds of the visible conductor in fixed time mode.
|
||||
* Requires that page already has an independent time conductor in view.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} startDate
|
||||
* @param {string} endDate
|
||||
* @param {string} start - The start date in 'YYYY-MM-DD HH:mm:ss.SSSZ' format
|
||||
* @param {string} end - The end date in 'YYYY-MM-DD HH:mm:ss.SSSZ' format
|
||||
*/
|
||||
async function setIndependentTimeConductorBounds(page, { start, end }) {
|
||||
async function setFixedIndependentTimeConductorBounds(page, { start, end }) {
|
||||
// Activate Independent Time Conductor
|
||||
await page.getByLabel('Enable Independent Time Conductor').click();
|
||||
|
||||
// Bring up the time conductor popup
|
||||
await page.getByLabel('Independent Time Conductor Settings').click();
|
||||
await expect(page.locator('.itc-popout')).toBeInViewport();
|
||||
await setTimeBounds(page, start, end);
|
||||
await expect(page.getByLabel('Time Conductor Options')).toBeInViewport();
|
||||
await _setTimeBounds(page, start, end);
|
||||
|
||||
await page.keyboard.press('Enter');
|
||||
}
|
||||
@ -523,10 +571,10 @@ async function setIndependentTimeConductorBounds(page, { start, end }) {
|
||||
* Set the bounds of the visible conductor in fixed time mode
|
||||
* @private
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} startDate
|
||||
* @param {string} endDate
|
||||
* @param {string} start - The start date in 'YYYY-MM-DD HH:mm:ss.SSSZ' format
|
||||
* @param {string} end - The end date in 'YYYY-MM-DD HH:mm:ss.SSSZ' format
|
||||
*/
|
||||
async function setTimeBounds(page, startDate, endDate) {
|
||||
async function _setTimeBounds(page, startDate, endDate) {
|
||||
if (startDate) {
|
||||
// Fill start time
|
||||
await page
|
||||
@ -556,11 +604,13 @@ async function setTimeBounds(page, startDate, endDate) {
|
||||
* all plots on the page and waits up to the default timeout for the class to be
|
||||
* attached to each plot.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {number} [timeout] Provide a custom timeout in milliseconds to override the default timeout
|
||||
*/
|
||||
async function waitForPlotsToRender(page) {
|
||||
async function waitForPlotsToRender(page, { timeout } = {}) {
|
||||
//eslint-disable-next-line playwright/no-raw-locators
|
||||
const plotLocator = page.locator('.gl-plot');
|
||||
for (const plot of await plotLocator.all()) {
|
||||
await expect(plot).toHaveClass(/js-series-data-loaded/);
|
||||
await expect(plot).toHaveClass(/js-series-data-loaded/, { timeout });
|
||||
}
|
||||
}
|
||||
|
||||
@ -581,9 +631,6 @@ async function waitForPlotsToRender(page) {
|
||||
* @return {Promise<PlotPixel[]>}
|
||||
*/
|
||||
async function getCanvasPixels(page, canvasSelector) {
|
||||
const getTelemValuePromise = new Promise((resolve) =>
|
||||
page.exposeFunction('getCanvasValue', resolve)
|
||||
);
|
||||
const canvasHandle = await page.evaluateHandle(
|
||||
(canvas) => document.querySelector(canvas),
|
||||
canvasSelector
|
||||
@ -594,7 +641,7 @@ async function getCanvasPixels(page, canvasSelector) {
|
||||
);
|
||||
|
||||
await waitForPlotsToRender(page);
|
||||
await page.evaluate(
|
||||
return page.evaluate(
|
||||
([canvas, ctx]) => {
|
||||
// The document canvas is where the plot points and lines are drawn.
|
||||
// The only way to access the canvas is using document (using page.evaluate)
|
||||
@ -622,27 +669,43 @@ async function getCanvasPixels(page, canvasSelector) {
|
||||
i = i + 4;
|
||||
}
|
||||
|
||||
window.getCanvasValue(plotPixels);
|
||||
return plotPixels;
|
||||
},
|
||||
[canvasHandle, canvasContextHandle]
|
||||
);
|
||||
|
||||
return getTelemValuePromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for telemetry and link it to an object. objectName should come from the domainObject.name function.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} parameterName
|
||||
* @param {string} objectName
|
||||
*/
|
||||
async function linkParameterToObject(page, parameterName, objectName) {
|
||||
await page.getByRole('searchbox', { name: 'Search Input' }).click();
|
||||
await page.getByRole('searchbox', { name: 'Search Input' }).fill(parameterName);
|
||||
await page.getByLabel('Object Results').getByText(parameterName).click();
|
||||
await page.getByLabel('More actions').click();
|
||||
await page.getByLabel('Create Link').click();
|
||||
await page.getByLabel('Modal Overlay').getByLabel('Search Input').click();
|
||||
await page.getByLabel('Modal Overlay').getByLabel('Search Input').fill(objectName);
|
||||
await page.getByLabel('Modal Overlay').getByLabel(`Navigate to ${objectName}`).click();
|
||||
await page.getByLabel('Save').click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename the currently viewed `domainObject` from the browse bar.
|
||||
*
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} myItemsFolderName
|
||||
* @param {string} url
|
||||
* @param {string} newName
|
||||
*/
|
||||
async function renameObjectFromContextMenu(page, url, newName) {
|
||||
await openObjectTreeContextMenu(page, url);
|
||||
await page.click('li:text("Edit Properties")');
|
||||
const nameInput = page.getByLabel('Title', { exact: true });
|
||||
async function renameCurrentObjectFromBrowseBar(page, newName) {
|
||||
const nameInput = page.getByLabel('Browse bar object name');
|
||||
await nameInput.click();
|
||||
await nameInput.fill('');
|
||||
await nameInput.fill(newName);
|
||||
await page.click('[aria-label="Save"]');
|
||||
// Click the browse bar container to save changes
|
||||
await page.getByLabel('Browse bar', { exact: true }).click();
|
||||
}
|
||||
|
||||
export {
|
||||
@ -650,20 +713,18 @@ export {
|
||||
createExampleTelemetryObject,
|
||||
createNotification,
|
||||
createPlanFromJSON,
|
||||
createStableStateTelemetry,
|
||||
expandEntireTree,
|
||||
expandTreePaneItemByName,
|
||||
getCanvasPixels,
|
||||
getFocusedObjectUuid,
|
||||
getHashUrlToDomainObject,
|
||||
linkParameterToObject,
|
||||
navigateToObjectWithFixedTimeBounds,
|
||||
openObjectTreeContextMenu,
|
||||
renameObjectFromContextMenu,
|
||||
navigateToObjectWithRealTime,
|
||||
renameCurrentObjectFromBrowseBar,
|
||||
setEndOffset,
|
||||
setFixedIndependentTimeConductorBounds,
|
||||
setFixedTimeMode,
|
||||
setIndependentTimeConductorBounds,
|
||||
setRealTimeMode,
|
||||
setStartOffset,
|
||||
setTimeConductorBounds,
|
||||
setTimeConductorMode,
|
||||
waitForPlotsToRender
|
||||
};
|
||||
|
@ -34,7 +34,7 @@
|
||||
*/
|
||||
|
||||
import AxeBuilder from '@axe-core/playwright';
|
||||
import fs from 'fs';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
@ -87,6 +87,27 @@ const extendedTest = test.extend({
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Writes the accessibility report to the specified path.
|
||||
*
|
||||
* @param {string} reportPath - The path to write the report to.
|
||||
* @param {Object} accessibilityScanResults - The results of the accessibility scan.
|
||||
* @returns {Promise<Object>} The accessibility scan results.
|
||||
* @throws Will throw an error if writing the report fails.
|
||||
*/
|
||||
async function writeAccessibilityReport(reportPath, accessibilityScanResults) {
|
||||
try {
|
||||
await fs.mkdir(path.dirname(reportPath), { recursive: true });
|
||||
const data = JSON.stringify(accessibilityScanResults, null, 2);
|
||||
await fs.writeFile(reportPath, data);
|
||||
console.log(`Accessibility report with violations saved successfully as ${reportPath}`);
|
||||
return accessibilityScanResults;
|
||||
} catch (err) {
|
||||
console.error(`Error writing the accessibility report to file ${reportPath}:`, err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans for accessibility violations on a page and writes a report to disk if violations are found.
|
||||
* Automatically asserts that no violations should be present.
|
||||
@ -104,25 +125,29 @@ export async function scanForA11yViolations(page, testCaseName, options = {}) {
|
||||
const accessibilityScanResults = await builder.analyze();
|
||||
|
||||
// Assert that no violations should be present
|
||||
expect(
|
||||
accessibilityScanResults.violations,
|
||||
`Accessibility violations found in test case: ${testCaseName}`
|
||||
).toEqual([]);
|
||||
expect
|
||||
.soft(
|
||||
accessibilityScanResults.violations,
|
||||
`Accessibility violations found in test case: ${testCaseName}`
|
||||
)
|
||||
.toEqual([]);
|
||||
|
||||
// Check if there are any violations
|
||||
if (accessibilityScanResults.violations.length > 0) {
|
||||
let reportName = options.reportName || testCaseName;
|
||||
let sanitizedReportName = reportName.replace(/\//g, '_');
|
||||
const reportPath = path.join(TEST_RESULTS_DIR, `${sanitizedReportName}.json`);
|
||||
const reportName = options.reportName || testCaseName;
|
||||
const sanitizedReportName = reportName.replace(/\//g, '_');
|
||||
const reportPath = path.join(
|
||||
TEST_RESULTS_DIR,
|
||||
'a11y-json-reports',
|
||||
`${sanitizedReportName}.json`
|
||||
);
|
||||
|
||||
try {
|
||||
if (!fs.existsSync(TEST_RESULTS_DIR)) {
|
||||
fs.mkdirSync(TEST_RESULTS_DIR);
|
||||
}
|
||||
await page.screenshot({
|
||||
path: path.join(TEST_RESULTS_DIR, 'a11y-screenshots', `${sanitizedReportName}.png`)
|
||||
});
|
||||
|
||||
fs.writeFileSync(reportPath, JSON.stringify(accessibilityScanResults, null, 2));
|
||||
console.log(`Accessibility report with violations saved successfully as ${reportPath}`);
|
||||
return accessibilityScanResults;
|
||||
return await writeAccessibilityReport(reportPath, accessibilityScanResults);
|
||||
} catch (err) {
|
||||
console.error(`Error writing the accessibility report to file ${reportPath}:`, err);
|
||||
throw err;
|
||||
|
@ -30,7 +30,6 @@
|
||||
import { expect, request, test } from '@playwright/test';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import sinon from 'sinon';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
@ -70,82 +69,6 @@ const extendedTest = test.extend({
|
||||
*/
|
||||
|
||||
coveragePath: [istanbulCLIOutput, { option: true }],
|
||||
/**
|
||||
* This allows the test to manipulate the browser clock. This is useful for Visual and Snapshot tests which need
|
||||
* the Time Indicator Clock to be in a specific state.
|
||||
*
|
||||
* Warning: Has many limitations and secondary side effects in Open MCT.
|
||||
* 1. The tree component does not render.
|
||||
* 2. page.WaitForNavigation does not trigger.
|
||||
*
|
||||
* Usage:
|
||||
* ```js
|
||||
* test.use({
|
||||
* clockOptions: {
|
||||
* now: MISSION_TIME,
|
||||
* shouldAdvanceTime: true
|
||||
* ```
|
||||
* If clockOptions are provided, will override the default clock with fake timers provided by SinonJS.
|
||||
*
|
||||
* Default: `undefined`
|
||||
*
|
||||
* @see {@link https://github.com/microsoft/playwright/issues/6347 Github RFE}
|
||||
* @see {@link https://github.com/sinonjs/fake-timers/#var-clock--faketimersinstallconfig SinonJS FakeTimers Config}
|
||||
* @type {import('@types/sinonjs__fake-timers').FakeTimerInstallOpts}
|
||||
*/
|
||||
clockOptions: [undefined, { option: true }],
|
||||
overrideClock: [
|
||||
async ({ context, clockOptions }, use) => {
|
||||
if (clockOptions !== undefined) {
|
||||
await context.addInitScript({
|
||||
path: fileURLToPath(new URL('../node_modules/sinon/pkg/sinon.js', import.meta.url))
|
||||
});
|
||||
await context.addInitScript((options) => {
|
||||
window.__clock = sinon.useFakeTimers(options);
|
||||
}, clockOptions);
|
||||
}
|
||||
|
||||
await use(context);
|
||||
},
|
||||
{
|
||||
auto: true,
|
||||
scope: 'test'
|
||||
}
|
||||
],
|
||||
/**
|
||||
* Exposes a function to manually tick the clock. This is useful when overriding the clock to not
|
||||
* tick (`shouldAdvanceTime: false`) for visual tests, as events such as re-renders and router params
|
||||
* updates are clock-driven and must be manually ticked.
|
||||
*
|
||||
* Usage:
|
||||
* ```js
|
||||
* test.describe('Manual Clock Tick', () => {
|
||||
* test.use({
|
||||
* clockOptions: {
|
||||
* now: MISSION_TIME, // Set to the desired time
|
||||
* shouldAdvanceTime: false // Clock overridden to no longer tick
|
||||
* }
|
||||
* });
|
||||
* test('Visual - Manual Clock Tick', async ({ page, tick }) => {
|
||||
* // Tick the clock 2 seconds in the future
|
||||
* await tick(2000);
|
||||
* });
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* @param {Object} param0
|
||||
* @param {import('@playwright/test').Page} param0.page
|
||||
* @param {import('@playwright/test').Use} param0.use
|
||||
*/
|
||||
tick: async ({ page }, use) => {
|
||||
// eslint-disable-next-line func-style
|
||||
const tick = async (milliseconds) => {
|
||||
await page.evaluate((_milliseconds) => {
|
||||
window.__clock.tick(_milliseconds);
|
||||
}, milliseconds);
|
||||
};
|
||||
await use(tick);
|
||||
},
|
||||
/**
|
||||
* Extends the base context class to add codecoverage shim.
|
||||
* @see {@link https://github.com/mxschmitt/playwright-test-coverage Github Project}
|
||||
@ -180,54 +103,40 @@ const extendedTest = test.extend({
|
||||
* Default: `true`
|
||||
*/
|
||||
failOnConsoleError: [true, { option: true }],
|
||||
ignore404s: [[], { option: true }],
|
||||
/**
|
||||
* Extends the base page class to enable console log error detection.
|
||||
* @see {@link https://github.com/microsoft/playwright/discussions/11690 Github Discussion}
|
||||
*/
|
||||
page: async ({ page, failOnConsoleError, clockOptions }, use) => {
|
||||
// If overriding the clock, we must also override the Date.now()
|
||||
// function in the generatorWorker context. This is necessary
|
||||
// to ensure that example telemetry data is generated for the new clock time.
|
||||
if (clockOptions?.now !== undefined) {
|
||||
page.on('worker', (worker) => {
|
||||
if (worker.url().includes('generatorWorker')) {
|
||||
worker.evaluate((time) => {
|
||||
self.Date.now = () => time;
|
||||
}, clockOptions.now);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
page: async ({ page, failOnConsoleError, ignore404s }, use) => {
|
||||
// Capture any console errors during test execution
|
||||
const messages = [];
|
||||
let messages = [];
|
||||
page.on('console', (msg) => messages.push(msg));
|
||||
|
||||
await use(page);
|
||||
|
||||
if (ignore404s.length > 0) {
|
||||
messages = messages.filter((msg) => {
|
||||
let keep = true;
|
||||
|
||||
if (msg.text().match(/404 \((Object )?Not Found\)/) !== null) {
|
||||
keep = ignore404s.every((ignoreRule) => {
|
||||
return msg.location().url.match(ignoreRule) === null;
|
||||
});
|
||||
}
|
||||
|
||||
return keep;
|
||||
});
|
||||
}
|
||||
|
||||
// Assert against console errors during teardown
|
||||
if (failOnConsoleError) {
|
||||
messages.forEach((msg) =>
|
||||
messages.forEach((msg) => {
|
||||
// eslint-disable-next-line playwright/no-standalone-expect
|
||||
expect
|
||||
.soft(msg.type(), `Console error detected: ${_consoleMessageToString(msg)}`)
|
||||
.not.toEqual('error')
|
||||
);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Extends the base browser class to enable CDP connection definition in playwright.config.js. Once
|
||||
* that RFE is implemented, this function can be removed.
|
||||
* @see {@link https://github.com/microsoft/playwright/issues/8379 Github RFE}
|
||||
*/
|
||||
browser: async ({ playwright, browser }, use, workerInfo) => {
|
||||
// Use browserless if configured
|
||||
if (workerInfo.project.name.match(/browserless/)) {
|
||||
const vBrowser = await playwright.chromium.connectOverCDP({
|
||||
endpointURL: 'ws://localhost:3003'
|
||||
.not.toEqual('error');
|
||||
});
|
||||
await use(vBrowser);
|
||||
} else {
|
||||
// Use Local Browser for testing.
|
||||
await use(browser);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -25,6 +25,7 @@ import { expect } from '../pluginFixtures.js';
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function navigateToFaultManagementWithExample(page) {
|
||||
await page.addInitScript({
|
||||
@ -36,6 +37,7 @@ export async function navigateToFaultManagementWithExample(page) {
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function navigateToFaultManagementWithStaticExample(page) {
|
||||
await page.addInitScript({
|
||||
@ -47,6 +49,7 @@ export async function navigateToFaultManagementWithStaticExample(page) {
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function navigateToFaultManagementWithoutExample(page) {
|
||||
await page.addInitScript({
|
||||
@ -58,9 +61,11 @@ export async function navigateToFaultManagementWithoutExample(page) {
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function navigateToFaultItemInTree(page) {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
async function navigateToFaultItemInTree(page) {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForURL('**/#/browse/mine?**');
|
||||
|
||||
const faultManagementTreeItem = page
|
||||
.getByRole('tree', {
|
||||
@ -76,6 +81,8 @@ export async function navigateToFaultItemInTree(page) {
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {number} rowNumber
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function acknowledgeFault(page, rowNumber) {
|
||||
await openFaultRowMenu(page, rowNumber);
|
||||
@ -85,6 +92,8 @@ export async function acknowledgeFault(page, rowNumber) {
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {...number} nums
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function shelveMultipleFaults(page, ...nums) {
|
||||
const selectRows = nums.map((num) => {
|
||||
@ -98,6 +107,8 @@ export async function shelveMultipleFaults(page, ...nums) {
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {...number} nums
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function acknowledgeMultipleFaults(page, ...nums) {
|
||||
const selectRows = nums.map((num) => {
|
||||
@ -105,50 +116,43 @@ export async function acknowledgeMultipleFaults(page, ...nums) {
|
||||
});
|
||||
await Promise.all(selectRows);
|
||||
|
||||
await page.locator('button:has-text("Acknowledge")').click();
|
||||
await page.getByLabel('Acknowledge selected faults').click();
|
||||
await page.getByLabel('Save').click();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {number} rowNumber
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function shelveFault(page, rowNumber) {
|
||||
await openFaultRowMenu(page, rowNumber);
|
||||
await page.locator('.c-menu >> text="Shelve"').click();
|
||||
// Click [aria-label="Save"]
|
||||
await page.getByLabel('Shelve', { exact: true }).click();
|
||||
await page.getByLabel('Save').click();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
export async function changeViewTo(page, view) {
|
||||
await page.locator('.c-fault-mgmt__search-row select').first().selectOption(view);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {'severity' | 'newest-first' | 'oldest-first'} sort
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function sortFaultsBy(page, sort) {
|
||||
await page.locator('.c-fault-mgmt__list-header-sortButton select').selectOption(sort);
|
||||
await page.getByTitle('Sort By').getByRole('combobox').selectOption(sort);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {'acknowledged' | 'shelved' | 'standard view'} view
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function enterSearchTerm(page, term) {
|
||||
await page.locator('.c-fault-mgmt-search [aria-label="Search Input"]').fill(term);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
export async function clearSearch(page) {
|
||||
await enterSearchTerm(page, '');
|
||||
export async function changeViewTo(page, view) {
|
||||
await page.getByTitle('View Filter').getByRole('combobox').selectOption(view);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {number} rowNumber
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function selectFaultItem(page, rowNumber) {
|
||||
await page
|
||||
@ -164,71 +168,37 @@ export async function selectFaultItem(page, rowNumber) {
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
export async function getHighestSeverity(page) {
|
||||
const criticalCount = await page.locator('[title=CRITICAL]').count();
|
||||
const warningCount = await page.locator('[title=WARNING]').count();
|
||||
|
||||
if (criticalCount > 0) {
|
||||
return 'CRITICAL';
|
||||
} else if (warningCount > 0) {
|
||||
return 'WARNING';
|
||||
}
|
||||
|
||||
return 'WATCH';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
export async function getLowestSeverity(page) {
|
||||
const warningCount = await page.locator('[title=WARNING]').count();
|
||||
const watchCount = await page.locator('[title=WATCH]').count();
|
||||
|
||||
if (watchCount > 0) {
|
||||
return 'WATCH';
|
||||
} else if (warningCount > 0) {
|
||||
return 'WARNING';
|
||||
}
|
||||
|
||||
return 'CRITICAL';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
export async function getFaultResultCount(page) {
|
||||
const count = await page.locator('.c-faults-list-view-item-body > .c-fault-mgmt__list').count();
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {number} rowNumber
|
||||
* @returns {import('@playwright/test').Locator}
|
||||
*/
|
||||
export function getFault(page, rowNumber) {
|
||||
const fault = page.locator(
|
||||
`.c-faults-list-view-item-body > .c-fault-mgmt__list >> nth=${rowNumber - 1}`
|
||||
);
|
||||
const fault = page.getByLabel('Fault triggered at').nth(rowNumber - 1);
|
||||
|
||||
return fault;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} name
|
||||
* @returns {import('@playwright/test').Locator}
|
||||
*/
|
||||
export function getFaultByName(page, name) {
|
||||
const fault = page.locator(`.c-fault-mgmt__list-faultname:has-text("${name}")`);
|
||||
const fault = page.getByLabel('Fault triggered at').filter({
|
||||
hasText: name
|
||||
});
|
||||
|
||||
return fault;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {number} rowNumber
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
export async function getFaultName(page, rowNumber) {
|
||||
const faultName = await page
|
||||
.locator(`.c-fault-mgmt__list-faultname >> nth=${rowNumber - 1}`)
|
||||
.getByLabel('Fault name', { exact: true })
|
||||
.nth(rowNumber - 1)
|
||||
.textContent();
|
||||
|
||||
return faultName;
|
||||
@ -236,21 +206,13 @@ export async function getFaultName(page, rowNumber) {
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
export async function getFaultSeverity(page, rowNumber) {
|
||||
const faultSeverity = await page
|
||||
.locator(`.c-faults-list-view-item-body .c-fault-mgmt__list-severity >> nth=${rowNumber - 1}`)
|
||||
.getAttribute('title');
|
||||
|
||||
return faultSeverity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {number} rowNumber
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
export async function getFaultNamespace(page, rowNumber) {
|
||||
const faultNamespace = await page
|
||||
.locator(`.c-fault-mgmt__list-path >> nth=${rowNumber - 1}`)
|
||||
.getByLabel('Fault namespace')
|
||||
.nth(rowNumber - 1)
|
||||
.textContent();
|
||||
|
||||
return faultNamespace;
|
||||
@ -258,10 +220,13 @@ export async function getFaultNamespace(page, rowNumber) {
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {number} rowNumber
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
export async function getFaultTriggerTime(page, rowNumber) {
|
||||
const faultTriggerTime = await page
|
||||
.locator(`.c-fault-mgmt__list-trigTime >> nth=${rowNumber - 1} >> .c-fault-mgmt-item__value`)
|
||||
.getByLabel('Last Trigger Time')
|
||||
.nth(rowNumber - 1)
|
||||
.textContent();
|
||||
|
||||
return faultTriggerTime.toString().trim();
|
||||
@ -269,11 +234,14 @@ export async function getFaultTriggerTime(page, rowNumber) {
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {number} rowNumber
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function openFaultRowMenu(page, rowNumber) {
|
||||
// select
|
||||
await page
|
||||
.getByLabel('Disposition actions')
|
||||
.getByLabel('Fault triggered at')
|
||||
.nth(rowNumber - 1)
|
||||
.getByLabel('Disposition Actions')
|
||||
.click();
|
||||
}
|
||||
|
47
e2e/helper/hotkeys/clipboard.js
Normal file
47
e2e/helper/hotkeys/clipboard.js
Normal file
@ -0,0 +1,47 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
const isMac = process.platform === 'darwin';
|
||||
const modifier = isMac ? 'Meta' : 'Control';
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function selectAll(page) {
|
||||
await page.keyboard.press(`${modifier}+KeyA`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function copy(page) {
|
||||
await page.keyboard.press(`${modifier}+KeyC`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function paste(page) {
|
||||
await page.keyboard.press(`${modifier}+KeyV`);
|
||||
}
|
||||
|
||||
export { copy, paste, selectAll };
|
23
e2e/helper/hotkeys/hotkeys.js
Normal file
23
e2e/helper/hotkeys/hotkeys.js
Normal file
@ -0,0 +1,23 @@
|
||||
/*****************************************************************************
|
||||
* 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 * from './clipboard.js';
|
33
e2e/helper/imageryUtils.js
Normal file
33
e2e/helper/imageryUtils.js
Normal file
@ -0,0 +1,33 @@
|
||||
import { createDomainObjectWithDefaults } from '../appActions.js';
|
||||
import { expect } from '../pluginFixtures.js';
|
||||
|
||||
const IMAGE_LOAD_DELAY = 5 * 1000;
|
||||
const FIVE_MINUTES = 1000 * 60 * 5;
|
||||
const THIRTY_SECONDS = 1000 * 30;
|
||||
const MOUSE_WHEEL_DELTA_Y = 120;
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function createImageryViewWithShortDelay(page, { name, parent }) {
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
name,
|
||||
type: 'Example Imagery',
|
||||
parent
|
||||
});
|
||||
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');
|
||||
await page.getByLabel('More actions').click();
|
||||
await page.getByLabel('Edit Properties').click();
|
||||
// Clear and set Image load delay to minimum value
|
||||
await page.locator('input[type="number"]').fill(`${IMAGE_LOAD_DELAY}`);
|
||||
await page.getByLabel('Save').click();
|
||||
}
|
||||
|
||||
export {
|
||||
createImageryViewWithShortDelay,
|
||||
FIVE_MINUTES,
|
||||
IMAGE_LOAD_DELAY,
|
||||
MOUSE_WHEEL_DELTA_Y,
|
||||
THIRTY_SECONDS
|
||||
};
|
@ -28,16 +28,28 @@ import { fileURLToPath } from 'url';
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} text
|
||||
*/
|
||||
async function enterTextEntry(page, text) {
|
||||
// Click the 'Add Notebook Entry' area
|
||||
await page.locator(NOTEBOOK_DROP_AREA).click();
|
||||
|
||||
// enter text
|
||||
await page.getByLabel('Notebook Entry Input').last().fill(text);
|
||||
await addNotebookEntry(page);
|
||||
await enterTextInLastEntry(page, text);
|
||||
await commitEntry(page);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function addNotebookEntry(page) {
|
||||
await page.locator(NOTEBOOK_DROP_AREA).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function enterTextInLastEntry(page, text) {
|
||||
await page.getByLabel('Notebook Entry Input').last().fill(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
@ -51,7 +63,7 @@ async function dragAndDropEmbed(page, notebookObject) {
|
||||
// Expand the tree to reveal the notebook
|
||||
await page.getByLabel('Show selected item in tree').click();
|
||||
// Drag and drop the SWG into the notebook
|
||||
await page.dragAndDrop(`text=${swg.name}`, NOTEBOOK_DROP_AREA);
|
||||
await page.getByLabel(`Navigate to ${swg.name}`).dragTo(page.locator(NOTEBOOK_DROP_AREA));
|
||||
await commitEntry(page);
|
||||
}
|
||||
|
||||
@ -72,6 +84,7 @@ async function startAndAddRestrictedNotebookObject(page) {
|
||||
path: fileURLToPath(new URL('./addInitRestrictedNotebook.js', import.meta.url))
|
||||
});
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForURL('**/browse/mine?**');
|
||||
|
||||
return createDomainObjectWithDefaults(page, {
|
||||
type: CUSTOM_NAME,
|
||||
@ -83,10 +96,9 @@ async function startAndAddRestrictedNotebookObject(page) {
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function lockPage(page) {
|
||||
const commitButton = page.locator('button:has-text("Commit Entries")');
|
||||
await commitButton.click();
|
||||
|
||||
//Wait until Lock Banner is visible
|
||||
// Click the Commit Entries button
|
||||
await page.getByLabel('Commit Entries').click();
|
||||
// Wait until Lock Banner is visible
|
||||
await page.locator('text=Lock Page').click();
|
||||
}
|
||||
|
||||
@ -140,10 +152,13 @@ async function createNotebookEntryAndTags(page, iterations = 1) {
|
||||
}
|
||||
|
||||
export {
|
||||
addNotebookEntry,
|
||||
commitEntry,
|
||||
createNotebookAndEntry,
|
||||
createNotebookEntryAndTags,
|
||||
dragAndDropEmbed,
|
||||
enterTextEntry,
|
||||
enterTextInLastEntry,
|
||||
lockPage,
|
||||
startAndAddRestrictedNotebookObject
|
||||
};
|
||||
|
@ -30,9 +30,9 @@ import { expect } from '../pluginFixtures.js';
|
||||
* start time as the start bound and the current activity's end time as the end bound.
|
||||
* @param {import('@playwright/test').Page} page the page
|
||||
* @param {Object} plan The raw plan json to assert against
|
||||
* @param {string} objectUrl The URL of the object to assert against (plan or gantt chart)
|
||||
* @param {string} planObjectUrl The URL of the object to assert against (plan or gantt chart)
|
||||
*/
|
||||
export async function assertPlanActivities(page, plan, objectUrl) {
|
||||
export async function assertPlanActivities(page, plan, planObjectUrl) {
|
||||
const groups = Object.keys(plan);
|
||||
for (const group of groups) {
|
||||
for (let i = 0; i < plan[group].length; i++) {
|
||||
@ -48,13 +48,12 @@ export async function assertPlanActivities(page, plan, objectUrl) {
|
||||
|
||||
// Switch to fixed time mode with all plan events within the bounds
|
||||
await page.goto(
|
||||
`${objectUrl}?tc.mode=fixed&tc.startBound=${startBound}&tc.endBound=${endBound}&tc.timeSystem=utc&view=plan.view`
|
||||
`${planObjectUrl}?tc.mode=fixed&tc.startBound=${startBound}&tc.endBound=${endBound}&tc.timeSystem=utc&view=plan.view`
|
||||
);
|
||||
|
||||
// Assert that the number of activities in the plan view matches the number of
|
||||
// activities in the plan data within the specified time bounds
|
||||
const eventCount = await page.locator('.activity-bounds').count();
|
||||
expect(eventCount).toEqual(
|
||||
await expect(page.locator('.activity-bounds')).toHaveCount(
|
||||
Object.values(plan)
|
||||
.flat()
|
||||
.filter((event) =>
|
||||
@ -101,8 +100,8 @@ export async function assertPlanOrderedSwimLanes(page, plan, objectUrl) {
|
||||
for (let i = 0; i < groups.length; i++) {
|
||||
// Assert that the order of groups in the plan view matches the order of
|
||||
// groups in the plan data
|
||||
const groupName = await planGroups[i].innerText();
|
||||
expect(groupName).toEqual(groups[i].name);
|
||||
const groupName = planGroups[i];
|
||||
await expect(groupName).toHaveText(groups[i].name);
|
||||
}
|
||||
}
|
||||
|
||||
@ -130,6 +129,7 @@ export async function setBoundsToSpanAllActivities(page, planJson, planObjectUrl
|
||||
*/
|
||||
export function getEarliestStartTime(planJson) {
|
||||
const activities = Object.values(planJson).flat();
|
||||
|
||||
return Math.min(...activities.map((activity) => activity.start));
|
||||
}
|
||||
|
||||
@ -140,6 +140,7 @@ export function getEarliestStartTime(planJson) {
|
||||
*/
|
||||
export function getLatestEndTime(planJson) {
|
||||
const activities = Object.values(planJson).flat();
|
||||
|
||||
return Math.max(...activities.map((activity) => activity.end));
|
||||
}
|
||||
|
||||
@ -152,6 +153,7 @@ export function getFirstActivity(planJson) {
|
||||
const groups = Object.keys(planJson);
|
||||
const firstGroupKey = groups[0];
|
||||
const firstGroupItems = planJson[firstGroupKey];
|
||||
|
||||
return firstGroupItems[0];
|
||||
}
|
||||
|
||||
@ -222,7 +224,7 @@ export async function createTimelistWithPlanAndSetActivityInProgress(page, planJ
|
||||
await page.getByRole('button', { name: 'Edit Object' }).click();
|
||||
|
||||
// Find the display properties section in the inspector
|
||||
await page.getByRole('tab', { name: 'View Properties' }).click();
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
// Switch to expanded view and save the setting
|
||||
await page.getByLabel('Display Style').selectOption({ label: 'Expanded' });
|
||||
|
||||
|
@ -76,6 +76,7 @@ export async function createTags({ page, canvas, xEnd = 700, yEnd = 520 }) {
|
||||
export async function testTelemetryItem(page, telemetryItem) {
|
||||
// Check that telemetry item also received the tag
|
||||
await page.goto(telemetryItem.url);
|
||||
await page.getByRole('tab', { name: 'Annotations' }).click();
|
||||
|
||||
await expect(page.getByText('No tags to display for this item')).toBeVisible();
|
||||
|
||||
@ -93,6 +94,7 @@ export async function testTelemetryItem(page, telemetryItem) {
|
||||
y: 100
|
||||
}
|
||||
});
|
||||
await page.getByRole('tab', { name: 'Annotations' }).click();
|
||||
|
||||
await expect(page.getByText('Science')).toBeVisible();
|
||||
await expect(page.getByText('Driving')).toBeHidden();
|
||||
@ -107,6 +109,8 @@ export async function basicTagsTests(page) {
|
||||
// Search for Driving
|
||||
await page.getByRole('searchbox', { name: 'Search Input' }).click();
|
||||
|
||||
await page.getByRole('tab', { name: 'Annotations' }).click();
|
||||
|
||||
// Clicking elsewhere should cause annotation selection to be cleared
|
||||
await expect(page.getByText('No tags to display for this item')).toBeVisible();
|
||||
//
|
||||
@ -119,6 +123,8 @@ export async function basicTagsTests(page) {
|
||||
.first()
|
||||
.click();
|
||||
|
||||
await page.getByRole('tab', { name: 'Annotations' }).click();
|
||||
|
||||
// Delete Driving Tag
|
||||
await page.hover('[aria-label="Tag"]:has-text("Driving")');
|
||||
await page.locator('[aria-label="Remove tag Driving"]').click();
|
||||
@ -155,6 +161,8 @@ export async function basicTagsTests(page) {
|
||||
}
|
||||
});
|
||||
|
||||
await page.getByRole('tab', { name: 'Annotations' }).click();
|
||||
|
||||
//Expect Science to be visible but Driving to be hidden
|
||||
await expect(page.getByText('Science')).toBeVisible();
|
||||
await expect(page.getByText('Driving')).toBeHidden();
|
||||
@ -170,7 +178,7 @@ export async function basicTagsTests(page) {
|
||||
});
|
||||
|
||||
// Add Driving Tag again
|
||||
await page.getByText('Annotations').click();
|
||||
await page.getByRole('tab', { name: 'Annotations' }).click();
|
||||
await page.getByRole('button', { name: /Add Tag/ }).click();
|
||||
await page.getByPlaceholder('Type to select tag').click();
|
||||
await page.getByText('Driving').click();
|
||||
|
29
e2e/helper/useDarkmatterTheme.js
Normal file
29
e2e/helper/useDarkmatterTheme.js
Normal file
@ -0,0 +1,29 @@
|
||||
/*****************************************************************************
|
||||
* 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 should be used to install the Darkmatter theme for Open MCT.
|
||||
// e.g.
|
||||
// await page.addInitScript({ path: path.join(__dirname, 'useDarkmatterTheme.js') });
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const openmct = window.openmct;
|
||||
openmct.install(openmct.plugins.DarkmatterTheme());
|
||||
});
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openmct-e2e",
|
||||
"version": "4.0.0-next",
|
||||
"version": "4.1.0-next",
|
||||
"description": "The Open MCT e2e framework",
|
||||
"type": "module",
|
||||
"module": "index.js",
|
||||
@ -10,18 +10,18 @@
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"pretest:visual": "npm install",
|
||||
"test": "npx playwright test",
|
||||
"test:visual": "percy exec"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/sinonjs__fake-timers": "8.1.5",
|
||||
"@percy/cli": "1.27.4",
|
||||
"@percy/playwright": "1.0.4",
|
||||
"@playwright/test": "1.42.1",
|
||||
"@axe-core/playwright": "4.8.5",
|
||||
"sinon": "17.0.0"
|
||||
"@playwright/test": "1.48.1",
|
||||
"@axe-core/playwright": "4.8.5"
|
||||
},
|
||||
"author": {
|
||||
"name": "National Aeronautics and Space Administration",
|
||||
"url": "https://www.nasa.gov"
|
||||
},
|
||||
"author": "NASA Ames Research Center",
|
||||
"license": "Apache-2.0"
|
||||
}
|
@ -12,7 +12,7 @@ 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
|
||||
testIgnore: '**/*.perf.spec.js', //Ignore performance tests and define in playwright-performance.config.js
|
||||
timeout: 60 * 1000,
|
||||
webServer: {
|
||||
command: 'npm run start:coverage',
|
||||
|
@ -10,7 +10,7 @@ import { fileURLToPath } from 'url';
|
||||
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
|
||||
testIgnore: '**/*.perf.spec.js', //Ignore performance tests and define in playwright-performance.config.js
|
||||
timeout: 30 * 1000,
|
||||
webServer: {
|
||||
command: 'npm run start:coverage',
|
||||
|
@ -13,7 +13,7 @@ const config = {
|
||||
cwd: fileURLToPath(new URL('../', import.meta.url)), // Provide cwd for the root of the project
|
||||
url: 'http://localhost:8080/#',
|
||||
timeout: 200 * 1000,
|
||||
reuseExistingServer: !process.env.CI
|
||||
reuseExistingServer: true //This was originally disabled to prevent differences in local debugging vs. CI. However, it significantly speeds up local debugging.
|
||||
},
|
||||
use: {
|
||||
baseURL: 'http://localhost:8080/',
|
||||
@ -36,6 +36,13 @@ const config = {
|
||||
browserName: 'chromium',
|
||||
theme: 'snow'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'darkmatter-theme', //Runs the same visual tests but with darkmatter-theme
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
theme: 'darkmatter'
|
||||
}
|
||||
}
|
||||
],
|
||||
reporter: [
|
||||
|
@ -127,6 +127,11 @@ const extendedTest = test.extend({
|
||||
await page.addInitScript({
|
||||
path: fileURLToPath(new URL('./helper/useSnowTheme.js', import.meta.url))
|
||||
});
|
||||
} else if (theme === 'darkmatter') {
|
||||
//inject darkmatter theme
|
||||
await page.addInitScript({
|
||||
path: fileURLToPath(new URL('./helper/useDarkmatterTheme.js', import.meta.url))
|
||||
});
|
||||
}
|
||||
|
||||
// Attach info about the currently running test and its project.
|
||||
|
22
e2e/test-data/condition_set_storage.json
Normal file
22
e2e/test-data/condition_set_storage.json
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -6,15 +6,15 @@
|
||||
"localStorage": [
|
||||
{
|
||||
"name": "mct",
|
||||
"value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"e78ca721-fb5e-409b-bf6d-597c87cb716f\",\"namespace\":\"\"},{\"key\":\"c6100044-56be-44b3-acca-6b9fddfb3849\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413602460,\"created\":1732413600960,\"persisted\":1732413602460},\"e78ca721-fb5e-409b-bf6d-597c87cb716f\":{\"identifier\":{\"key\":\"e78ca721-fb5e-409b-bf6d-597c87cb716f\",\"namespace\":\"\"},\"name\":\"Overlay Plot with Telemetry Object\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"c6100044-56be-44b3-acca-6b9fddfb3849\",\"namespace\":\"\"}],\"configuration\":{\"series\":[{\"identifier\":{\"key\":\"c6100044-56be-44b3-acca-6b9fddfb3849\",\"namespace\":\"\"}}]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate Overlay Plot with Telemetry Object\\nchrome\",\"modified\":1732413603880,\"location\":\"mine\",\"created\":1732413601740,\"persisted\":1732413603880},\"c6100044-56be-44b3-acca-6b9fddfb3849\":{\"name\":\"VIPER Rover Heading\",\"type\":\"generator\",\"identifier\":{\"key\":\"c6100044-56be-44b3-acca-6b9fddfb3849\",\"namespace\":\"\"},\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":0,\"infinityValues\":false,\"exceedFloat32\":false,\"staleness\":false},\"modified\":1732413602460,\"location\":\"mine\",\"created\":1732413602460,\"persisted\":1732413602460}}"
|
||||
},
|
||||
{
|
||||
"name": "mct-recent-objects",
|
||||
"value": "[{\"objectPath\":[{\"identifier\":{\"key\":\"c6100044-56be-44b3-acca-6b9fddfb3849\",\"namespace\":\"\"},\"name\":\"VIPER Rover Heading\",\"type\":\"generator\",\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":0,\"infinityValues\":false,\"exceedFloat32\":false,\"staleness\":false},\"modified\":1732413602460,\"location\":\"mine\",\"created\":1732413602460,\"persisted\":1732413602460},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"e78ca721-fb5e-409b-bf6d-597c87cb716f\",\"namespace\":\"\"},{\"key\":\"c6100044-56be-44b3-acca-6b9fddfb3849\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413602460,\"created\":1732413600960,\"persisted\":1732413602460},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/c6100044-56be-44b3-acca-6b9fddfb3849\",\"domainObject\":{\"identifier\":{\"key\":\"c6100044-56be-44b3-acca-6b9fddfb3849\",\"namespace\":\"\"},\"name\":\"VIPER Rover Heading\",\"type\":\"generator\",\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":0,\"infinityValues\":false,\"exceedFloat32\":false,\"staleness\":false},\"modified\":1732413602460,\"location\":\"mine\",\"created\":1732413602460,\"persisted\":1732413602460}},{\"objectPath\":[{\"identifier\":{\"key\":\"e78ca721-fb5e-409b-bf6d-597c87cb716f\",\"namespace\":\"\"},\"name\":\"Overlay Plot with Telemetry Object\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"c6100044-56be-44b3-acca-6b9fddfb3849\",\"namespace\":\"\"}],\"configuration\":{\"series\":[{\"identifier\":{\"key\":\"c6100044-56be-44b3-acca-6b9fddfb3849\",\"namespace\":\"\"}}]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate Overlay Plot with Telemetry Object\\nchrome\",\"modified\":1732413603880,\"location\":\"mine\",\"created\":1732413601740,\"persisted\":1732413603880},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"e78ca721-fb5e-409b-bf6d-597c87cb716f\",\"namespace\":\"\"},{\"key\":\"c6100044-56be-44b3-acca-6b9fddfb3849\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413602460,\"created\":1732413600960,\"persisted\":1732413602460},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/e78ca721-fb5e-409b-bf6d-597c87cb716f\",\"domainObject\":{\"identifier\":{\"key\":\"e78ca721-fb5e-409b-bf6d-597c87cb716f\",\"namespace\":\"\"},\"name\":\"Overlay Plot with Telemetry Object\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"c6100044-56be-44b3-acca-6b9fddfb3849\",\"namespace\":\"\"}],\"configuration\":{\"series\":[{\"identifier\":{\"key\":\"c6100044-56be-44b3-acca-6b9fddfb3849\",\"namespace\":\"\"}}]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate Overlay Plot with Telemetry Object\\nchrome\",\"modified\":1732413603880,\"location\":\"mine\",\"created\":1732413601740,\"persisted\":1732413603880}},{\"objectPath\":[{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"e78ca721-fb5e-409b-bf6d-597c87cb716f\",\"namespace\":\"\"},{\"key\":\"c6100044-56be-44b3-acca-6b9fddfb3849\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413602460,\"created\":1732413600960,\"persisted\":1732413602460},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine\",\"domainObject\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"e78ca721-fb5e-409b-bf6d-597c87cb716f\",\"namespace\":\"\"},{\"key\":\"c6100044-56be-44b3-acca-6b9fddfb3849\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413602460,\"created\":1732413600960,\"persisted\":1732413602460}}]"
|
||||
"value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"d3014736-1182-4b70-8122-6d0c6ef540e1\",\"namespace\":\"\"},{\"key\":\"8c53d61f-b514-4535-be87-0fb20eb56576\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413605677,\"created\":1732413603298,\"persisted\":1732413605677},\"d3014736-1182-4b70-8122-6d0c6ef540e1\":{\"identifier\":{\"key\":\"d3014736-1182-4b70-8122-6d0c6ef540e1\",\"namespace\":\"\"},\"name\":\"Overlay Plot with Telemetry Object\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"8c53d61f-b514-4535-be87-0fb20eb56576\",\"namespace\":\"\"}],\"configuration\":{\"series\":[]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata @clock\\nGenerate Overlay Plot with Telemetry Object\\nchrome\",\"modified\":1732413607031,\"location\":\"mine\",\"created\":1732413605018,\"persisted\":1732413607031},\"8c53d61f-b514-4535-be87-0fb20eb56576\":{\"name\":\"VIPER Rover Heading\",\"type\":\"generator\",\"identifier\":{\"key\":\"8c53d61f-b514-4535-be87-0fb20eb56576\",\"namespace\":\"\"},\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":0,\"infinityValues\":false,\"exceedFloat32\":false,\"staleness\":false},\"modified\":1732413605677,\"location\":\"mine\",\"created\":1732413605677,\"persisted\":1732413605677}}"
|
||||
},
|
||||
{
|
||||
"name": "mct-tree-expanded",
|
||||
"value": "[]"
|
||||
},
|
||||
{
|
||||
"name": "mct-recent-objects",
|
||||
"value": "[{\"objectPath\":[{\"identifier\":{\"key\":\"8c53d61f-b514-4535-be87-0fb20eb56576\",\"namespace\":\"\"},\"name\":\"VIPER Rover Heading\",\"type\":\"generator\",\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":0,\"infinityValues\":false,\"exceedFloat32\":false,\"staleness\":false},\"modified\":1732413605677,\"location\":\"mine\",\"created\":1732413605677,\"persisted\":1732413605677},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"d3014736-1182-4b70-8122-6d0c6ef540e1\",\"namespace\":\"\"},{\"key\":\"8c53d61f-b514-4535-be87-0fb20eb56576\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413605677,\"created\":1732413603298,\"persisted\":1732413605677},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/8c53d61f-b514-4535-be87-0fb20eb56576\",\"domainObject\":{\"identifier\":{\"key\":\"8c53d61f-b514-4535-be87-0fb20eb56576\",\"namespace\":\"\"},\"name\":\"VIPER Rover Heading\",\"type\":\"generator\",\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":0,\"infinityValues\":false,\"exceedFloat32\":false,\"staleness\":false},\"modified\":1732413605677,\"location\":\"mine\",\"created\":1732413605677,\"persisted\":1732413605677}},{\"objectPath\":[{\"identifier\":{\"key\":\"d3014736-1182-4b70-8122-6d0c6ef540e1\",\"namespace\":\"\"},\"name\":\"Overlay Plot with Telemetry Object\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"8c53d61f-b514-4535-be87-0fb20eb56576\",\"namespace\":\"\"}],\"configuration\":{\"series\":[{\"identifier\":{\"key\":\"8c53d61f-b514-4535-be87-0fb20eb56576\",\"namespace\":\"\"}}]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata @clock\\nGenerate Overlay Plot with Telemetry Object\\nchrome\",\"modified\":1732413607031,\"location\":\"mine\",\"created\":1732413605018,\"persisted\":1732413607031},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"d3014736-1182-4b70-8122-6d0c6ef540e1\",\"namespace\":\"\"},{\"key\":\"8c53d61f-b514-4535-be87-0fb20eb56576\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413605677,\"created\":1732413603298,\"persisted\":1732413605677},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/d3014736-1182-4b70-8122-6d0c6ef540e1\",\"domainObject\":{\"identifier\":{\"key\":\"d3014736-1182-4b70-8122-6d0c6ef540e1\",\"namespace\":\"\"},\"name\":\"Overlay Plot with Telemetry Object\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"8c53d61f-b514-4535-be87-0fb20eb56576\",\"namespace\":\"\"}],\"configuration\":{\"series\":[{\"identifier\":{\"key\":\"8c53d61f-b514-4535-be87-0fb20eb56576\",\"namespace\":\"\"}}]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata @clock\\nGenerate Overlay Plot with Telemetry Object\\nchrome\",\"modified\":1732413607031,\"location\":\"mine\",\"created\":1732413605018,\"persisted\":1732413607031}},{\"objectPath\":[{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"d3014736-1182-4b70-8122-6d0c6ef540e1\",\"namespace\":\"\"},{\"key\":\"8c53d61f-b514-4535-be87-0fb20eb56576\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413605677,\"created\":1732413603298,\"persisted\":1732413605677},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine\",\"domainObject\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"d3014736-1182-4b70-8122-6d0c6ef540e1\",\"namespace\":\"\"},{\"key\":\"8c53d61f-b514-4535-be87-0fb20eb56576\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413605677,\"created\":1732413603298,\"persisted\":1732413605677}}]"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -6,7 +6,7 @@
|
||||
"localStorage": [
|
||||
{
|
||||
"name": "mct",
|
||||
"value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"67ca2e0a-b37e-4eda-86a4-ccdbb228bbc0\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413601720,\"created\":1732413600920,\"persisted\":1732413601720},\"67ca2e0a-b37e-4eda-86a4-ccdbb228bbc0\":{\"identifier\":{\"key\":\"67ca2e0a-b37e-4eda-86a4-ccdbb228bbc0\",\"namespace\":\"\"},\"name\":\"Overlay Plot with 5s Delay\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"8f524b49-ad06-47f9-98e0-087b31a2f3e0\",\"namespace\":\"\"}],\"configuration\":{\"series\":[{\"identifier\":{\"key\":\"8f524b49-ad06-47f9-98e0-087b31a2f3e0\",\"namespace\":\"\"}}]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate Overlay Plot with 5s Delay\\nchrome\",\"modified\":1732413603020,\"location\":\"mine\",\"created\":1732413601720,\"persisted\":1732413603020},\"8f524b49-ad06-47f9-98e0-087b31a2f3e0\":{\"identifier\":{\"key\":\"8f524b49-ad06-47f9-98e0-087b31a2f3e0\",\"namespace\":\"\"},\"name\":\"VIPER Rover Heading\",\"type\":\"generator\",\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":5000,\"infinityValues\":false,\"exceedFloat32\":false,\"staleness\":false},\"modified\":1732413602920,\"location\":\"67ca2e0a-b37e-4eda-86a4-ccdbb228bbc0\",\"created\":1732413602420,\"persisted\":1732413602920}}"
|
||||
"value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"f34f457e-d7f4-4fc4-ba71-52e19e925646\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413605044,\"created\":1732413603140,\"persisted\":1732413605044},\"f34f457e-d7f4-4fc4-ba71-52e19e925646\":{\"identifier\":{\"key\":\"f34f457e-d7f4-4fc4-ba71-52e19e925646\",\"namespace\":\"\"},\"name\":\"Overlay Plot with 5s Delay\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"c568fa66-62e0-4eee-97eb-cdbc7421e556\",\"namespace\":\"\"}],\"configuration\":{\"series\":[{\"identifier\":{\"key\":\"c568fa66-62e0-4eee-97eb-cdbc7421e556\",\"namespace\":\"\"}}]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata @clock\\nGenerate Overlay Plot with 5s Delay\\nchrome\",\"modified\":1732413606208.9,\"location\":\"mine\",\"created\":1732413605044,\"persisted\":1732413606208.9},\"c568fa66-62e0-4eee-97eb-cdbc7421e556\":{\"identifier\":{\"key\":\"c568fa66-62e0-4eee-97eb-cdbc7421e556\",\"namespace\":\"\"},\"name\":\"VIPER Rover Heading\",\"type\":\"generator\",\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":5000,\"infinityValues\":false,\"exceedFloat32\":false,\"staleness\":false},\"modified\":1732413606049,\"location\":\"f34f457e-d7f4-4fc4-ba71-52e19e925646\",\"created\":1732413605554,\"persisted\":1732413606049}}"
|
||||
},
|
||||
{
|
||||
"name": "mct-tree-expanded",
|
||||
|
File diff suppressed because one or more lines are too long
BIN
e2e/test-data/rick space roll.jpg
Normal file
BIN
e2e/test-data/rick space roll.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
@ -19,19 +19,35 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import fs from 'fs';
|
||||
|
||||
import {
|
||||
createDomainObjectWithDefaults,
|
||||
createExampleTelemetryObject,
|
||||
createNotification,
|
||||
createPlanFromJSON,
|
||||
createStableStateTelemetry,
|
||||
expandEntireTree,
|
||||
openObjectTreeContextMenu
|
||||
getCanvasPixels,
|
||||
linkParameterToObject,
|
||||
navigateToObjectWithFixedTimeBounds,
|
||||
navigateToObjectWithRealTime,
|
||||
setEndOffset,
|
||||
setFixedIndependentTimeConductorBounds,
|
||||
setFixedTimeMode,
|
||||
setRealTimeMode,
|
||||
setStartOffset,
|
||||
setTimeConductorBounds,
|
||||
waitForPlotsToRender
|
||||
} from '../../appActions.js';
|
||||
import { assertPlanActivities, setBoundsToSpanAllActivities } from '../../helper/planningUtils.js';
|
||||
import { expect, test } from '../../pluginFixtures.js';
|
||||
|
||||
test.describe('AppActions', () => {
|
||||
test('createDomainObjectsWithDefaults', async ({ page }) => {
|
||||
test.describe('AppActions @framework', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
});
|
||||
test('createDomainObjectsWithDefaults', async ({ page }) => {
|
||||
const e2eFolder = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder',
|
||||
name: 'e2e folder'
|
||||
@ -90,8 +106,39 @@ test.describe('AppActions', () => {
|
||||
expect(folder3.url).toBe(`${e2eFolder.url}/${folder1.uuid}/${folder2.uuid}/${folder3.uuid}`);
|
||||
});
|
||||
});
|
||||
test('createExampleTelemetryObject', async ({ page }) => {
|
||||
const gauge = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Gauge',
|
||||
name: 'Gauge with no data'
|
||||
});
|
||||
|
||||
const swgWithParent = await createExampleTelemetryObject(page, gauge.uuid);
|
||||
|
||||
await page.goto(swgWithParent.url);
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toHaveText(swgWithParent.name);
|
||||
await page.getByLabel('More actions').click();
|
||||
await page.getByLabel('Edit Properties...').click();
|
||||
|
||||
// Check Default values of created object
|
||||
await expect(page.getByLabel('Title', { exact: true })).toHaveValue('VIPER Rover Heading');
|
||||
await expect(page.getByRole('spinbutton', { name: 'Period' })).toHaveValue('10');
|
||||
await expect(page.getByRole('spinbutton', { name: 'Amplitude' })).toHaveValue('1');
|
||||
await expect(page.getByRole('spinbutton', { name: 'Offset' })).toHaveValue('0');
|
||||
await expect(page.getByRole('spinbutton', { name: 'Data Rate (hz)' })).toHaveValue('1');
|
||||
await expect(page.getByRole('spinbutton', { name: 'Phase (radians)' })).toHaveValue('0');
|
||||
await expect(page.getByRole('spinbutton', { name: 'Randomness' })).toHaveValue('0');
|
||||
await expect(page.getByRole('spinbutton', { name: 'Loading Delay (ms)' })).toHaveValue('0');
|
||||
|
||||
await page.getByLabel('Cancel').click();
|
||||
|
||||
const swgWithoutParent = await createExampleTelemetryObject(page);
|
||||
|
||||
await page.getByLabel('Show selected item in tree').click();
|
||||
|
||||
expect(swgWithParent.url).toBe(`${gauge.url}/${swgWithParent.uuid}`);
|
||||
expect(swgWithoutParent.url).toBe(`./#/browse/mine/${swgWithoutParent.uuid}`);
|
||||
});
|
||||
test('createNotification', async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
await createNotification(page, {
|
||||
message: 'Test info notification',
|
||||
severity: 'info'
|
||||
@ -114,9 +161,20 @@ test.describe('AppActions', () => {
|
||||
await expect(page.locator('.c-message-banner')).toHaveClass(/error/);
|
||||
await page.locator('[aria-label="Dismiss"]').click();
|
||||
});
|
||||
test('createPlanFromJSON', async ({ page }) => {
|
||||
const examplePlanSmall1 = JSON.parse(
|
||||
fs.readFileSync(
|
||||
new URL('../../test-data/examplePlans/ExamplePlan_Small1.json', import.meta.url)
|
||||
)
|
||||
);
|
||||
const plan = await createPlanFromJSON(page, {
|
||||
name: 'Test Plan',
|
||||
json: examplePlanSmall1
|
||||
});
|
||||
await setBoundsToSpanAllActivities(page, examplePlanSmall1, plan.url);
|
||||
await assertPlanActivities(page, examplePlanSmall1, plan.url);
|
||||
});
|
||||
test('expandEntireTree', async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
const rootFolder = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder'
|
||||
});
|
||||
@ -152,28 +210,154 @@ test.describe('AppActions', () => {
|
||||
name: 'Main Tree'
|
||||
});
|
||||
const treePaneCollapsedItems = treePane.getByRole('treeitem', { expanded: false });
|
||||
expect(await treePaneCollapsedItems.count()).toBe(0);
|
||||
await expect(treePaneCollapsedItems).toHaveCount(0);
|
||||
|
||||
await page.goto('./#/browse/mine');
|
||||
//Click the Create button
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
|
||||
// Click the object specified by 'type'
|
||||
await page.click(`li[role='menuitem']:text("Clock")`);
|
||||
await page.getByRole('menuitem', { name: 'Clock' }).click();
|
||||
await expandEntireTree(page, 'Create Modal Tree');
|
||||
const locatorTree = page.getByRole('tree', {
|
||||
name: 'Create Modal Tree'
|
||||
});
|
||||
const locatorTreeCollapsedItems = locatorTree.locator('role=treeitem[expanded=false]');
|
||||
expect(await locatorTreeCollapsedItems.count()).toBe(0);
|
||||
await expect(locatorTreeCollapsedItems).toHaveCount(0);
|
||||
});
|
||||
test('openObjectTreeContextMenu', async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
const folder = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder'
|
||||
test('getCanvasPixels', async ({ page }) => {
|
||||
let overlayPlot = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Overlay Plot'
|
||||
});
|
||||
await openObjectTreeContextMenu(page, folder.url);
|
||||
await expect(page.getByLabel(`${folder.name} Context Menu`)).toBeVisible();
|
||||
|
||||
await createExampleTelemetryObject(page, overlayPlot.uuid);
|
||||
|
||||
await page.goto(overlayPlot.url);
|
||||
//Get pixel data from Canvas
|
||||
const plotPixels = await getCanvasPixels(page, 'canvas');
|
||||
const plotPixelSize = plotPixels.length;
|
||||
expect(plotPixelSize).toBeGreaterThan(0);
|
||||
});
|
||||
test('navigateToObjectWithFixedTimeBounds', async ({ page }) => {
|
||||
const exampleTelemetry = await createExampleTelemetryObject(page);
|
||||
//Navigate without explicit bounds
|
||||
await navigateToObjectWithFixedTimeBounds(page, exampleTelemetry.url);
|
||||
await expect(page.getByLabel('Start bounds:')).toBeVisible();
|
||||
await expect(page.getByLabel('End bounds:')).toBeVisible();
|
||||
//Navigate with explicit bounds
|
||||
await navigateToObjectWithFixedTimeBounds(
|
||||
page,
|
||||
exampleTelemetry.url,
|
||||
1693592063607,
|
||||
1693593893607
|
||||
);
|
||||
await expect(page.getByLabel('Start bounds: 2023-09-01 18:')).toBeVisible();
|
||||
await expect(page.getByLabel('End bounds: 2023-09-01 18:44:')).toBeVisible();
|
||||
});
|
||||
test('navigateToObjectWithRealTime', async ({ page }) => {
|
||||
const exampleTelemetry = await createExampleTelemetryObject(page);
|
||||
//Navigate without explicit bounds
|
||||
await navigateToObjectWithRealTime(page, exampleTelemetry.url);
|
||||
await expect(page.getByLabel('Start offset:')).toBeVisible();
|
||||
await expect(page.getByLabel('End offset: 00:00:')).toBeVisible();
|
||||
//Navigate with explicit bounds
|
||||
await navigateToObjectWithRealTime(page, exampleTelemetry.url, 1693592063607, 1693593893607);
|
||||
await expect(page.getByLabel('Start offset: 18:14:')).toBeVisible();
|
||||
await expect(page.getByLabel('End offset: 18:44:')).toBeVisible();
|
||||
});
|
||||
test('setTimeConductorMode', async ({ page }) => {
|
||||
await test.step('setFixedTimeMode', async () => {
|
||||
await setFixedTimeMode(page);
|
||||
await expect(page.getByLabel('Start bounds:')).toBeVisible();
|
||||
await expect(page.getByLabel('End bounds:')).toBeVisible();
|
||||
});
|
||||
await test.step('setTimeConductorBounds', async () => {
|
||||
await setTimeConductorBounds(page, {
|
||||
startDate: '2024-01-01',
|
||||
endDate: '2024-01-02',
|
||||
startTime: '00:00:00',
|
||||
endTime: '23:59:59'
|
||||
});
|
||||
await expect(page.getByLabel('Start bounds: 2024-01-01 00:00:00')).toBeVisible();
|
||||
await expect(page.getByLabel('End bounds: 2024-01-02 23:59:59')).toBeVisible();
|
||||
});
|
||||
await test.step('setRealTimeMode', async () => {
|
||||
await setRealTimeMode(page);
|
||||
await expect(page.getByLabel('Start offset')).toBeVisible();
|
||||
await expect(page.getByLabel('End offset')).toBeVisible();
|
||||
});
|
||||
await test.step('setStartOffset', async () => {
|
||||
await setStartOffset(page, {
|
||||
startHours: '04',
|
||||
startMins: '20',
|
||||
startSecs: '22'
|
||||
});
|
||||
await expect(page.getByLabel('Start offset: 04:20:22')).toBeVisible();
|
||||
});
|
||||
await test.step('setEndOffset', async () => {
|
||||
await setEndOffset(page, {
|
||||
endHours: '04',
|
||||
endMins: '20',
|
||||
endSecs: '22'
|
||||
});
|
||||
await expect(page.getByLabel('End offset: 04:20:22')).toBeVisible();
|
||||
});
|
||||
});
|
||||
test('setFixedIndependentTimeConductorBounds', async ({ page }) => {
|
||||
// Create a Display Layout
|
||||
const displayLayout = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Display Layout'
|
||||
});
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Example Imagery',
|
||||
parent: displayLayout.uuid
|
||||
});
|
||||
|
||||
const startDate = '2021-12-30 01:01:00.000Z';
|
||||
const endDate = '2021-12-30 01:11:00.000Z';
|
||||
await setFixedIndependentTimeConductorBounds(page, { start: startDate, end: endDate });
|
||||
|
||||
// check image date
|
||||
await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible();
|
||||
|
||||
// flip it off
|
||||
await page.getByRole('switch').click();
|
||||
// timestamp shouldn't be in the past anymore
|
||||
await expect(page.getByText('2021-12-30 01:11:00.000Z')).toBeHidden();
|
||||
});
|
||||
test.fail('waitForPlotsToRender', async ({ page }) => {
|
||||
// Create a SWG
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator'
|
||||
});
|
||||
// Edit the SWG
|
||||
await page.getByLabel('More actions').click();
|
||||
await page.getByLabel('Edit Properties...').click();
|
||||
// Set loading delay to 10 seconds
|
||||
await page.getByLabel('Loading Delay (ms)', { exact: true }).fill('10000');
|
||||
await page.getByLabel('Save').click();
|
||||
// Reload the page
|
||||
await page.reload();
|
||||
// Expect this step to fail
|
||||
await waitForPlotsToRender(page, { timeout: 1000 });
|
||||
});
|
||||
test('createStableStateTelemetry', async ({ page }) => {
|
||||
const stableStateTelemetry = await createStableStateTelemetry(page);
|
||||
expect(stableStateTelemetry.name).toBe('Stable State Generator');
|
||||
expect(stableStateTelemetry.url).toBe(`./#/browse/mine/${stableStateTelemetry.uuid}`);
|
||||
expect(stableStateTelemetry.uuid).toBeDefined();
|
||||
});
|
||||
test('linkParameterToObject', async ({ page }) => {
|
||||
const displayLayout = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Display Layout',
|
||||
name: 'Test Display Layout'
|
||||
});
|
||||
const exampleTelemetry = await createExampleTelemetryObject(page);
|
||||
|
||||
await linkParameterToObject(page, exampleTelemetry.name, displayLayout.name);
|
||||
await page.goto(displayLayout.url);
|
||||
await expect(page.getByRole('main').getByText('Test Display Layout')).toBeVisible();
|
||||
await expandEntireTree(page);
|
||||
await expect(page.getByLabel('Navigate to VIPER Rover').first()).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
@ -26,7 +26,7 @@ relates to how we've extended it (i.e. ./e2e/baseFixtures.js) and assumptions ma
|
||||
(`npm start` and ./e2e/webpack-dev-middleware.js)
|
||||
*/
|
||||
|
||||
import { expect, test } from '../../baseFixtures.js';
|
||||
import { test } from '../../baseFixtures.js';
|
||||
import { MISSION_TIME } from '../../constants.js';
|
||||
|
||||
test.describe('baseFixtures tests', () => {
|
||||
@ -42,6 +42,21 @@ test.describe('baseFixtures tests', () => {
|
||||
page.waitForEvent('console') // always wait for the event to happen while triggering it!
|
||||
]);
|
||||
});
|
||||
test('Verify that tests fail if console.error is thrown with clock override @clock', async ({
|
||||
page
|
||||
}) => {
|
||||
test.fail();
|
||||
//Set clock time
|
||||
await page.clock.install({ time: MISSION_TIME });
|
||||
await page.clock.resume();
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
//Verify that ../fixtures.js detects console log errors
|
||||
await Promise.all([
|
||||
page.evaluate(() => console.error('This should result in a failure')),
|
||||
page.waitForEvent('console') // always wait for the event to happen while triggering it!
|
||||
]);
|
||||
});
|
||||
test('Verify that tests pass if console.warn is thrown', async ({ page }) => {
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
@ -53,27 +68,3 @@ test.describe('baseFixtures tests', () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('baseFixtures tests @clock', () => {
|
||||
test.use({
|
||||
clockOptions: {
|
||||
now: MISSION_TIME,
|
||||
shouldAdvanceTime: false
|
||||
}
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
});
|
||||
|
||||
test('Can use clockOptions and tick fixtures to control the clock', async ({ page, tick }) => {
|
||||
let time = await page.evaluate(() => new Date().getTime());
|
||||
expect(time).toBe(MISSION_TIME);
|
||||
await tick(1000);
|
||||
time = await page.evaluate(() => new Date().getTime());
|
||||
expect(time).toBe(MISSION_TIME + 1000 * 1);
|
||||
await tick(1000);
|
||||
time = await page.evaluate(() => new Date().getTime());
|
||||
expect(time).toBe(MISSION_TIME + 1000 * 2);
|
||||
});
|
||||
});
|
||||
|
@ -22,7 +22,7 @@
|
||||
|
||||
/*
|
||||
* This test suite template is to be used when creating new test suites. It will be kept up to date with the latest improvements
|
||||
* made by the Open MCT team. It will also follow our best pratices as those evolve. Please use this structure as a _reference_ and clear
|
||||
* made by the Open MCT team. It will also follow our best practices as those evolve. Please use this structure as a _reference_ and clear
|
||||
* or update any references when creating a new test suite!
|
||||
*
|
||||
* To illustrate current best practices, we've included a mocked up test suite for Renaming a Timer domain object.
|
||||
@ -30,7 +30,6 @@
|
||||
* Demonstrated:
|
||||
* - Using appActions to leverage existing functions
|
||||
* - Structure
|
||||
* - @unstable annotation
|
||||
* - await, expect, test, describe syntax
|
||||
* - Writing a custom function for a test suite
|
||||
* - Test stub for unfinished test coverage (test.fixme)
|
||||
@ -45,7 +44,7 @@
|
||||
*/
|
||||
|
||||
// Structure: Some standard Imports. Please update the required pathing.
|
||||
import { createDomainObjectWithDefaults } from '../../appActions.js';
|
||||
import { createDomainObjectWithDefaults, createExampleTelemetryObject } from '../../appActions.js';
|
||||
import { expect, test } from '../../pluginFixtures.js';
|
||||
|
||||
/**
|
||||
@ -53,11 +52,8 @@ import { expect, test } from '../../pluginFixtures.js';
|
||||
* Try to keep a single describe block per logical groups of tests.
|
||||
* If your test runtime exceeds 5 minutes or 500 lines, it's likely that it will need to be split.
|
||||
*
|
||||
* Annotations:
|
||||
* Please use the @unstable tag at the end of the test title so that our automation can pick it up
|
||||
* as a part of our test promotion pipeline.
|
||||
*/
|
||||
test.describe('Renaming Timer Object', () => {
|
||||
test.describe('Example - Renaming Timer Object', () => {
|
||||
// Top-level declaration of the Timer object created in beforeEach().
|
||||
// We can then use this throughout the entire test suite.
|
||||
let timer;
|
||||
@ -70,7 +66,7 @@ test.describe('Renaming Timer Object', () => {
|
||||
timer = await createDomainObjectWithDefaults(page, { type: 'Timer' });
|
||||
|
||||
// Assert the object to be created and check its name in the title
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText(timer.name);
|
||||
await expect(page.getByRole('main')).toContainText(timer.name);
|
||||
});
|
||||
|
||||
/**
|
||||
@ -81,11 +77,11 @@ test.describe('Renaming Timer Object', () => {
|
||||
test('An existing Timer object can be renamed via the 3dot actions menu', async ({ page }) => {
|
||||
const newObjectName = 'Renamed Timer';
|
||||
|
||||
// We've created an example of a shared function which pases the page and newObjectName values
|
||||
// We've created an example of a shared function which passes the page and newObjectName values
|
||||
await renameTimerFrom3DotMenu(page, timer.url, newObjectName);
|
||||
|
||||
// Assert that the name has changed in the browser bar to the value we assigned above
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText(newObjectName);
|
||||
await expect(page.getByRole('main')).toContainText(newObjectName);
|
||||
});
|
||||
|
||||
test('An existing Timer object can be renamed twice', async ({ page }) => {
|
||||
@ -95,13 +91,13 @@ test.describe('Renaming Timer Object', () => {
|
||||
await renameTimerFrom3DotMenu(page, timer.url, newObjectName);
|
||||
|
||||
// Assert that the name has changed in the browser bar to the value we assigned above
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText(newObjectName);
|
||||
await expect(page.getByRole('main')).toContainText(newObjectName);
|
||||
|
||||
// Rename the Timer object again
|
||||
await renameTimerFrom3DotMenu(page, timer.url, newObjectName2);
|
||||
|
||||
// Assert that the name has changed in the browser bar to the second value
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText(newObjectName2);
|
||||
await expect(page.getByRole('main')).toContainText(newObjectName2);
|
||||
});
|
||||
|
||||
/**
|
||||
@ -121,7 +117,7 @@ test.describe('Renaming Timer Object', () => {
|
||||
* The next most important concept in our testing is working with telemetry objects. Telemetry is at the core of Open MCT
|
||||
* and we have developed a great pattern for working with it.
|
||||
*/
|
||||
test.describe('Advanced: Working with telemetry objects', () => {
|
||||
test.describe('Advanced Example - Working with telemetry objects', () => {
|
||||
let displayLayout;
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
@ -132,17 +128,14 @@ test.describe('Advanced: Working with telemetry objects', () => {
|
||||
name: 'Display Layout with Embedded SWG'
|
||||
});
|
||||
// Create Telemetry object within the parent object created above
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
name: 'Telemetry',
|
||||
parent: displayLayout.uuid //reference the display layout in the creation process
|
||||
});
|
||||
//reference the display layout in the creation process
|
||||
await createExampleTelemetryObject(page, displayLayout.uuid);
|
||||
});
|
||||
test('Can directly navigate to a Display Layout with embedded telemetry', async ({ page }) => {
|
||||
//Now you can directly navigate to the displayLayout created in the beforeEach with the embedded telemetry
|
||||
await page.goto(displayLayout.url);
|
||||
//Expect the created Telemetry Object to be visible when directly navigating to the displayLayout
|
||||
await expect(page.getByTitle('Sine')).toBeVisible();
|
||||
await expect(page.getByLabel('Alpha-numeric telemetry name')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@ -160,18 +153,14 @@ test.describe('Advanced: Working with telemetry objects', () => {
|
||||
* @param {string} newNameForTimer New name for object
|
||||
*/
|
||||
async function renameTimerFrom3DotMenu(page, timerUrl, newNameForTimer) {
|
||||
// Navigate to the timer object
|
||||
// Navigate to the timer object directly
|
||||
await page.goto(timerUrl);
|
||||
|
||||
// Click on 3 Dot Menu
|
||||
await page.locator('button[title="More actions"]').click();
|
||||
|
||||
// Click text=Edit Properties...
|
||||
await page.locator('text=Edit Properties...').click();
|
||||
await page.getByLabel('More actions').click();
|
||||
await page.getByLabel('Edit Properties...').click();
|
||||
|
||||
// Rename the timer object
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(newNameForTimer);
|
||||
await page.getByLabel('Title', { exact: true }).fill(newNameForTimer);
|
||||
|
||||
// Click Ok button to Save
|
||||
await page.locator('button:has-text("OK")').click();
|
||||
await page.getByLabel('Save').click();
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ import { fileURLToPath } from 'url';
|
||||
import {
|
||||
createDomainObjectWithDefaults,
|
||||
createExampleTelemetryObject,
|
||||
setIndependentTimeConductorBounds,
|
||||
setFixedIndependentTimeConductorBounds,
|
||||
setTimeConductorBounds
|
||||
} from '../../appActions.js';
|
||||
import { MISSION_TIME } from '../../constants.js';
|
||||
@ -45,14 +45,10 @@ import { expect, test } from '../../pluginFixtures.js';
|
||||
const overlayPlotName = 'Overlay Plot with Telemetry Object';
|
||||
|
||||
test.describe('Generate Visual Test Data @localStorage @generatedata @clock', () => {
|
||||
test.use({
|
||||
clockOptions: {
|
||||
now: MISSION_TIME,
|
||||
shouldAdvanceTime: true
|
||||
}
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Override the clock
|
||||
await page.clock.install({ time: MISSION_TIME });
|
||||
await page.clock.resume();
|
||||
// Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
});
|
||||
@ -112,23 +108,30 @@ test.describe('Generate Visual Test Data @localStorage @generatedata @clock', ()
|
||||
|
||||
await page.goto(parent.url, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
await setIndependentTimeConductorBounds(page, {
|
||||
await setFixedIndependentTimeConductorBounds(page, {
|
||||
start: '2024-11-12 19:11:11.000Z',
|
||||
end: '2024-11-12 20:11:11.000Z'
|
||||
});
|
||||
|
||||
const NEW_GLOBAL_START_BOUNDS = '2024-11-11 19:11:11.000Z';
|
||||
const NEW_GLOBAL_END_BOUNDS = '2024-11-11 20:11:11.000Z';
|
||||
const NEW_GLOBAL_START_DATE = '2024-11-11';
|
||||
const NEW_GLOBAL_START_TIME = '19:11:11';
|
||||
const NEW_GLOBAL_END_DATE = '2024-11-11';
|
||||
const NEW_GLOBAL_END_TIME = '20:11:11';
|
||||
|
||||
await setTimeConductorBounds(page, NEW_GLOBAL_START_BOUNDS, NEW_GLOBAL_END_BOUNDS);
|
||||
await setTimeConductorBounds(page, {
|
||||
startDate: NEW_GLOBAL_START_DATE,
|
||||
startTime: NEW_GLOBAL_START_TIME,
|
||||
endDate: NEW_GLOBAL_END_DATE,
|
||||
endTime: NEW_GLOBAL_END_TIME
|
||||
});
|
||||
|
||||
// Verify that the global time conductor bounds have been updated
|
||||
expect(
|
||||
await page.getByLabel('Global Time Conductor').getByLabel('Start bounds').textContent()
|
||||
).toEqual(NEW_GLOBAL_START_BOUNDS);
|
||||
expect(
|
||||
await page.getByLabel('Global Time Conductor').getByLabel('End bounds').textContent()
|
||||
).toEqual(NEW_GLOBAL_END_BOUNDS);
|
||||
await expect(
|
||||
page.getByLabel(`Start bounds: ${NEW_GLOBAL_START_DATE} ${NEW_GLOBAL_START_TIME}.000Z`)
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByLabel(`End bounds: ${NEW_GLOBAL_END_DATE} ${NEW_GLOBAL_END_TIME}.000Z`)
|
||||
).toBeVisible();
|
||||
|
||||
//Save localStorage for future test execution
|
||||
await context.storageState({
|
||||
@ -204,11 +207,7 @@ test.describe('Generate Visual Test Data @localStorage @generatedata @clock', ()
|
||||
// TODO: Flesh Out Assertions against created Objects
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText(overlayPlotName);
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
await page
|
||||
.getByRole('list', { name: 'Plot Series Properties' })
|
||||
.locator('span')
|
||||
.first()
|
||||
.click();
|
||||
await page.getByLabel('Plot Series Items').getByLabel('Expand').click();
|
||||
|
||||
// TODO: Modify the Overlay Plot to use fixed Scaling
|
||||
// TODO: Verify Autoscaling.
|
||||
@ -267,7 +266,7 @@ test.describe('Generate Visual Test Data @localStorage @generatedata @clock', ()
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=OK').click(),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
page.locator('.c-message-banner__message').hover({ trial: true })
|
||||
]);
|
||||
|
||||
// focus the overlay plot
|
||||
@ -277,7 +276,7 @@ test.describe('Generate Visual Test Data @localStorage @generatedata @clock', ()
|
||||
|
||||
// Clear Recently Viewed
|
||||
await page.getByRole('button', { name: 'Clear Recently Viewed' }).click();
|
||||
await page.getByRole('button', { name: 'OK', exact: true }).click();
|
||||
await page.getByRole('button', { name: 'Ok', exact: true }).click();
|
||||
//Save localStorage for future test execution
|
||||
await context.storageState({
|
||||
path: fileURLToPath(
|
||||
@ -287,6 +286,55 @@ test.describe('Generate Visual Test Data @localStorage @generatedata @clock', ()
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Generate Conditional Styling Data @localStorage @generatedata', () => {
|
||||
test('Generate basic condition set', async ({ page, context }) => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
// Create a Condition Set
|
||||
const conditionSet = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Condition Set',
|
||||
name: 'Test Condition Set'
|
||||
});
|
||||
|
||||
// Create a Telemetry Object (Sine Wave Generator)
|
||||
const swg = await createExampleTelemetryObject(page, conditionSet.uuid);
|
||||
|
||||
// Edit the Telemetry Object to have a 10hz data rate (Gotta go fast!)
|
||||
await page.goto(swg.url);
|
||||
await page.getByLabel('More actions').click();
|
||||
await page.getByRole('menuitem', { name: 'Edit Properties...' }).click();
|
||||
await page.getByLabel('Period', { exact: true }).fill('5');
|
||||
await page.getByLabel('Save').click();
|
||||
|
||||
// Edit the Condition Set
|
||||
await page.goto(conditionSet.url);
|
||||
await page.getByLabel('Edit Object').click();
|
||||
|
||||
// Add a Condition to the Condition Set
|
||||
await page.getByLabel('Add Condition').click();
|
||||
await page.getByLabel('Condition Name Input').first().fill('Test Condition');
|
||||
await page.getByLabel('Condition Output Type').first().selectOption('String');
|
||||
await page.getByLabel('Condition Output String').first().fill('Test Condition Met');
|
||||
|
||||
// Condition: True if sine value > 0 (half the time)
|
||||
await page.getByLabel('Criterion Telemetry Selection').selectOption(swg.name);
|
||||
await page.getByLabel('Criterion Metadata Selection').selectOption('Sine');
|
||||
await page.getByLabel('Criterion Comparison Selection').selectOption('is greater than');
|
||||
await page.getByLabel('Criterion Input').first().fill('0');
|
||||
|
||||
// Rename default condition
|
||||
await page.getByLabel('Condition Output String').nth(1).fill('Test Condition Unmet');
|
||||
await page.getByLabel('Save').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
// Save localStorage for future test execution
|
||||
await context.storageState({
|
||||
path: fileURLToPath(
|
||||
new URL('../../../e2e/test-data/condition_set_storage.json', import.meta.url)
|
||||
)
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Validate Overlay Plot with Telemetry Object @localStorage @generatedata', () => {
|
||||
test.use({
|
||||
storageState: fileURLToPath(
|
||||
@ -300,11 +348,7 @@ test.describe('Validate Overlay Plot with Telemetry Object @localStorage @genera
|
||||
// TODO: Flesh Out Assertions against created Objects
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText(overlayPlotName);
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
await page
|
||||
.getByRole('list', { name: 'Plot Series Properties' })
|
||||
.locator('span')
|
||||
.first()
|
||||
.click();
|
||||
await page.getByLabel('Plot Series Items').getByLabel('Expand').click();
|
||||
|
||||
// TODO: Modify the Overlay Plot to use fixed Scaling
|
||||
// TODO: Verify Autoscaling.
|
||||
@ -345,11 +389,7 @@ test.describe('Validate Overlay Plot with 5s Delay Telemetry Object @localStorag
|
||||
// TODO: Flesh Out Assertions against created Objects
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText(plotName);
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
await page
|
||||
.getByRole('list', { name: 'Plot Series Properties' })
|
||||
.locator('span')
|
||||
.first()
|
||||
.click();
|
||||
await page.getByLabel('Plot Series Items').getByLabel('Expand').click();
|
||||
|
||||
// TODO: Modify the Overlay Plot to use fixed Scaling
|
||||
// TODO: Verify Autoscaling.
|
||||
|
@ -31,7 +31,7 @@ import { test } from '../../pluginFixtures.js';
|
||||
test.describe.skip('pluginFixtures tests', () => {
|
||||
// test.use({ domainObjectName: 'Timer' });
|
||||
// let timerUUID;
|
||||
// test('Creates a timer object @framework @unstable', ({ domainObject }) => {
|
||||
// test('Creates a timer object @framework', ({ domainObject }) => {
|
||||
// const { uuid } = domainObject;
|
||||
// const uuidRegexp = /[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/;
|
||||
// expect(uuid).toMatch(uuidRegexp);
|
||||
|
@ -59,6 +59,6 @@ test.describe('Clear Data Action', () => {
|
||||
|
||||
// Verify that the background image is no longer visible
|
||||
await expect(page.locator(backgroundImageSelector)).toBeHidden();
|
||||
expect(await page.locator('.c-thumb__image').count()).toBe(0);
|
||||
await expect(page.locator('.c-thumb__image')).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
|
@ -41,7 +41,7 @@ test.describe('CouchDB Status Indicator with mocked responses @couchdb', () => {
|
||||
|
||||
//Go to baseURL
|
||||
await page.goto('./#/browse/mine?hideTree=true&hideInspector=true', {
|
||||
waitUntil: 'networkidle'
|
||||
waitUntil: 'domcontentloaded'
|
||||
});
|
||||
await expect(page.locator('div:has-text("CouchDB is connected")').nth(3)).toBeVisible();
|
||||
});
|
||||
@ -56,7 +56,7 @@ test.describe('CouchDB Status Indicator with mocked responses @couchdb', () => {
|
||||
|
||||
//Go to baseURL
|
||||
await page.goto('./#/browse/mine?hideTree=true&hideInspector=true', {
|
||||
waitUntil: 'networkidle'
|
||||
waitUntil: 'domcontentloaded'
|
||||
});
|
||||
await expect(page.locator('div:has-text("CouchDB is offline")').nth(3)).toBeVisible();
|
||||
});
|
||||
@ -71,7 +71,7 @@ test.describe('CouchDB Status Indicator with mocked responses @couchdb', () => {
|
||||
|
||||
//Go to baseURL
|
||||
await page.goto('./#/browse/mine?hideTree=true&hideInspector=true', {
|
||||
waitUntil: 'networkidle'
|
||||
waitUntil: 'domcontentloaded'
|
||||
});
|
||||
await expect(page.locator('div:has-text("CouchDB connectivity unknown")').nth(3)).toBeVisible();
|
||||
});
|
||||
|
@ -0,0 +1,67 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import { createDomainObjectWithDefaults, setRealTimeMode } from '../../../appActions.js';
|
||||
import { MISSION_TIME } from '../../../constants.js';
|
||||
import { expect, test } from '../../../pluginFixtures.js';
|
||||
|
||||
const TELEMETRY_RATE = 2500;
|
||||
|
||||
test.describe('Example Event Generator Acknowledge with Controlled Clock @clock', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.clock.install({ time: MISSION_TIME });
|
||||
await page.clock.resume();
|
||||
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
await setRealTimeMode(page);
|
||||
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Event Message Generator with Acknowledge'
|
||||
});
|
||||
});
|
||||
|
||||
test('Rows are updatable in place', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/7938'
|
||||
});
|
||||
|
||||
await test.step('First telemetry datum gets added as new row', async () => {
|
||||
await page.clock.fastForward(TELEMETRY_RATE);
|
||||
const rows = page.getByLabel('table content').getByLabel('Table Row');
|
||||
const acknowledgeCell = rows.first().getByLabel('acknowledge table cell');
|
||||
|
||||
await expect(rows).toHaveCount(1);
|
||||
await expect(acknowledgeCell).not.toHaveAttribute('title', 'OK');
|
||||
});
|
||||
|
||||
await test.step('Incoming Telemetry datum matching an existing rows in place update key has data merged to existing row', async () => {
|
||||
await page.clock.fastForward(TELEMETRY_RATE * 2);
|
||||
const rows = page.getByLabel('table content').getByLabel('Table Row');
|
||||
const acknowledgeCell = rows.first().getByLabel('acknowledge table cell');
|
||||
|
||||
await expect(rows).toHaveCount(1);
|
||||
await expect(acknowledgeCell).toHaveAttribute('title', 'OK');
|
||||
});
|
||||
});
|
||||
});
|
@ -41,7 +41,7 @@ test.describe('Sine Wave Generator', () => {
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
|
||||
// Click Sine Wave Generator
|
||||
await page.click('text=Sine Wave Generator');
|
||||
await page.getByRole('menuitem', { name: 'Sine Wave Generator' }).click();
|
||||
|
||||
// Verify that the each required field has required indicator
|
||||
// Title
|
||||
@ -107,11 +107,11 @@ test.describe('Sine Wave Generator', () => {
|
||||
await page.locator('.field.control.l-input-sm input').first().press('ArrowUp');
|
||||
await page.locator('.field.control.l-input-sm input').first().press('ArrowUp');
|
||||
|
||||
const value = await page.locator('.field.control.l-input-sm input').first().inputValue();
|
||||
await expect(value).toBe('6');
|
||||
const value = page.locator('.field.control.l-input-sm input').first();
|
||||
await expect(value).toHaveValue('6');
|
||||
|
||||
//Click text=OK
|
||||
await Promise.all([page.waitForNavigation(), page.click('button:has-text("OK")')]);
|
||||
//Click save button
|
||||
await page.getByLabel('Save').click();
|
||||
|
||||
// Verify that the Sine Wave Generator is displayed and correct
|
||||
// Verify object properties
|
||||
|
@ -45,25 +45,23 @@ test.describe('Form Validation Behavior', () => {
|
||||
await page.getByRole('menuitem', { name: 'Folder' }).click();
|
||||
|
||||
// Fill in empty string into title and trigger validation with 'Tab'
|
||||
await page.click('text=Properties Title Notes >> input[type="text"]');
|
||||
await page.fill('text=Properties Title Notes >> input[type="text"]', '');
|
||||
await page.press('text=Properties Title Notes >> input[type="text"]', 'Tab');
|
||||
await page.getByLabel('Title', { exact: true }).fill('');
|
||||
await page.getByLabel('Title', { exact: true }).press('Tab');
|
||||
|
||||
//Required Field Form Validation
|
||||
await expect(page.locator('button:has-text("OK")')).toBeDisabled();
|
||||
await expect(page.getByLabel('Save')).toBeDisabled();
|
||||
await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(/invalid/);
|
||||
|
||||
//Correct Form Validation for missing title and trigger validation with 'Tab'
|
||||
await page.click('text=Properties Title Notes >> input[type="text"]');
|
||||
await page.fill('text=Properties Title Notes >> input[type="text"]', TEST_FOLDER);
|
||||
await page.press('text=Properties Title Notes >> input[type="text"]', 'Tab');
|
||||
await page.getByLabel('Title', { exact: true }).fill(TEST_FOLDER);
|
||||
await page.getByLabel('Title', { exact: true }).press('Tab');
|
||||
|
||||
//Required Field Form Validation is corrected
|
||||
await expect(page.locator('button:has-text("OK")')).toBeEnabled();
|
||||
await expect(page.getByLabel('Save')).toBeEnabled();
|
||||
await expect(page.locator('.c-form-row__state-indicator').first()).not.toHaveClass(/invalid/);
|
||||
|
||||
//Finish Creating Domain Object
|
||||
await Promise.all([page.waitForNavigation(), page.click('button:has-text("OK")')]);
|
||||
await page.getByLabel('Save').click();
|
||||
|
||||
//Verify that the Domain Object has been created with the corrected title property
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText(TEST_FOLDER);
|
||||
@ -87,8 +85,8 @@ test.describe('Form File Input Behavior', () => {
|
||||
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
const type = await page.locator('#file-input-type').textContent();
|
||||
await expect(type).toBe(`"string"`);
|
||||
const type = page.locator('#file-input-type');
|
||||
await expect(type).toHaveText(`"string"`);
|
||||
});
|
||||
|
||||
test('Can select an image file type', async ({ page }) => {
|
||||
@ -101,8 +99,8 @@ test.describe('Form File Input Behavior', () => {
|
||||
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
const type = await page.locator('#file-input-type').textContent();
|
||||
await expect(type).toBe(`"object"`);
|
||||
const type = page.locator('#file-input-type');
|
||||
await expect(type).toHaveText(`"object"`);
|
||||
});
|
||||
});
|
||||
|
||||
@ -123,16 +121,16 @@ test.describe('Persistence operations @addInit', () => {
|
||||
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
|
||||
await page.click('text=Condition Set');
|
||||
await page.getByRole('menuitem', { name: 'Condition Set' }).click();
|
||||
|
||||
await page.locator('form[name="mctForm"] >> text=Persistence Testing').click();
|
||||
|
||||
const okButton = page.locator('button:has-text("OK")');
|
||||
const okButton = page.getByLabel('Save');
|
||||
await expect(okButton).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Persistence operations @couchdb', () => {
|
||||
test.describe('Persistence operations @couchdb @network', () => {
|
||||
test.use({ failOnConsoleError: false });
|
||||
test('Editing object properties should generate a single persistence operation', async ({
|
||||
page
|
||||
@ -158,14 +156,12 @@ test.describe('Persistence operations @couchdb', () => {
|
||||
});
|
||||
|
||||
// Open the edit form for the clock object
|
||||
await page.click('button[title="More actions"]');
|
||||
await page.click('li[title="Edit properties of this object."]');
|
||||
await page.getByLabel('More actions').click();
|
||||
await page.getByLabel('Edit Properties...').click();
|
||||
|
||||
// Modify the display format from default 12hr -> 24hr and click 'Save'
|
||||
await page
|
||||
.locator('select[aria-label="12 or 24 hour clock"]')
|
||||
.selectOption({ value: 'clock24' });
|
||||
await page.click('button[aria-label="Save"]');
|
||||
await page.getByLabel('12 or 24 hour clock').selectOption({ value: 'clock24' });
|
||||
await page.getByLabel('Save').click();
|
||||
|
||||
await expect
|
||||
.poll(() => putRequestCount, {
|
||||
@ -174,7 +170,7 @@ test.describe('Persistence operations @couchdb', () => {
|
||||
})
|
||||
.toEqual(1);
|
||||
});
|
||||
test('Can create an object after a conflict error @couchdb @2p', async ({
|
||||
test('Can create an object after a conflict error @couchdb @network @2p', async ({
|
||||
page,
|
||||
openmctConfig
|
||||
}) => {
|
||||
@ -188,8 +184,8 @@ test.describe('Persistence operations @couchdb', () => {
|
||||
|
||||
// Both pages: Go to baseURL
|
||||
await Promise.all([
|
||||
page.goto('./', { waitUntil: 'networkidle' }),
|
||||
page2.goto('./', { waitUntil: 'networkidle' })
|
||||
page.goto('./', { waitUntil: 'domcontentloaded' }),
|
||||
page2.goto('./', { waitUntil: 'domcontentloaded' })
|
||||
]);
|
||||
|
||||
//Slow down the test a bit
|
||||
@ -202,14 +198,14 @@ test.describe('Persistence operations @couchdb', () => {
|
||||
|
||||
// Both pages: Click the Create button
|
||||
await Promise.all([
|
||||
page.click('button:has-text("Create")'),
|
||||
page2.click('button:has-text("Create")')
|
||||
page.getByRole('button', { name: 'Create' }).click(),
|
||||
page2.getByRole('button', { name: 'Create' }).click()
|
||||
]);
|
||||
|
||||
// Both pages: Click "Clock" in the Create menu
|
||||
await Promise.all([
|
||||
page.click(`li[role='menuitem']:text("Clock")`),
|
||||
page2.click(`li[role='menuitem']:text("Clock")`)
|
||||
page.getByRole('menuitem', { name: 'Clock' }).click(),
|
||||
page2.getByRole('menuitem', { name: 'Clock' }).click()
|
||||
]);
|
||||
|
||||
// Generate unique names for both objects
|
||||
@ -236,9 +232,9 @@ test.describe('Persistence operations @couchdb', () => {
|
||||
// conditions for a conflict error from the first page.
|
||||
await Promise.all([
|
||||
page2.waitForLoadState(),
|
||||
page2.click('[aria-label="Save"]'),
|
||||
page2.getByLabel('Save').click(),
|
||||
// Wait for Save Banner to appear
|
||||
page2.waitForSelector('.c-message-banner__message')
|
||||
page2.locator('.c-message-banner__message').hover({ trial: true })
|
||||
]);
|
||||
|
||||
// Close Page 2, we're done with it.
|
||||
@ -249,9 +245,9 @@ test.describe('Persistence operations @couchdb', () => {
|
||||
// the composition of the parent folder.
|
||||
await Promise.all([
|
||||
page.waitForLoadState(),
|
||||
page.click('[aria-label="Save"]'),
|
||||
page.getByLabel('Save').click(),
|
||||
// Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
page.locator('.c-message-banner__message').hover({ trial: true })
|
||||
]);
|
||||
|
||||
// Page 1: Verify that the conflict has occurred and an error notification is displayed.
|
||||
|
@ -50,13 +50,13 @@ test.describe('Notifications List', () => {
|
||||
});
|
||||
|
||||
// Verify that there is a button with aria-label "Review 2 Notifications"
|
||||
expect(await page.locator('button[aria-label="Review 2 Notifications"]').count()).toBe(1);
|
||||
await expect(page.locator('button[aria-label="Review 2 Notifications"]')).toHaveCount(1);
|
||||
|
||||
// Click on button with aria-label "Review 2 Notifications"
|
||||
await page.click('button[aria-label="Review 2 Notifications"]');
|
||||
await page.getByLabel('Review 2 Notifications').click();
|
||||
|
||||
// Click on button with aria-label="Dismiss notification of Error message"
|
||||
await page.click('button[aria-label="Dismiss notification of Error message"]');
|
||||
await page.getByLabel('Dismiss notification of Error message').click();
|
||||
|
||||
// Verify there is no a notification (listitem) with the text "Error message" since it was dismissed
|
||||
expect(await page.locator('div[role="dialog"] div[role="listitem"]').innerText()).not.toContain(
|
||||
@ -69,10 +69,10 @@ test.describe('Notifications List', () => {
|
||||
);
|
||||
|
||||
// Click on button with aria-label="Dismiss notification of Alert message"
|
||||
await page.click('button[aria-label="Dismiss notification of Alert message"]');
|
||||
await page.getByLabel('Dismiss notification of Alert message').click();
|
||||
|
||||
// Verify that there is no dialog since the notification overlay was closed automatically after all notifications were dismissed
|
||||
expect(await page.locator('div[role="dialog"]').count()).toBe(0);
|
||||
await expect(page.locator('div[role="dialog"]')).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -21,7 +21,7 @@
|
||||
*****************************************************************************/
|
||||
import fs from 'fs';
|
||||
|
||||
import { createPlanFromJSON } from '../../../appActions.js';
|
||||
import { createPlanFromJSON, navigateToObjectWithFixedTimeBounds } from '../../../appActions.js';
|
||||
import {
|
||||
addPlanGetInterceptor,
|
||||
assertPlanActivities,
|
||||
@ -81,9 +81,7 @@ test.describe('Plan', () => {
|
||||
}
|
||||
|
||||
// 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`
|
||||
);
|
||||
await navigateToObjectWithFixedTimeBounds(page, plan.url, startBound, endBound);
|
||||
|
||||
// select the first activity in the list
|
||||
await page.getByText('Past event 1').click();
|
||||
|
@ -21,7 +21,11 @@
|
||||
*****************************************************************************/
|
||||
import fs from 'fs';
|
||||
|
||||
import { createDomainObjectWithDefaults, createPlanFromJSON } from '../../../appActions.js';
|
||||
import {
|
||||
createDomainObjectWithDefaults,
|
||||
createPlanFromJSON,
|
||||
navigateToObjectWithFixedTimeBounds
|
||||
} from '../../../appActions.js';
|
||||
import { expect, test } from '../../../pluginFixtures.js';
|
||||
|
||||
const examplePlanSmall1 = JSON.parse(
|
||||
@ -59,14 +63,12 @@ test.describe('Time List', () => {
|
||||
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`
|
||||
);
|
||||
await navigateToObjectWithFixedTimeBounds(page, timelist.url, startBound, endBound);
|
||||
|
||||
// Verify all events are displayed
|
||||
const eventCount = await page.getByRole('row').count();
|
||||
// subtracting one for the header
|
||||
await expect(eventCount - 1).toEqual(firstGroupItems.length);
|
||||
expect(eventCount - 1).toEqual(firstGroupItems.length);
|
||||
});
|
||||
|
||||
await test.step('Does not show milliseconds in times', async () => {
|
||||
@ -124,15 +126,13 @@ test("View a timelist in expanded view, verify all the activities are displayed
|
||||
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`
|
||||
);
|
||||
await navigateToObjectWithFixedTimeBounds(page, timelist.url, startBound, endBound);
|
||||
|
||||
// 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();
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
// Switch to expanded view and save the setting
|
||||
await page.getByLabel('Display Style').selectOption({ label: 'Expanded' });
|
||||
|
||||
@ -142,7 +142,7 @@ test("View a timelist in expanded view, verify all the activities are displayed
|
||||
|
||||
// Verify all events are displayed
|
||||
const eventCount = await page.getByRole('row').count();
|
||||
await expect(eventCount).toEqual(firstGroupItems.length);
|
||||
expect(eventCount).toEqual(firstGroupItems.length);
|
||||
});
|
||||
|
||||
await test.step('Shows activity properties when a row is selected in the expanded view', async () => {
|
||||
@ -158,7 +158,7 @@ test("View a timelist in expanded view, verify all the activities are displayed
|
||||
|
||||
await test.step("Verify absence of progress indication for an activity that's not in progress", async () => {
|
||||
// When an activity is not in progress, the progress pie is not visible
|
||||
const hidden = await page.getByRole('row').locator('path').nth(1).isHidden();
|
||||
await expect(hidden).toBe(true);
|
||||
const hidden = page.getByRole('row').locator('path').nth(1);
|
||||
await expect(hidden).toBeHidden();
|
||||
});
|
||||
});
|
||||
|
@ -22,12 +22,16 @@
|
||||
|
||||
/*
|
||||
Collection of Time List tests set to run with browser clock manipulate made possible with the
|
||||
clockOptions plugin fixture.
|
||||
page.clock() API.
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
|
||||
import { createDomainObjectWithDefaults, createPlanFromJSON } from '../../../appActions.js';
|
||||
import {
|
||||
createDomainObjectWithDefaults,
|
||||
createPlanFromJSON,
|
||||
navigateToObjectWithRealTime
|
||||
} from '../../../appActions.js';
|
||||
import {
|
||||
createTimelistWithPlanAndSetActivityInProgress,
|
||||
getEarliestStartTime,
|
||||
@ -50,8 +54,7 @@ const examplePlanSmall1 = JSON.parse(
|
||||
const TIME_TO_FROM_COLUMN = 2;
|
||||
const HEADER_ROW = 0;
|
||||
const NUM_COLUMNS = 5;
|
||||
const FULL_CIRCLE_PATH =
|
||||
'M3.061616997868383e-15,-50A50,50,0,1,1,-3.061616997868383e-15,50A50,50,0,1,1,3.061616997868383e-15,-50Z';
|
||||
const FULL_CIRCLE_PATH = 'M0,-50A50,50,0,1,1,0,50A50,50,0,1,1,0,-50Z';
|
||||
|
||||
/**
|
||||
* The regular expression used to parse the countdown string.
|
||||
@ -93,14 +96,12 @@ const COUNTDOWN = Object.freeze({
|
||||
SECONDS: 5
|
||||
});
|
||||
|
||||
const FIRST_ACTIVITY_SMALL_1 = getFirstActivity(examplePlanSmall1);
|
||||
|
||||
test.describe('Time List with controlled clock @clock', () => {
|
||||
test.use({
|
||||
clockOptions: {
|
||||
now: getEarliestStartTime(examplePlanSmall3),
|
||||
shouldAdvanceTime: true
|
||||
}
|
||||
});
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.clock.install({ time: getEarliestStartTime(examplePlanSmall3) });
|
||||
await page.clock.resume();
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
// Create Time List
|
||||
const timelist = await createDomainObjectWithDefaults(page, {
|
||||
@ -115,9 +116,7 @@ test.describe('Time List with controlled clock @clock', () => {
|
||||
});
|
||||
|
||||
// Navigate to the Time List in real-time mode
|
||||
await page.goto(
|
||||
`${timelist.url}?tc.mode=local&tc.startDelta=900000&tc.endDelta=1800000&tc.timeSystem=utc&view=grid`
|
||||
);
|
||||
await navigateToObjectWithRealTime(page, timelist.url, 900000, 1800000);
|
||||
|
||||
//Expand the viewport to show the entire time list
|
||||
await page.getByLabel('Collapse Inspect Pane').click();
|
||||
@ -172,16 +171,9 @@ test.describe('Time List with controlled clock @clock', () => {
|
||||
});
|
||||
|
||||
test.describe('Activity progress when activity is in the future @clock', () => {
|
||||
const firstActivity = getFirstActivity(examplePlanSmall1);
|
||||
|
||||
test.use({
|
||||
clockOptions: {
|
||||
now: firstActivity.start - 1,
|
||||
shouldAdvanceTime: true
|
||||
}
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.clock.install({ time: FIRST_ACTIVITY_SMALL_1.start - 1 });
|
||||
await page.clock.resume();
|
||||
await createTimelistWithPlanAndSetActivityInProgress(page, examplePlanSmall1);
|
||||
});
|
||||
|
||||
@ -195,18 +187,13 @@ test.describe('Activity progress when activity is in the future @clock', () => {
|
||||
});
|
||||
|
||||
test.describe('Activity progress when now is between start and end of the activity @clock', () => {
|
||||
const firstActivity = getFirstActivity(examplePlanSmall1);
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.clock.install({ time: FIRST_ACTIVITY_SMALL_1.start + 50000 });
|
||||
await page.clock.resume();
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
await createTimelistWithPlanAndSetActivityInProgress(page, examplePlanSmall1);
|
||||
});
|
||||
|
||||
test.use({
|
||||
clockOptions: {
|
||||
now: firstActivity.start + 50000,
|
||||
shouldAdvanceTime: true
|
||||
}
|
||||
});
|
||||
|
||||
test('progress pie is partially filled', async ({ page }) => {
|
||||
const anActivity = page.getByRole('row').nth(0);
|
||||
const pathElement = anActivity.getByLabel('Activity in progress').locator('path');
|
||||
@ -216,16 +203,10 @@ test.describe('Activity progress when now is between start and end of the activi
|
||||
});
|
||||
|
||||
test.describe('Activity progress when now is after end of the activity @clock', () => {
|
||||
const firstActivity = getFirstActivity(examplePlanSmall1);
|
||||
|
||||
test.use({
|
||||
clockOptions: {
|
||||
now: firstActivity.end + 10000,
|
||||
shouldAdvanceTime: true
|
||||
}
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.clock.install({ time: FIRST_ACTIVITY_SMALL_1.end + 10000 });
|
||||
await page.clock.resume();
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
await createTimelistWithPlanAndSetActivityInProgress(page, examplePlanSmall1);
|
||||
});
|
||||
|
||||
|
@ -23,7 +23,10 @@
|
||||
import {
|
||||
createDomainObjectWithDefaults,
|
||||
createPlanFromJSON,
|
||||
setIndependentTimeConductorBounds
|
||||
navigateToObjectWithFixedTimeBounds,
|
||||
setFixedIndependentTimeConductorBounds,
|
||||
setFixedTimeMode,
|
||||
setTimeConductorBounds
|
||||
} from '../../../appActions.js';
|
||||
import { expect, test } from '../../../pluginFixtures.js';
|
||||
|
||||
@ -73,7 +76,40 @@ const testPlan = {
|
||||
};
|
||||
|
||||
test.describe('Time Strip', () => {
|
||||
test('Create two Time Strips, add a single Plan to both, and verify they can have separate Independent Time Contexts @unstable', async ({
|
||||
let timestrip;
|
||||
let plan;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Goto baseURL
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
timestrip = await test.step('Create a Time Strip', async () => {
|
||||
const createdTimeStrip = await createDomainObjectWithDefaults(page, { type: 'Time Strip' });
|
||||
const objectName = await page.locator('.l-browse-bar__object-name').innerText();
|
||||
expect(objectName).toBe(createdTimeStrip.name);
|
||||
|
||||
return createdTimeStrip;
|
||||
});
|
||||
|
||||
plan = await test.step('Create a Plan and add it to the timestrip', async () => {
|
||||
const createdPlan = await createPlanFromJSON(page, {
|
||||
name: 'Test Plan',
|
||||
json: testPlan
|
||||
});
|
||||
|
||||
await page.goto(timestrip.url);
|
||||
// Expand the tree to show the plan
|
||||
await page.getByLabel('Show selected item in tree').click();
|
||||
await page
|
||||
.getByLabel(`Navigate to ${createdPlan.name}`)
|
||||
.dragTo(page.getByLabel('Object View'));
|
||||
await page.getByLabel('Save').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
return createdPlan;
|
||||
});
|
||||
});
|
||||
test('Create two Time Strips, add a single Plan to both, and verify they can have separate Independent Time Contexts', async ({
|
||||
page
|
||||
}) => {
|
||||
test.info().annotations.push({
|
||||
@ -84,42 +120,16 @@ test.describe('Time Strip', () => {
|
||||
// Constant locators
|
||||
const activityBounds = page.locator('.activity-bounds');
|
||||
|
||||
// Goto baseURL
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
const timestrip = await test.step('Create a Time Strip', async () => {
|
||||
const createdTimeStrip = await createDomainObjectWithDefaults(page, { type: 'Time Strip' });
|
||||
const objectName = await page.locator('.l-browse-bar__object-name').innerText();
|
||||
expect(objectName).toBe(createdTimeStrip.name);
|
||||
|
||||
return createdTimeStrip;
|
||||
});
|
||||
|
||||
const plan = await test.step('Create a Plan and add it to the timestrip', async () => {
|
||||
const createdPlan = await createPlanFromJSON(page, {
|
||||
name: 'Test Plan',
|
||||
json: testPlan
|
||||
});
|
||||
|
||||
await page.goto(timestrip.url);
|
||||
// Expand the tree to show the plan
|
||||
await page.click("button[title='Show selected item in tree']");
|
||||
await page.dragAndDrop(`role=treeitem[name=/${createdPlan.name}/]`, '.c-object-view');
|
||||
await page.click("button[title='Save']");
|
||||
await page.click("li[title='Save and Finish Editing']");
|
||||
await test.step('Set time strip to fixed timespan mode and verify activities', async () => {
|
||||
const startBound = testPlan.TEST_GROUP[0].start;
|
||||
const endBound = testPlan.TEST_GROUP[testPlan.TEST_GROUP.length - 1].end;
|
||||
|
||||
// Switch to fixed time mode with all plan events within the bounds
|
||||
await page.goto(
|
||||
`${timestrip.url}?tc.mode=fixed&tc.startBound=${startBound}&tc.endBound=${endBound}&tc.timeSystem=utc&view=time-strip.view`
|
||||
);
|
||||
await navigateToObjectWithFixedTimeBounds(page, timestrip.url, startBound, endBound);
|
||||
|
||||
// Verify all events are displayed
|
||||
const eventCount = await page.locator('.activity-bounds').count();
|
||||
expect(eventCount).toEqual(testPlan.TEST_GROUP.length);
|
||||
|
||||
return createdPlan;
|
||||
});
|
||||
|
||||
await test.step('TimeStrip can use the Independent Time Conductor', async () => {
|
||||
@ -131,7 +141,7 @@ test.describe('Time Strip', () => {
|
||||
const startBoundString = new Date(startBound).toISOString().replace('T', ' ');
|
||||
const endBoundString = new Date(endBound).toISOString().replace('T', ' ');
|
||||
|
||||
await setIndependentTimeConductorBounds(page, {
|
||||
await setFixedIndependentTimeConductorBounds(page, {
|
||||
start: startBoundString,
|
||||
end: endBoundString
|
||||
});
|
||||
@ -149,9 +159,9 @@ test.describe('Time Strip', () => {
|
||||
expect(objectName).toBe(createdTimeStrip.name);
|
||||
|
||||
// Drag the existing Plan onto the newly created Time Strip, and save.
|
||||
await page.dragAndDrop(`role=treeitem[name=/${plan.name}/]`, '.c-object-view');
|
||||
await page.click("button[title='Save']");
|
||||
await page.click("li[title='Save and Finish Editing']");
|
||||
await page.getByLabel(`Navigate to ${plan.name}`).dragTo(page.getByLabel('Object View'));
|
||||
await page.getByLabel('Save').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
// All events should be displayed at this point because the
|
||||
// initial independent context bounds will match the global bounds
|
||||
@ -163,7 +173,7 @@ test.describe('Time Strip', () => {
|
||||
const startBoundString = new Date(startBound).toISOString().replace('T', ' ');
|
||||
const endBoundString = new Date(endBound).toISOString().replace('T', ' ');
|
||||
|
||||
await setIndependentTimeConductorBounds(page, {
|
||||
await setFixedIndependentTimeConductorBounds(page, {
|
||||
start: startBoundString,
|
||||
end: endBoundString
|
||||
});
|
||||
@ -176,4 +186,48 @@ test.describe('Time Strip', () => {
|
||||
expect(await activityBounds.count()).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
test('Time strip now line', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/7817'
|
||||
});
|
||||
|
||||
await test.step('Is displayed in realtime mode', async () => {
|
||||
await expect(page.getByLabel('Now Marker')).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Is hidden when out of bounds of the time axis', async () => {
|
||||
// Switch to fixed timespan mode
|
||||
await setFixedTimeMode(page);
|
||||
// Get the end bounds
|
||||
const endBounds = await page.getByLabel('End bounds').textContent();
|
||||
|
||||
// Add 2 minutes to end bound datetime and use it as the new end time
|
||||
let endTimeStamp = new Date(endBounds);
|
||||
endTimeStamp.setUTCMinutes(endTimeStamp.getUTCMinutes() + 2);
|
||||
const endDate = endTimeStamp.toISOString().split('T')[0];
|
||||
const milliseconds = endTimeStamp.getMilliseconds();
|
||||
const endTime = endTimeStamp.toISOString().split('T')[1].replace(`.${milliseconds}Z`, '');
|
||||
|
||||
// Subtract 1 minute from the end bound and use it as the new start time
|
||||
let startTimeStamp = new Date(endBounds);
|
||||
startTimeStamp.setUTCMinutes(startTimeStamp.getUTCMinutes() + 1);
|
||||
const startDate = startTimeStamp.toISOString().split('T')[0];
|
||||
const startMilliseconds = startTimeStamp.getMilliseconds();
|
||||
const startTime = startTimeStamp
|
||||
.toISOString()
|
||||
.split('T')[1]
|
||||
.replace(`.${startMilliseconds}Z`, '');
|
||||
// Set fixed timespan mode to the future so that "now" is out of bounds.
|
||||
await setTimeConductorBounds(page, {
|
||||
startDate,
|
||||
endDate,
|
||||
startTime,
|
||||
endTime
|
||||
});
|
||||
|
||||
await expect(page.getByLabel('Now Marker')).toBeHidden();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -41,11 +41,10 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage @2p', () =>
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
|
||||
await page.locator('li[role="menuitem"]:has-text("Condition Set")').click();
|
||||
|
||||
await Promise.all([page.waitForNavigation(), page.click('button:has-text("OK")')]);
|
||||
const conditionSet = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Condition Set',
|
||||
name: 'Unnamed Condition Set'
|
||||
});
|
||||
|
||||
//Save localStorage for future test execution
|
||||
await context.storageState({
|
||||
@ -55,7 +54,7 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage @2p', () =>
|
||||
});
|
||||
|
||||
//Set object identifier from url
|
||||
conditionSetUrl = page.url();
|
||||
conditionSetUrl = conditionSet.url;
|
||||
|
||||
await page.close();
|
||||
});
|
||||
@ -68,44 +67,39 @@ 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 after reload @localStorage', async ({
|
||||
page
|
||||
}) => {
|
||||
//Navigate to baseURL with injected localStorage
|
||||
await page.goto(conditionSetUrl, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
//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.getByRole('main')).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
|
||||
await expect(
|
||||
page.getByLabel('Title inspector properties').getByLabel('inspector property value')
|
||||
).toContainText('Unnamed Condition Set');
|
||||
|
||||
//Reload Page
|
||||
await Promise.all([page.reload(), page.waitForLoadState('networkidle')]);
|
||||
//Reload Page
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
|
||||
//Re-verify after reload
|
||||
await expect(page.getByRole('main')).toContainText('Unnamed Condition Set');
|
||||
|
||||
//Assertions on loaded Condition Set in Inspector
|
||||
await expect(
|
||||
page.getByLabel('Title inspector properties').getByLabel('inspector property value')
|
||||
).toContainText('Unnamed Condition Set');
|
||||
});
|
||||
|
||||
//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;
|
||||
|
||||
await page.goto(conditionSetUrl, { waitUntil: 'networkidle' });
|
||||
await page.goto(conditionSetUrl, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
//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');
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
|
||||
|
||||
//Update the Condition Set properties
|
||||
// Click Edit Button
|
||||
@ -151,7 +145,7 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage @2p', () =>
|
||||
expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
|
||||
|
||||
//Reload Page
|
||||
await Promise.all([page.reload(), page.waitForLoadState('networkidle')]);
|
||||
await Promise.all([page.reload(), page.waitForLoadState('domcontentloaded')]);
|
||||
|
||||
//Verify Main section reflects updated Name Property
|
||||
await expect
|
||||
@ -213,7 +207,7 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage @2p', () =>
|
||||
|
||||
//Feature?
|
||||
//Domain Object is still available by direct URL after delete
|
||||
await page.goto(conditionSetUrl, { waitUntil: 'networkidle' });
|
||||
await page.goto(conditionSetUrl, { waitUntil: 'domcontentloaded' });
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
|
||||
});
|
||||
});
|
||||
@ -267,7 +261,7 @@ test.describe('Basic Condition Set Use', () => {
|
||||
await page.getByLabel('Edit Object').click();
|
||||
|
||||
// Expand the 'My Items' folder in the left tree
|
||||
page.click('button[title="Show selected item in tree"]');
|
||||
await page.getByLabel('Show selected item in tree').click();
|
||||
// Add the Alpha & Beta Sine Wave Generator to the Condition Set and save changes
|
||||
const treePane = page.getByRole('tree', {
|
||||
name: 'Main Tree'
|
||||
@ -371,7 +365,7 @@ test.describe('Basic Condition Set Use', () => {
|
||||
|
||||
// Validate that the condition set is evaluating and outputting
|
||||
// the correct value when the underlying telemetry subscription is active.
|
||||
let outputValue = page.locator('[aria-label="Current Output Value"]');
|
||||
let outputValue = page.getByLabel('Current Output Value');
|
||||
await expect(outputValue).toHaveText('false');
|
||||
|
||||
await page.goto(exampleTelemetry.url);
|
||||
@ -462,7 +456,7 @@ test.describe('Basic Condition Set Use', () => {
|
||||
|
||||
// Validate that the condition set is evaluating and outputting
|
||||
// the correct value when the underlying telemetry subscription is active.
|
||||
let outputValue = page.locator('[aria-label="Current Output Value"]');
|
||||
let outputValue = page.getByLabel('Current Output Value');
|
||||
await expect(outputValue).toHaveText('false');
|
||||
|
||||
await page.goto(exampleTelemetry.url);
|
||||
@ -475,3 +469,81 @@ test.describe('Basic Condition Set Use', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Condition Set Composition', () => {
|
||||
let conditionSet;
|
||||
let exampleTelemetry;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Create Condition Set
|
||||
conditionSet = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Condition Set'
|
||||
});
|
||||
|
||||
// Create Telemetry Object as child to Condition Set
|
||||
exampleTelemetry = await createExampleTelemetryObject(page, conditionSet.uuid);
|
||||
|
||||
// Edit Condition Set
|
||||
await page.goto(conditionSet.url);
|
||||
await page.getByRole('button', { name: 'Edit Object' }).click();
|
||||
|
||||
// Add Condition to Condition Set
|
||||
await page.getByRole('button', { name: 'Add Condition' }).click();
|
||||
|
||||
// Enter Condition Output
|
||||
await page.getByLabel('Condition Name Input').first().fill('Negative');
|
||||
await page.getByLabel('Condition Output Type').first().selectOption({ value: 'string' });
|
||||
await page.getByLabel('Condition Output String').first().fill('Negative');
|
||||
|
||||
// Condition Trigger default is okay so no change needed to form
|
||||
|
||||
// Enter Condition Criterion
|
||||
await page.getByLabel('Criterion Telemetry Selection').first().selectOption({ value: 'all' });
|
||||
await page.getByLabel('Criterion Metadata Selection').first().selectOption({ value: 'sin' });
|
||||
await page
|
||||
.locator('select[aria-label="Criterion Comparison Selection"]')
|
||||
.first()
|
||||
.selectOption({ value: 'lessThan' });
|
||||
await page.getByLabel('Criterion Input').first().fill('0');
|
||||
|
||||
// Save the Condition Set
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
});
|
||||
|
||||
test('You can remove telemetry from a condition set with existing conditions', async ({
|
||||
page
|
||||
}) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/7710'
|
||||
});
|
||||
|
||||
await page.getByLabel('Expand My Items folder').click();
|
||||
await page.getByLabel(`Expand ${conditionSet.name} conditionSet`).click();
|
||||
|
||||
await page
|
||||
.getByLabel(`Navigate to ${exampleTelemetry.name}`, { exact: false })
|
||||
.click({ button: 'right' });
|
||||
|
||||
await page
|
||||
.getByLabel(`${exampleTelemetry.name} Context Menu`)
|
||||
.getByRole('menuitem', { name: 'Remove' })
|
||||
.click();
|
||||
await page.getByRole('button', { name: 'Ok', exact: true }).click();
|
||||
|
||||
await page
|
||||
.getByLabel(`Navigate to ${conditionSet.name} conditionSet Object`, { exact: true })
|
||||
.click();
|
||||
await page.getByRole('button', { name: 'Edit Object' }).click();
|
||||
await page.getByRole('tab', { name: 'Elements' }).click();
|
||||
expect(
|
||||
await page
|
||||
.getByRole('tabpanel', { name: 'Inspector Views' })
|
||||
.getByRole('listitem', { name: exampleTelemetry.name })
|
||||
.count()
|
||||
).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
@ -0,0 +1,499 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
/*
|
||||
This test suite is dedicated to tests which verify the basic operations surrounding conditionSets. Note: this
|
||||
suite is sharing state between tests which is considered an anti-pattern. Implementing in this way to
|
||||
demonstrate some playwright for test developers. This pattern should not be re-used in other CRUD suites.
|
||||
*/
|
||||
|
||||
import {
|
||||
createDomainObjectWithDefaults,
|
||||
createExampleTelemetryObject,
|
||||
setRealTimeMode
|
||||
} from '../../../../appActions.js';
|
||||
import { expect, test } from '../../../../pluginFixtures.js';
|
||||
|
||||
test.describe('Basic Condition Set Use', () => {
|
||||
let conditionSet;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Open a browser, navigate to the main page, and wait until all network events to resolve
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
// Create a new condition set
|
||||
conditionSet = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Condition Set',
|
||||
name: 'Test Condition Set'
|
||||
});
|
||||
});
|
||||
test('Creating a condition defaults the condition name to "Unnamed Condition"', async ({
|
||||
page
|
||||
}) => {
|
||||
await page.goto(conditionSet.url);
|
||||
|
||||
// Change the object to edit mode
|
||||
await page.getByLabel('Edit Object').click();
|
||||
|
||||
// Click Add Condition button
|
||||
await page.locator('#addCondition').click();
|
||||
// Check that the new Unnamed Condition section appears
|
||||
const numOfUnnamedConditions = await page
|
||||
.locator('.c-condition__name', { hasText: 'Unnamed Condition' })
|
||||
.count();
|
||||
expect(numOfUnnamedConditions).toEqual(1);
|
||||
});
|
||||
test('ConditionSet should display appropriate view options', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/5924'
|
||||
});
|
||||
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
name: 'Alpha Sine Wave Generator'
|
||||
});
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
name: 'Beta Sine Wave Generator'
|
||||
});
|
||||
|
||||
await page.goto(conditionSet.url);
|
||||
|
||||
// Change the object to edit mode
|
||||
await page.getByLabel('Edit Object').click();
|
||||
|
||||
// Expand the 'My Items' folder in the left tree
|
||||
await page.getByLabel('Show selected item in tree').click();
|
||||
// Add the Alpha & Beta Sine Wave Generator to the Condition Set and save changes
|
||||
const treePane = page.getByRole('tree', {
|
||||
name: 'Main Tree'
|
||||
});
|
||||
const alphaGeneratorTreeItem = treePane.getByRole('treeitem', {
|
||||
name: 'Alpha Sine Wave Generator'
|
||||
});
|
||||
const betaGeneratorTreeItem = treePane.getByRole('treeitem', {
|
||||
name: 'Beta Sine Wave Generator'
|
||||
});
|
||||
const conditionCollection = page.locator('#conditionCollection');
|
||||
|
||||
await alphaGeneratorTreeItem.dragTo(conditionCollection);
|
||||
await betaGeneratorTreeItem.dragTo(conditionCollection);
|
||||
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
await page.getByLabel('Open the View Switcher Menu').click();
|
||||
|
||||
await expect(page.getByRole('menuitem', { name: /Lad Table/ })).toBeHidden();
|
||||
await expect(page.getByRole('menuitem', { name: /Conditions View/ })).toBeVisible();
|
||||
await expect(page.getByRole('menuitem', { name: /Plot/ })).toBeVisible();
|
||||
await expect(page.getByRole('menuitem', { name: /Telemetry Table/ })).toBeVisible();
|
||||
await page.getByLabel('Plot').click();
|
||||
await expect(
|
||||
page.getByLabel('Plot Legend Collapsed').getByText('Test Condition Set')
|
||||
).toBeVisible();
|
||||
await page.getByLabel('Open the View Switcher Menu').click();
|
||||
await page.getByLabel('Telemetry Table').click();
|
||||
await expect(page.getByRole('searchbox', { name: 'output filter input' })).toBeVisible();
|
||||
await page.getByLabel('Open the View Switcher Menu').click();
|
||||
await page.getByLabel('Conditions View').click();
|
||||
await expect(page.getByText('Current Output')).toBeVisible();
|
||||
});
|
||||
test('ConditionSet produces an output when telemetry is available, and does not when it is not', async ({
|
||||
page
|
||||
}) => {
|
||||
const exampleTelemetry = await createExampleTelemetryObject(page);
|
||||
|
||||
await page.getByLabel('Show selected item in tree').click();
|
||||
await page.goto(conditionSet.url);
|
||||
// Change the object to edit mode
|
||||
await page.getByLabel('Edit Object').click();
|
||||
|
||||
// Create two conditions
|
||||
await page.locator('#addCondition').click();
|
||||
await page.locator('#addCondition').click();
|
||||
await page.locator('#conditionCollection').getByRole('textbox').nth(0).fill('First Condition');
|
||||
await page.locator('#conditionCollection').getByRole('textbox').nth(1).fill('Second Condition');
|
||||
|
||||
// Add Telemetry to ConditionSet
|
||||
const sineWaveGeneratorTreeItem = page
|
||||
.getByRole('tree', {
|
||||
name: 'Main Tree'
|
||||
})
|
||||
.getByRole('treeitem', {
|
||||
name: exampleTelemetry.name
|
||||
});
|
||||
const conditionCollection = page.locator('#conditionCollection');
|
||||
await sineWaveGeneratorTreeItem.dragTo(conditionCollection);
|
||||
|
||||
// Modify First Criterion
|
||||
const firstCriterionTelemetry = page.locator(
|
||||
'[aria-label="Criterion Telemetry Selection"] >> nth=0'
|
||||
);
|
||||
firstCriterionTelemetry.selectOption({ label: exampleTelemetry.name });
|
||||
const firstCriterionMetadata = page.locator(
|
||||
'[aria-label="Criterion Metadata Selection"] >> nth=0'
|
||||
);
|
||||
firstCriterionMetadata.selectOption({ label: 'Sine' });
|
||||
const firstCriterionComparison = page.locator(
|
||||
'[aria-label="Criterion Comparison Selection"] >> nth=0'
|
||||
);
|
||||
firstCriterionComparison.selectOption({ label: 'is greater than or equal to' });
|
||||
const firstCriterionInput = page.locator('[aria-label="Criterion Input"] >> nth=0');
|
||||
await firstCriterionInput.fill('0');
|
||||
|
||||
// Modify First Criterion
|
||||
const secondCriterionTelemetry = page.locator(
|
||||
'[aria-label="Criterion Telemetry Selection"] >> nth=1'
|
||||
);
|
||||
secondCriterionTelemetry.selectOption({ label: exampleTelemetry.name });
|
||||
|
||||
const secondCriterionMetadata = page.locator(
|
||||
'[aria-label="Criterion Metadata Selection"] >> nth=1'
|
||||
);
|
||||
secondCriterionMetadata.selectOption({ label: 'Sine' });
|
||||
|
||||
const secondCriterionComparison = page.locator(
|
||||
'[aria-label="Criterion Comparison Selection"] >> nth=1'
|
||||
);
|
||||
secondCriterionComparison.selectOption({ label: 'is less than' });
|
||||
|
||||
const secondCriterionInput = page.locator('[aria-label="Criterion Input"] >> nth=1');
|
||||
await secondCriterionInput.fill('0');
|
||||
|
||||
// Save ConditionSet
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
// Validate that the condition set is evaluating and outputting
|
||||
// the correct value when the underlying telemetry subscription is active.
|
||||
let outputValue = page.getByLabel('Current Output Value');
|
||||
await expect(outputValue).toHaveText('false');
|
||||
|
||||
await page.goto(exampleTelemetry.url);
|
||||
|
||||
// Edit SWG to add 8 second loading delay to simulate the case
|
||||
// where telemetry is not available.
|
||||
await page.getByTitle('More actions').click();
|
||||
await page.getByRole('menuitem', { name: 'Edit Properties...' }).click();
|
||||
await page.getByRole('spinbutton', { name: 'Loading Delay (ms)' }).fill('8000');
|
||||
await page.getByLabel('Save').click();
|
||||
|
||||
// Expect that the output value is blank or '---' if the
|
||||
// underlying telemetry subscription is not active.
|
||||
await page.goto(conditionSet.url);
|
||||
await expect(outputValue).toHaveText('---');
|
||||
});
|
||||
|
||||
test('ConditionSet has correct outputs when test data is enabled', async ({ page }) => {
|
||||
const exampleTelemetry = await createExampleTelemetryObject(page);
|
||||
|
||||
await page.getByLabel('Show selected item in tree').click();
|
||||
await page.goto(conditionSet.url);
|
||||
// Change the object to edit mode
|
||||
await page.getByLabel('Edit Object').click();
|
||||
|
||||
// Create two conditions
|
||||
await page.locator('#addCondition').click();
|
||||
await page.locator('#addCondition').click();
|
||||
await page.locator('#conditionCollection').getByRole('textbox').nth(0).fill('First Condition');
|
||||
await page.locator('#conditionCollection').getByRole('textbox').nth(1).fill('Second Condition');
|
||||
|
||||
// Add Telemetry to ConditionSet
|
||||
const sineWaveGeneratorTreeItem = page
|
||||
.getByRole('tree', {
|
||||
name: 'Main Tree'
|
||||
})
|
||||
.getByRole('treeitem', {
|
||||
name: exampleTelemetry.name
|
||||
});
|
||||
const conditionCollection = page.locator('#conditionCollection');
|
||||
await sineWaveGeneratorTreeItem.dragTo(conditionCollection);
|
||||
|
||||
// Modify First Criterion
|
||||
const firstCriterionTelemetry = page.locator(
|
||||
'[aria-label="Criterion Telemetry Selection"] >> nth=0'
|
||||
);
|
||||
firstCriterionTelemetry.selectOption({ label: exampleTelemetry.name });
|
||||
const firstCriterionMetadata = page.locator(
|
||||
'[aria-label="Criterion Metadata Selection"] >> nth=0'
|
||||
);
|
||||
firstCriterionMetadata.selectOption({ label: 'Sine' });
|
||||
const firstCriterionComparison = page.locator(
|
||||
'[aria-label="Criterion Comparison Selection"] >> nth=0'
|
||||
);
|
||||
firstCriterionComparison.selectOption({ label: 'is greater than or equal to' });
|
||||
const firstCriterionInput = page.locator('[aria-label="Criterion Input"] >> nth=0');
|
||||
await firstCriterionInput.fill('0');
|
||||
|
||||
// Modify Second Criterion
|
||||
const secondCriterionTelemetry = page.locator(
|
||||
'[aria-label="Criterion Telemetry Selection"] >> nth=1'
|
||||
);
|
||||
await secondCriterionTelemetry.selectOption({ label: exampleTelemetry.name });
|
||||
|
||||
const secondCriterionMetadata = page.locator(
|
||||
'[aria-label="Criterion Metadata Selection"] >> nth=1'
|
||||
);
|
||||
await secondCriterionMetadata.selectOption({ label: 'Sine' });
|
||||
|
||||
const secondCriterionComparison = page.locator(
|
||||
'[aria-label="Criterion Comparison Selection"] >> nth=1'
|
||||
);
|
||||
await secondCriterionComparison.selectOption({ label: 'is less than' });
|
||||
|
||||
const secondCriterionInput = page.locator('[aria-label="Criterion Input"] >> nth=1');
|
||||
await secondCriterionInput.fill('0');
|
||||
|
||||
// Enable test data
|
||||
await page.getByLabel('Apply Test Data').nth(1).click();
|
||||
const testDataTelemetry = page.locator('[aria-label="Test Data Telemetry Selection"] >> nth=0');
|
||||
await testDataTelemetry.selectOption({ label: exampleTelemetry.name });
|
||||
|
||||
const testDataMetadata = page.locator('[aria-label="Test Data Metadata Selection"] >> nth=0');
|
||||
await testDataMetadata.selectOption({ label: 'Sine' });
|
||||
|
||||
const testInput = page.locator('[aria-label="Test Data Input"] >> nth=0');
|
||||
await testInput.fill('0');
|
||||
|
||||
// Validate that the condition set is evaluating and outputting
|
||||
// the correct value when the underlying telemetry subscription is active.
|
||||
let outputValue = page.getByLabel('Current Output Value');
|
||||
await expect(outputValue).toHaveText('false');
|
||||
|
||||
await page.goto(exampleTelemetry.url);
|
||||
});
|
||||
|
||||
test('Short circuit evaluation does not cause incorrect evaluation https://github.com/nasa/openmct/issues/7992', async ({
|
||||
page
|
||||
}) => {
|
||||
await setRealTimeMode(page);
|
||||
await page.getByLabel('Create', { exact: true }).click();
|
||||
await page.getByLabel('State Generator').click();
|
||||
await page.getByLabel('Title', { exact: true }).fill('P1');
|
||||
await page.getByLabel('State Duration (seconds)').fill('1');
|
||||
await page.getByLabel('Save').click();
|
||||
await page.getByLabel('Create', { exact: true }).click();
|
||||
await page.getByLabel('State Generator').click();
|
||||
await page.getByLabel('Title', { exact: true }).fill('P2');
|
||||
await page.getByLabel('State Duration (seconds)', { exact: true }).fill('1');
|
||||
await page.getByRole('treeitem', { name: 'Test Condition Set' }).click();
|
||||
await page.getByLabel('Save').click();
|
||||
await page.getByLabel('Expand My Items folder').click();
|
||||
await page.getByRole('treeitem', { name: 'Test Condition Set' }).click();
|
||||
await page.getByLabel('Edit Object').click();
|
||||
await page.getByLabel('Add Condition').click();
|
||||
await page.getByLabel('Condition Name Input').first().fill('P1 IS ON AND P2 IS ON');
|
||||
await page.getByLabel('Criterion Telemetry Selection').selectOption({ label: 'P1' });
|
||||
await page.getByLabel('Criterion Metadata Selection').selectOption('value');
|
||||
await page.getByLabel('Criterion Comparison Selection').selectOption('equalTo');
|
||||
await page.getByLabel('Criterion Input').fill('1');
|
||||
await page.getByLabel('Add Criteria - Enabled').click();
|
||||
await page.getByLabel('Criterion Telemetry Selection').nth(1).selectOption({ label: 'P2' });
|
||||
await page.getByLabel('Criterion Metadata Selection').nth(1).selectOption('value');
|
||||
await page.getByLabel('Criterion Comparison Selection').nth(1).selectOption('equalTo');
|
||||
await page.getByLabel('Criterion Input').nth(1).fill('1');
|
||||
await page.getByLabel('Add Condition').click();
|
||||
await page.getByLabel('Condition Name Input').first().fill('P1 IS OFF OR P2 IS OFF');
|
||||
await page.getByLabel('Condition Trigger').first().selectOption('any');
|
||||
await page.getByLabel('Criterion Telemetry Selection').first().selectOption({ label: 'P1' });
|
||||
await page.getByLabel('Criterion Metadata Selection').first().selectOption('value');
|
||||
await page.getByLabel('Criterion Comparison Selection').first().selectOption('equalTo');
|
||||
await page.getByLabel('Criterion Input').first().fill('0');
|
||||
await page.getByLabel('Add Criteria - Enabled').first().click();
|
||||
await page.getByLabel('Criterion Telemetry Selection').nth(1).selectOption({ label: 'P2' });
|
||||
await page.getByLabel('Criterion Metadata Selection').nth(1).selectOption('value');
|
||||
await page.getByLabel('Criterion Comparison Selection').nth(1).selectOption('equalTo');
|
||||
await page.getByLabel('Criterion Input').nth(1).fill('0');
|
||||
await page.getByLabel('Condition Name Input').first().dblclick();
|
||||
await page.getByLabel('Save').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
await page.getByLabel('Edit Object').click();
|
||||
|
||||
/**
|
||||
* Create default conditions for test. Start with invalid values to put condition set into
|
||||
* "default" state
|
||||
*/
|
||||
await page.getByLabel('Test Data Telemetry Selection').selectOption({ label: 'P1' });
|
||||
await page.getByLabel('Test Data Metadata Selection').selectOption({ label: 'Value' });
|
||||
await page.getByLabel('Test Data Input').fill('3');
|
||||
await page.getByLabel('Add Test Datum').click();
|
||||
await page.getByLabel('Test Data Telemetry Selection').nth(1).selectOption({ label: 'P2' });
|
||||
await page.getByLabel('Test Data Metadata Selection').nth(1).selectOption({ label: 'Value' });
|
||||
await page.getByLabel('Test Data Input').nth(1).fill('3');
|
||||
await page.getByLabel('Apply Test Data').nth(1).click();
|
||||
|
||||
let activeCondition = page.getByLabel('Active Condition Set Condition');
|
||||
let activeConditionName = activeCondition.getByLabel('Condition Name Label');
|
||||
|
||||
await expect(activeConditionName).toHaveText('Default');
|
||||
|
||||
/**
|
||||
* Set P1 to 0
|
||||
*/
|
||||
await page.getByLabel('Test Data Input').nth(0).fill('0');
|
||||
|
||||
activeCondition = page.getByLabel('Active Condition Set Condition');
|
||||
activeConditionName = activeCondition.getByLabel('Condition Name Label');
|
||||
|
||||
await expect(activeConditionName).toHaveText('P1 IS OFF OR P2 IS OFF');
|
||||
|
||||
/**
|
||||
* Set P2 to 1
|
||||
*/
|
||||
await page.getByLabel('Test Data Input').nth(1).fill('1');
|
||||
|
||||
activeCondition = page.getByLabel('Active Condition Set Condition');
|
||||
activeConditionName = activeCondition.getByLabel('Condition Name Label');
|
||||
|
||||
await expect(activeConditionName).toHaveText('P1 IS OFF OR P2 IS OFF');
|
||||
|
||||
/**
|
||||
* Set P1 to 1
|
||||
*/
|
||||
await page.getByLabel('Test Data Input').nth(0).fill('1');
|
||||
|
||||
activeCondition = page.getByLabel('Active Condition Set Condition');
|
||||
activeConditionName = activeCondition.getByLabel('Condition Name Label');
|
||||
|
||||
await expect(activeConditionName).toHaveText('P1 IS ON AND P2 IS ON');
|
||||
});
|
||||
|
||||
test.fixme('Ensure condition sets work with telemetry like operator status', ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/7484'
|
||||
});
|
||||
});
|
||||
|
||||
test('ConditionSet has add criteria button enabled/disabled when composition is and is not available', async ({
|
||||
page
|
||||
}) => {
|
||||
const exampleTelemetry = await createExampleTelemetryObject(page);
|
||||
|
||||
await page.getByLabel('Show selected item in tree').click();
|
||||
await page.goto(conditionSet.url);
|
||||
// Change the object to edit mode
|
||||
await page.getByLabel('Edit Object').click();
|
||||
|
||||
// Create a condition
|
||||
await page.locator('#addCondition').click();
|
||||
await page.locator('#conditionCollection').getByRole('textbox').nth(0).fill('First Condition');
|
||||
|
||||
// Validate that the add criteria button is disabled
|
||||
await expect(page.getByLabel('Add Criteria - Disabled')).toHaveAttribute('disabled');
|
||||
|
||||
// Add Telemetry to ConditionSet
|
||||
const sineWaveGeneratorTreeItem = page
|
||||
.getByRole('tree', {
|
||||
name: 'Main Tree'
|
||||
})
|
||||
.getByRole('treeitem', {
|
||||
name: exampleTelemetry.name
|
||||
});
|
||||
const conditionCollection = page.locator('#conditionCollection');
|
||||
await sineWaveGeneratorTreeItem.dragTo(conditionCollection);
|
||||
|
||||
// Validate that the add criteria button is enabled and adds a new criterion
|
||||
await expect(page.getByLabel('Add Criteria - Enabled')).not.toHaveAttribute('disabled');
|
||||
await page.getByLabel('Add Criteria - Enabled').click();
|
||||
const numOfUnnamedCriteria = await page.getByLabel('Criterion Telemetry Selection').count();
|
||||
expect(numOfUnnamedCriteria).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Condition Set Composition', () => {
|
||||
let conditionSet;
|
||||
let exampleTelemetry;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Create Condition Set
|
||||
conditionSet = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Condition Set'
|
||||
});
|
||||
|
||||
// Create Telemetry Object as child to Condition Set
|
||||
exampleTelemetry = await createExampleTelemetryObject(page, conditionSet.uuid);
|
||||
|
||||
// Edit Condition Set
|
||||
await page.goto(conditionSet.url);
|
||||
await page.getByRole('button', { name: 'Edit Object' }).click();
|
||||
|
||||
// Add Condition to Condition Set
|
||||
await page.getByRole('button', { name: 'Add Condition' }).click();
|
||||
|
||||
// Enter Condition Output
|
||||
await page.getByLabel('Condition Name Input').first().fill('Negative');
|
||||
await page.getByLabel('Condition Output Type').first().selectOption({ value: 'string' });
|
||||
await page.getByLabel('Condition Output String').first().fill('Negative');
|
||||
|
||||
// Condition Trigger default is okay so no change needed to form
|
||||
|
||||
// Enter Condition Criterion
|
||||
await page.getByLabel('Criterion Telemetry Selection').first().selectOption({ value: 'all' });
|
||||
await page.getByLabel('Criterion Metadata Selection').first().selectOption({ value: 'sin' });
|
||||
await page
|
||||
.locator('select[aria-label="Criterion Comparison Selection"]')
|
||||
.first()
|
||||
.selectOption({ value: 'lessThan' });
|
||||
await page.getByLabel('Criterion Input').first().fill('0');
|
||||
|
||||
// Save the Condition Set
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
});
|
||||
|
||||
test('You can remove telemetry from a condition set with existing conditions', async ({
|
||||
page
|
||||
}) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/7710'
|
||||
});
|
||||
|
||||
await page.getByLabel('Expand My Items folder').click();
|
||||
await page.getByLabel(`Expand ${conditionSet.name} conditionSet`).click();
|
||||
|
||||
await page
|
||||
.getByLabel(`Navigate to ${exampleTelemetry.name}`, { exact: false })
|
||||
.click({ button: 'right' });
|
||||
|
||||
await page
|
||||
.getByLabel(`${exampleTelemetry.name} Context Menu`)
|
||||
.getByRole('menuitem', { name: 'Remove' })
|
||||
.click();
|
||||
await page.getByRole('button', { name: 'Ok', exact: true }).click();
|
||||
|
||||
await page
|
||||
.getByLabel(`Navigate to ${conditionSet.name} conditionSet Object`, { exact: true })
|
||||
.click();
|
||||
await page.getByRole('button', { name: 'Edit Object' }).click();
|
||||
await page.getByRole('tab', { name: 'Elements' }).click();
|
||||
expect(
|
||||
await page
|
||||
.getByRole('tabpanel', { name: 'Inspector Views' })
|
||||
.getByRole('listitem', { name: exampleTelemetry.name })
|
||||
.count()
|
||||
).toEqual(0);
|
||||
});
|
||||
});
|
@ -24,8 +24,8 @@ import { fileURLToPath } from 'url';
|
||||
import {
|
||||
createDomainObjectWithDefaults,
|
||||
navigateToObjectWithFixedTimeBounds,
|
||||
setFixedIndependentTimeConductorBounds,
|
||||
setFixedTimeMode,
|
||||
setIndependentTimeConductorBounds,
|
||||
setRealTimeMode,
|
||||
setStartOffset
|
||||
} from '../../../../appActions.js';
|
||||
@ -75,18 +75,12 @@ test.describe('Display Layout Sub-object Actions @localStorage', () => {
|
||||
const TEST_FIXED_END_TIME = TEST_FIXED_START_TIME + 3600000; // 2024-11-11 20:11:11.000Z
|
||||
|
||||
// Verify the ITC has the expected initial bounds
|
||||
expect(
|
||||
await page
|
||||
.getByLabel('Child Overlay Plot 1 Frame Controls')
|
||||
.getByLabel('Start bounds')
|
||||
.textContent()
|
||||
).toEqual(INIT_ITC_START_BOUNDS);
|
||||
expect(
|
||||
await page
|
||||
.getByLabel('Child Overlay Plot 1 Frame Controls')
|
||||
.getByLabel('End bounds')
|
||||
.textContent()
|
||||
).toEqual(INIT_ITC_END_BOUNDS);
|
||||
await expect(
|
||||
page.getByLabel('Child Overlay Plot 1 Frame Controls').getByLabel('Start bounds')
|
||||
).toHaveText(INIT_ITC_START_BOUNDS);
|
||||
await expect(
|
||||
page.getByLabel('Child Overlay Plot 1 Frame Controls').getByLabel('End bounds')
|
||||
).toHaveText(INIT_ITC_END_BOUNDS);
|
||||
|
||||
// Update the global fixed bounds to 2024-11-11 19:11:11.000Z / 2024-11-11 20:11:11.000Z
|
||||
const url = page.url().split('?')[0];
|
||||
@ -98,18 +92,12 @@ test.describe('Display Layout Sub-object Actions @localStorage', () => {
|
||||
);
|
||||
|
||||
// ITC bounds should still match the initial ITC bounds
|
||||
expect(
|
||||
await page
|
||||
.getByLabel('Child Overlay Plot 1 Frame Controls')
|
||||
.getByLabel('Start bounds')
|
||||
.textContent()
|
||||
).toEqual(INIT_ITC_START_BOUNDS);
|
||||
expect(
|
||||
await page
|
||||
.getByLabel('Child Overlay Plot 1 Frame Controls')
|
||||
.getByLabel('End bounds')
|
||||
.textContent()
|
||||
).toEqual(INIT_ITC_END_BOUNDS);
|
||||
await expect(
|
||||
page.getByLabel('Child Overlay Plot 1 Frame Controls').getByLabel('Start bounds')
|
||||
).toHaveText(INIT_ITC_START_BOUNDS);
|
||||
await expect(
|
||||
page.getByLabel('Child Overlay Plot 1 Frame Controls').getByLabel('End bounds')
|
||||
).toHaveText(INIT_ITC_END_BOUNDS);
|
||||
|
||||
// Open the Child Overlay Plot 1 in a new tab
|
||||
await page.getByLabel('View menu items').click();
|
||||
@ -120,28 +108,22 @@ test.describe('Display Layout Sub-object Actions @localStorage', () => {
|
||||
await newPage.waitForLoadState('domcontentloaded');
|
||||
|
||||
// Verify that the global time conductor bounds in the new page match the updated global bounds
|
||||
expect(
|
||||
await newPage.getByLabel('Global Time Conductor').getByLabel('Start bounds').textContent()
|
||||
).toEqual(NEW_GLOBAL_START_BOUNDS);
|
||||
expect(
|
||||
await newPage.getByLabel('Global Time Conductor').getByLabel('End bounds').textContent()
|
||||
).toEqual(NEW_GLOBAL_END_BOUNDS);
|
||||
await expect(newPage.getByLabel('Global Time Conductor').getByLabel('Start bounds')).toHaveText(
|
||||
NEW_GLOBAL_START_BOUNDS
|
||||
);
|
||||
await expect(newPage.getByLabel('Global Time Conductor').getByLabel('End bounds')).toHaveText(
|
||||
NEW_GLOBAL_END_BOUNDS
|
||||
);
|
||||
|
||||
// Verify that the ITC is enabled in the new page
|
||||
await expect(newPage.getByLabel('Disable Independent Time Conductor')).toBeVisible();
|
||||
// Verify that the ITC bounds in the new page match the original ITC bounds
|
||||
expect(
|
||||
await newPage
|
||||
.getByLabel('Independent Time Conductor Panel')
|
||||
.getByLabel('Start bounds')
|
||||
.textContent()
|
||||
).toEqual(INIT_ITC_START_BOUNDS);
|
||||
expect(
|
||||
await newPage
|
||||
.getByLabel('Independent Time Conductor Panel')
|
||||
.getByLabel('End bounds')
|
||||
.textContent()
|
||||
).toEqual(INIT_ITC_END_BOUNDS);
|
||||
await expect(
|
||||
newPage.getByLabel('Independent Time Conductor Panel').getByLabel('Start bounds')
|
||||
).toHaveText(INIT_ITC_START_BOUNDS);
|
||||
await expect(
|
||||
newPage.getByLabel('Independent Time Conductor Panel').getByLabel('End bounds')
|
||||
).toHaveText(INIT_ITC_END_BOUNDS);
|
||||
});
|
||||
});
|
||||
|
||||
@ -174,17 +156,17 @@ test.describe('Display Layout Toolbar Actions @localStorage', () => {
|
||||
test('can add/remove Image to a single layout', async ({ page }) => {
|
||||
const layoutObject = 'Image';
|
||||
await test.step("Add and remove image element from the parent's layout", async () => {
|
||||
expect(await page.getByLabel(`Move ${layoutObject} Frame`).count()).toBe(0);
|
||||
await expect(page.getByLabel(`Move ${layoutObject} Frame`)).toHaveCount(0);
|
||||
await addLayoutObject(page, PARENT_DISPLAY_LAYOUT_NAME, layoutObject);
|
||||
expect(await page.getByLabel(`Move ${layoutObject} Frame`).count()).toBe(1);
|
||||
await expect(page.getByLabel(`Move ${layoutObject} Frame`)).toHaveCount(1);
|
||||
await removeLayoutObject(page, layoutObject);
|
||||
expect(await page.getByLabel(`Move ${layoutObject} Frame`).count()).toBe(0);
|
||||
await expect(page.getByLabel(`Move ${layoutObject} Frame`)).toHaveCount(0);
|
||||
});
|
||||
await test.step("Add and remove image from the child's layout", async () => {
|
||||
await addLayoutObject(page, CHILD_DISPLAY_LAYOUT_NAME1, layoutObject);
|
||||
expect(await page.getByLabel(`Move ${layoutObject} Frame`).count()).toBe(1);
|
||||
await expect(page.getByLabel(`Move ${layoutObject} Frame`)).toHaveCount(1);
|
||||
await removeLayoutObject(page, layoutObject);
|
||||
expect(await page.getByLabel(`Move ${layoutObject} Frame`).count()).toBe(0);
|
||||
await expect(page.getByLabel(`Move ${layoutObject} Frame`)).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
test(`can add/remove Box to a single layout`, async ({ page }) => {
|
||||
@ -253,20 +235,17 @@ test.describe('Display Layout', () => {
|
||||
const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
|
||||
name: new RegExp(sineWaveObject.name)
|
||||
});
|
||||
const layoutGridHolder = page.locator('.l-layout__grid-holder');
|
||||
await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder);
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await sineWaveGeneratorTreeItem.dragTo(page.getByLabel('Layout Grid'));
|
||||
await page.getByLabel('Save', { exact: true }).click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
// Subscribe to the Sine Wave Generator data
|
||||
// On getting data, check if the value found in the Display Layout is the most recent value
|
||||
// from the Sine Wave Generator
|
||||
const getTelemValuePromise = await subscribeToTelemetry(page, sineWaveObject.uuid);
|
||||
const formattedTelemetryValue = getTelemValuePromise;
|
||||
const displayLayoutValuePromise = await page.waitForSelector(
|
||||
`text="${formattedTelemetryValue}"`
|
||||
);
|
||||
const displayLayoutValue = await displayLayoutValuePromise.textContent();
|
||||
const getTelemValuePromise = subscribeToTelemetry(page, sineWaveObject.uuid);
|
||||
const formattedTelemetryValue = await getTelemValuePromise;
|
||||
await expect(page.getByText(formattedTelemetryValue)).toBeVisible();
|
||||
const displayLayoutValue = await page.getByText(formattedTelemetryValue).textContent();
|
||||
const trimmedDisplayValue = displayLayoutValue.trim();
|
||||
|
||||
expect(trimmedDisplayValue).toBe(formattedTelemetryValue);
|
||||
@ -298,24 +277,21 @@ test.describe('Display Layout', () => {
|
||||
const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
|
||||
name: new RegExp(sineWaveObject.name)
|
||||
});
|
||||
const layoutGridHolder = page.locator('.l-layout__grid-holder');
|
||||
await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder);
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await sineWaveGeneratorTreeItem.dragTo(page.getByLabel('Layout Grid'));
|
||||
await page.getByLabel('Save', { exact: true }).click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
// Subscribe to the Sine Wave Generator data
|
||||
const getTelemValuePromise = await subscribeToTelemetry(page, sineWaveObject.uuid);
|
||||
const getTelemValuePromise = subscribeToTelemetry(page, sineWaveObject.uuid);
|
||||
// Set an offset of 1 minute and then change the time mode to fixed to set a 1 minute historical window
|
||||
await setStartOffset(page, { mins: '1' });
|
||||
await setStartOffset(page, { startMins: '1' });
|
||||
await setFixedTimeMode(page);
|
||||
|
||||
// On getting data, check if the value found in the Display Layout is the most recent value
|
||||
// from the Sine Wave Generator
|
||||
const formattedTelemetryValue = getTelemValuePromise;
|
||||
const displayLayoutValuePromise = await page.waitForSelector(
|
||||
`text="${formattedTelemetryValue}"`
|
||||
);
|
||||
const displayLayoutValue = await displayLayoutValuePromise.textContent();
|
||||
const formattedTelemetryValue = await getTelemValuePromise;
|
||||
await expect(page.getByText(formattedTelemetryValue)).toBeVisible();
|
||||
const displayLayoutValue = await page.getByText(formattedTelemetryValue).textContent();
|
||||
const trimmedDisplayValue = displayLayoutValue.trim();
|
||||
|
||||
expect(trimmedDisplayValue).toBe(formattedTelemetryValue);
|
||||
@ -340,9 +316,8 @@ test.describe('Display Layout', () => {
|
||||
const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
|
||||
name: new RegExp(sineWaveObject.name)
|
||||
});
|
||||
const layoutGridHolder = page.locator('.l-layout__grid-holder');
|
||||
await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder);
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await sineWaveGeneratorTreeItem.dragTo(page.getByLabel('Layout Grid'));
|
||||
await page.getByLabel('Save', { exact: true }).click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
expect.soft(await page.locator('.l-layout .l-layout__frame').count()).toEqual(1);
|
||||
@ -382,9 +357,8 @@ test.describe('Display Layout', () => {
|
||||
const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
|
||||
name: new RegExp(sineWaveObject.name)
|
||||
});
|
||||
const layoutGridHolder = page.locator('.l-layout__grid-holder');
|
||||
await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder);
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await sineWaveGeneratorTreeItem.dragTo(page.getByLabel('Layout Grid'));
|
||||
await page.getByLabel('Save', { exact: true }).click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
expect.soft(await page.locator('.l-layout .l-layout__frame').count()).toEqual(1);
|
||||
@ -428,8 +402,7 @@ test.describe('Display Layout', () => {
|
||||
const exampleImageryTreeItem = treePane.getByRole('treeitem', {
|
||||
name: new RegExp(exampleImageryObject.name)
|
||||
});
|
||||
let layoutGridHolder = page.locator('.l-layout__grid-holder');
|
||||
await exampleImageryTreeItem.dragTo(layoutGridHolder);
|
||||
await exampleImageryTreeItem.dragTo(page.getByLabel('Layout Grid'));
|
||||
|
||||
//adjust so that we can see the independent time conductor toggle
|
||||
// Adjust object height
|
||||
@ -440,12 +413,12 @@ test.describe('Display Layout', () => {
|
||||
await page.locator('div[title="Resize object width"] > input').click();
|
||||
await page.locator('div[title="Resize object width"] > input').fill('70');
|
||||
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.getByLabel('Save', { exact: true }).click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
const startDate = '2021-12-30 01:01:00.000Z';
|
||||
const endDate = '2021-12-30 01:11:00.000Z';
|
||||
await setIndependentTimeConductorBounds(page, { start: startDate, end: endDate });
|
||||
await setFixedIndependentTimeConductorBounds(page, { start: startDate, end: endDate });
|
||||
|
||||
// check image date
|
||||
await expect(page.getByText('2021-12-30 01:11:00.000Z').first()).toBeVisible();
|
||||
@ -456,7 +429,7 @@ test.describe('Display Layout', () => {
|
||||
await expect(page.getByText('2021-12-30 01:11:00.000Z')).toBeHidden();
|
||||
});
|
||||
|
||||
test('When multiple plots are contained in a layout, we only ask for annotations once @couchdb', async ({
|
||||
test('When multiple plots are contained in a layout, we only ask for annotations once @couchdb @network', async ({
|
||||
page
|
||||
}) => {
|
||||
await setFixedTimeMode(page);
|
||||
@ -485,9 +458,8 @@ test.describe('Display Layout', () => {
|
||||
name: new RegExp(sineWaveObject.name)
|
||||
});
|
||||
|
||||
let layoutGridHolder = page.locator('.l-layout__grid-holder');
|
||||
// eslint-disable-next-line playwright/no-force-option
|
||||
await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder, { force: true });
|
||||
await sineWaveGeneratorTreeItem.dragTo(page.getByLabel('Layout Grid'), { force: true });
|
||||
|
||||
await page.getByText('View type').click();
|
||||
await page.getByText('Overlay Plot').click();
|
||||
@ -495,14 +467,13 @@ test.describe('Display Layout', () => {
|
||||
const anotherSineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
|
||||
name: new RegExp(anotherSineWaveObject.name)
|
||||
});
|
||||
layoutGridHolder = page.locator('.l-layout__grid-holder');
|
||||
// eslint-disable-next-line playwright/no-force-option
|
||||
await anotherSineWaveGeneratorTreeItem.dragTo(layoutGridHolder, { force: true });
|
||||
await anotherSineWaveGeneratorTreeItem.dragTo(page.getByLabel('Layout Grid'), { force: true });
|
||||
|
||||
await page.getByText('View type').click();
|
||||
await page.getByText('Overlay Plot').click();
|
||||
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.getByLabel('Save', { exact: true }).click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
// Time to inspect some network traffic
|
||||
@ -519,10 +490,10 @@ test.describe('Display Layout', () => {
|
||||
await page.reload();
|
||||
|
||||
// wait for annotations requests to be batched and requested
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
// Network requests for the composite telemetry with multiple items should be:
|
||||
// 1. a single batched request for annotations
|
||||
expect(networkRequests.length).toBe(1);
|
||||
await expect.poll(() => networkRequests, { timeout: 10000 }).toHaveLength(1);
|
||||
|
||||
await setRealTimeMode(page);
|
||||
|
||||
@ -531,15 +502,147 @@ test.describe('Display Layout', () => {
|
||||
await page.reload();
|
||||
|
||||
// wait for annotations to not load (if we have any, we've got a problem)
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
|
||||
// In real time mode, we don't fetch annotations at all
|
||||
expect(networkRequests.length).toBe(0);
|
||||
await expect.poll(() => networkRequests, { timeout: 10000 }).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('Same objects with different request options have unique subscriptions', async ({
|
||||
page
|
||||
}) => {
|
||||
// Expand My Items
|
||||
await page.getByLabel('Expand My Items folder').click();
|
||||
|
||||
// Create a Display Layout
|
||||
const displayLayout = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Display Layout',
|
||||
name: 'Test Display'
|
||||
});
|
||||
|
||||
// Create a State Generator, set to higher frequency updates
|
||||
const stateGenerator = await createDomainObjectWithDefaults(page, {
|
||||
type: 'State Generator',
|
||||
name: 'State Generator'
|
||||
});
|
||||
const stateGeneratorTreeItem = page.getByRole('treeitem', {
|
||||
name: stateGenerator.name
|
||||
});
|
||||
await stateGeneratorTreeItem.click({ button: 'right' });
|
||||
await page.getByLabel('Edit Properties...').click();
|
||||
await page.getByLabel('State Duration (seconds)', { exact: true }).fill('0.1');
|
||||
await page.getByLabel('Save', { exact: true }).click();
|
||||
|
||||
// Create a Table for filtering ON values
|
||||
const tableFilterOnValue = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Telemetry Table',
|
||||
name: 'Table Filter On Value'
|
||||
});
|
||||
const tableFilterOnTreeItem = page.getByRole('treeitem', {
|
||||
name: tableFilterOnValue.name
|
||||
});
|
||||
|
||||
// Create a Table for filtering OFF values
|
||||
const tableFilterOffValue = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Telemetry Table',
|
||||
name: 'Table Filter Off Value'
|
||||
});
|
||||
const tableFilterOffTreeItem = page.getByRole('treeitem', {
|
||||
name: tableFilterOffValue.name
|
||||
});
|
||||
|
||||
// Navigate to ON filtering table and add state generator and setup filters
|
||||
await page.goto(tableFilterOnValue.url);
|
||||
await stateGeneratorTreeItem.dragTo(page.getByLabel('Object View'));
|
||||
await selectFilterOption(page, '1');
|
||||
await page.getByLabel('Save', { exact: true }).click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
// Navigate to OFF filtering table and add state generator and setup filters
|
||||
await page.goto(tableFilterOffValue.url);
|
||||
await stateGeneratorTreeItem.dragTo(page.getByLabel('Object View'));
|
||||
await selectFilterOption(page, '0');
|
||||
await page.getByLabel('Save', { exact: true }).click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
// Navigate to the display layout and edit it
|
||||
await page.goto(displayLayout.url);
|
||||
|
||||
// Add the tables to the display layout
|
||||
await page.getByLabel('Edit Object').click();
|
||||
await tableFilterOffTreeItem.dragTo(page.getByLabel('Layout Grid'), {
|
||||
targetPosition: { x: 10, y: 300 }
|
||||
});
|
||||
await page.locator('.c-frame-edit > div:nth-child(4)').dragTo(page.getByLabel('Layout Grid'), {
|
||||
targetPosition: { x: 400, y: 500 },
|
||||
// eslint-disable-next-line playwright/no-force-option
|
||||
force: true
|
||||
});
|
||||
await tableFilterOnTreeItem.dragTo(page.getByLabel('Layout Grid'), {
|
||||
targetPosition: { x: 10, y: 100 }
|
||||
});
|
||||
await page.locator('.c-frame-edit > div:nth-child(4)').dragTo(page.getByLabel('Layout Grid'), {
|
||||
targetPosition: { x: 400, y: 300 },
|
||||
// eslint-disable-next-line playwright/no-force-option
|
||||
force: true
|
||||
});
|
||||
await page.getByLabel('Save', { exact: true }).click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
// Get the tables so we can verify filtering is working as expected
|
||||
const tableFilterOn = page.getByLabel(`${tableFilterOnValue.name} Frame`, {
|
||||
exact: true
|
||||
});
|
||||
const tableFilterOff = page.getByLabel(`${tableFilterOffValue.name} Frame`, {
|
||||
exact: true
|
||||
});
|
||||
|
||||
// Verify filtering is working correctly
|
||||
|
||||
// Check that no filtered values appear for at least 2 seconds
|
||||
const VERIFICATION_TIME = 2000; // 2 seconds
|
||||
const CHECK_INTERVAL = 100; // Check every 100ms
|
||||
|
||||
// Create a promise that will check for filtered values periodically
|
||||
const checkForCorrectValues = new Promise((resolve, reject) => {
|
||||
const interval = setInterval(async () => {
|
||||
const offCount = await tableFilterOn.locator('td[title="OFF"]').count();
|
||||
const onCount = await tableFilterOff.locator('td[title="ON"]').count();
|
||||
if (offCount > 0 || onCount > 0) {
|
||||
clearInterval(interval);
|
||||
reject(
|
||||
new Error(
|
||||
`Found ${offCount} OFF and ${onCount} ON values when expecting 0 OFF and 0 ON`
|
||||
)
|
||||
);
|
||||
}
|
||||
}, CHECK_INTERVAL);
|
||||
|
||||
// After VERIFICATION_TIME, if no filtered values were found, resolve successfully
|
||||
setTimeout(() => {
|
||||
clearInterval(interval);
|
||||
resolve();
|
||||
}, VERIFICATION_TIME);
|
||||
});
|
||||
|
||||
await expect(checkForCorrectValues).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
async function selectFilterOption(page, filterOption) {
|
||||
await page.getByRole('tab', { name: 'Filters' }).click();
|
||||
await page
|
||||
.getByLabel('Inspector Views')
|
||||
.locator('li')
|
||||
.filter({ hasText: 'State Generator' })
|
||||
.locator('span')
|
||||
.click();
|
||||
await page.getByRole('switch').click();
|
||||
await page.selectOption('select[name="setSelectionThreshold"]', filterOption);
|
||||
}
|
||||
|
||||
async function addAndRemoveDrawingObjectAndAssert(page, layoutObject, DISPLAY_LAYOUT_NAME) {
|
||||
expect(await page.getByLabel(layoutObject, { exact: true }).count()).toBe(0);
|
||||
await expect(page.getByLabel(layoutObject, { exact: true })).toHaveCount(0);
|
||||
await addLayoutObject(page, DISPLAY_LAYOUT_NAME, layoutObject);
|
||||
expect(
|
||||
await page
|
||||
@ -549,7 +652,7 @@ async function addAndRemoveDrawingObjectAndAssert(page, layoutObject, DISPLAY_LA
|
||||
.count()
|
||||
).toBe(1);
|
||||
await removeLayoutObject(page, layoutObject);
|
||||
expect(await page.getByLabel(layoutObject, { exact: true }).count()).toBe(0);
|
||||
await expect(page.getByLabel(layoutObject, { exact: true })).toHaveCount(0);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -565,7 +668,7 @@ async function removeLayoutObject(page, layoutObject) {
|
||||
// eslint-disable-next-line playwright/no-force-option
|
||||
.click({ force: true });
|
||||
await page.getByTitle('Delete the selected object').click();
|
||||
await page.getByRole('button', { name: 'OK', exact: true }).click();
|
||||
await page.getByRole('button', { name: 'Ok', exact: true }).click();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -584,10 +687,10 @@ async function addLayoutObject(page, layoutName, layoutObject) {
|
||||
.click();
|
||||
if (layoutObject === 'Text') {
|
||||
await page.getByRole('textbox', { name: 'Text' }).fill('Hello, Universe!');
|
||||
await page.getByText('OK').click();
|
||||
await page.getByText('Ok').click();
|
||||
} else if (layoutObject === 'Image') {
|
||||
await page.getByLabel('Image URL').fill(TINY_IMAGE_BASE64);
|
||||
await page.getByText('OK').click();
|
||||
await page.getByText('Ok').click();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -24,19 +24,13 @@ import {
|
||||
acknowledgeFault,
|
||||
acknowledgeMultipleFaults,
|
||||
changeViewTo,
|
||||
clearSearch,
|
||||
enterSearchTerm,
|
||||
getFault,
|
||||
getFaultByName,
|
||||
getFaultName,
|
||||
getFaultNamespace,
|
||||
getFaultResultCount,
|
||||
getFaultSeverity,
|
||||
getFaultTriggerTime,
|
||||
getHighestSeverity,
|
||||
getLowestSeverity,
|
||||
navigateToFaultManagementWithExample,
|
||||
navigateToFaultManagementWithoutExample,
|
||||
navigateToFaultManagementWithStaticExample,
|
||||
selectFaultItem,
|
||||
shelveFault,
|
||||
shelveMultipleFaults,
|
||||
@ -46,7 +40,7 @@ import { expect, test } from '../../../../pluginFixtures.js';
|
||||
|
||||
test.describe('The Fault Management Plugin using example faults', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await navigateToFaultManagementWithExample(page);
|
||||
await navigateToFaultManagementWithStaticExample(page);
|
||||
});
|
||||
|
||||
test('Shows a criticality icon for every fault', async ({ page }) => {
|
||||
@ -56,23 +50,19 @@ test.describe('The Fault Management Plugin using example faults', () => {
|
||||
expect(faultCount).toEqual(criticalityIconCount);
|
||||
});
|
||||
|
||||
test('When selecting a fault, it has an "is-selected" class and it\'s information shows in the inspector', async ({
|
||||
test('When selecting a fault, it has an "is-selected" class and its information shows in the inspector', async ({
|
||||
page
|
||||
}) => {
|
||||
await selectFaultItem(page, 1);
|
||||
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
const selectedFaultName = await page
|
||||
.locator('.c-fault-mgmt__list.is-selected .c-fault-mgmt__list-faultname')
|
||||
.textContent();
|
||||
const inspectorFaultNameCount = await page
|
||||
.locator(`.c-inspector__properties >> :text("${selectedFaultName}")`)
|
||||
.count();
|
||||
|
||||
await expect(
|
||||
page.locator('.c-faults-list-view-item-body > .c-fault-mgmt__list').first()
|
||||
).toHaveClass(/is-selected/);
|
||||
expect(inspectorFaultNameCount).toEqual(1);
|
||||
const inspectorFaultName = page
|
||||
.getByLabel('Source inspector properties')
|
||||
.getByLabel('inspector property value');
|
||||
|
||||
await expect(page.getByLabel('Fault triggered at').first()).toHaveClass(/is-selected/);
|
||||
await expect(inspectorFaultName).toHaveCount(1);
|
||||
});
|
||||
|
||||
test('When selecting multiple faults, no specific fault information is shown in the inspector', async ({
|
||||
@ -81,23 +71,18 @@ test.describe('The Fault Management Plugin using example faults', () => {
|
||||
await selectFaultItem(page, 1);
|
||||
await selectFaultItem(page, 2);
|
||||
|
||||
const selectedRows = page.locator(
|
||||
'.c-fault-mgmt__list.is-selected .c-fault-mgmt__list-faultname'
|
||||
);
|
||||
expect(await selectedRows.count()).toEqual(2);
|
||||
const selectedRows = page.getByRole('checkbox', { checked: true });
|
||||
await expect(selectedRows).toHaveCount(2);
|
||||
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
const firstSelectedFaultName = await selectedRows.nth(0).textContent();
|
||||
const secondSelectedFaultName = await selectedRows.nth(1).textContent();
|
||||
const firstNameInInspectorCount = await page
|
||||
.locator(`.c-inspector__properties >> :text("${firstSelectedFaultName}")`)
|
||||
.count();
|
||||
const secondNameInInspectorCount = await page
|
||||
.locator(`.c-inspector__properties >> :text("${secondSelectedFaultName}")`)
|
||||
.count();
|
||||
|
||||
expect(firstNameInInspectorCount).toEqual(0);
|
||||
expect(secondNameInInspectorCount).toEqual(0);
|
||||
await expect(
|
||||
page.locator(`.c-inspector__properties >> :text("${firstSelectedFaultName}")`)
|
||||
).toHaveCount(0);
|
||||
await expect(
|
||||
page.locator(`.c-inspector__properties >> :text("${secondSelectedFaultName}")`)
|
||||
).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('Allows you to shelve a fault', async ({ page }) => {
|
||||
@ -110,13 +95,13 @@ test.describe('The Fault Management Plugin using example faults', () => {
|
||||
|
||||
// check it is removed from standard view
|
||||
const afterShelvedFault = getFaultByName(page, shelvedFaultName);
|
||||
expect(await afterShelvedFault.count()).toBe(0);
|
||||
await expect(afterShelvedFault).toHaveCount(0);
|
||||
|
||||
await changeViewTo(page, 'shelved');
|
||||
|
||||
const shelvedViewFault = getFaultByName(page, shelvedFaultName);
|
||||
|
||||
expect(await shelvedViewFault.count()).toBe(1);
|
||||
await expect(shelvedViewFault).toHaveCount(1);
|
||||
});
|
||||
|
||||
test('Allows you to acknowledge a fault', async ({ page }) => {
|
||||
@ -188,44 +173,60 @@ test.describe('The Fault Management Plugin using example faults', () => {
|
||||
const faultFiveTriggerTime = await getFaultTriggerTime(page, 5);
|
||||
|
||||
// should be all faults (5)
|
||||
let faultResultCount = await getFaultResultCount(page);
|
||||
expect(faultResultCount).toEqual(5);
|
||||
await expect(page.getByLabel('Fault triggered at')).toHaveCount(5);
|
||||
|
||||
// search namespace
|
||||
await enterSearchTerm(page, faultThreeNamespace);
|
||||
await page
|
||||
.getByLabel('Fault Management Object View')
|
||||
.getByLabel('Search Input')
|
||||
.fill(faultThreeNamespace);
|
||||
|
||||
faultResultCount = await getFaultResultCount(page);
|
||||
expect(faultResultCount).toEqual(1);
|
||||
await expect(page.getByLabel('Fault triggered at')).toHaveCount(1);
|
||||
expect(await getFaultNamespace(page, 1)).toEqual(faultThreeNamespace);
|
||||
|
||||
// all faults
|
||||
await clearSearch(page);
|
||||
faultResultCount = await getFaultResultCount(page);
|
||||
expect(faultResultCount).toEqual(5);
|
||||
await page.getByLabel('Fault Management Object View').getByLabel('Search Input').fill('');
|
||||
await expect(page.getByLabel('Fault triggered at')).toHaveCount(5);
|
||||
|
||||
// search name
|
||||
await enterSearchTerm(page, faultTwoName);
|
||||
await page
|
||||
.getByLabel('Fault Management Object View')
|
||||
.getByLabel('Search Input')
|
||||
.fill(faultTwoName);
|
||||
|
||||
faultResultCount = await getFaultResultCount(page);
|
||||
expect(faultResultCount).toEqual(1);
|
||||
await expect(page.getByLabel('Fault triggered at')).toHaveCount(1);
|
||||
expect(await getFaultName(page, 1)).toEqual(faultTwoName);
|
||||
|
||||
// all faults
|
||||
await clearSearch(page);
|
||||
faultResultCount = await getFaultResultCount(page);
|
||||
expect(faultResultCount).toEqual(5);
|
||||
await page.getByLabel('Fault Management Object View').getByLabel('Search Input').fill('');
|
||||
await expect(page.getByLabel('Fault triggered at')).toHaveCount(5);
|
||||
|
||||
// search triggerTime
|
||||
await enterSearchTerm(page, faultFiveTriggerTime);
|
||||
await page
|
||||
.getByLabel('Fault Management Object View')
|
||||
.getByLabel('Search Input')
|
||||
.fill(faultFiveTriggerTime);
|
||||
|
||||
faultResultCount = await getFaultResultCount(page);
|
||||
expect(faultResultCount).toEqual(1);
|
||||
await expect(page.getByLabel('Fault triggered at')).toHaveCount(1);
|
||||
expect(await getFaultTriggerTime(page, 1)).toEqual(faultFiveTriggerTime);
|
||||
});
|
||||
|
||||
test('Allows you to sort faults', async ({ page }) => {
|
||||
const highestSeverity = await getHighestSeverity(page);
|
||||
const lowestSeverity = await getLowestSeverity(page);
|
||||
/**
|
||||
* Compares two severity levels and returns a number indicating their relative order.
|
||||
*
|
||||
* @param {'CRITICAL' | 'WARNING' | 'WATCH'} severity1 - The first severity level to compare.
|
||||
* @param {'CRITICAL' | 'WARNING' | 'WATCH'} severity2 - The second severity level to compare.
|
||||
* @returns {number} - A negative number if severity1 is less severe than severity2,
|
||||
* a positive number if severity1 is more severe than severity2,
|
||||
* or 0 if they are equally severe.
|
||||
*/
|
||||
// eslint-disable-next-line func-style
|
||||
const compareSeverity = (severity1, severity2) => {
|
||||
const severityOrder = ['WATCH', 'WARNING', 'CRITICAL'];
|
||||
return severityOrder.indexOf(severity1) - severityOrder.indexOf(severity2);
|
||||
};
|
||||
|
||||
const faultOneName = 'Example Fault 1';
|
||||
const faultFiveName = 'Example Fault 5';
|
||||
let firstFaultName = await getFaultName(page, 1);
|
||||
@ -239,10 +240,19 @@ test.describe('The Fault Management Plugin using example faults', () => {
|
||||
|
||||
await sortFaultsBy(page, 'severity');
|
||||
|
||||
const sortedHighestSeverity = await getFaultSeverity(page, 1);
|
||||
const sortedLowestSeverity = await getFaultSeverity(page, 5);
|
||||
expect(sortedHighestSeverity).toEqual(highestSeverity);
|
||||
expect(sortedLowestSeverity).toEqual(lowestSeverity);
|
||||
const firstFaultSeverityLabel = await page
|
||||
.getByLabel('Severity:')
|
||||
.first()
|
||||
.getAttribute('aria-label');
|
||||
const firstFaultSeverity = firstFaultSeverityLabel.split(' ').slice(1).join(' ');
|
||||
|
||||
const lastFaultSeverityLabel = await page
|
||||
.getByLabel('Severity:')
|
||||
.last()
|
||||
.getAttribute('aria-label');
|
||||
const lastFaultSeverity = lastFaultSeverityLabel.split(' ').slice(1).join(' ');
|
||||
|
||||
expect(compareSeverity(firstFaultSeverity, lastFaultSeverity)).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
@ -252,24 +262,18 @@ test.describe('The Fault Management Plugin without using example faults', () =>
|
||||
});
|
||||
|
||||
test('Shows no faults when no faults are provided', async ({ page }) => {
|
||||
const faultCount = await page.locator('c-fault-mgmt__list').count();
|
||||
|
||||
expect(faultCount).toEqual(0);
|
||||
await expect(page.getByLabel('Fault triggered at')).toHaveCount(0);
|
||||
|
||||
await changeViewTo(page, 'acknowledged');
|
||||
const acknowledgedCount = await page.locator('c-fault-mgmt__list').count();
|
||||
expect(acknowledgedCount).toEqual(0);
|
||||
await expect(page.getByLabel('Fault triggered at')).toHaveCount(0);
|
||||
|
||||
await changeViewTo(page, 'shelved');
|
||||
const shelvedCount = await page.locator('c-fault-mgmt__list').count();
|
||||
expect(shelvedCount).toEqual(0);
|
||||
await expect(page.getByLabel('Fault triggered at')).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('Will return no faults when searching', async ({ page }) => {
|
||||
await enterSearchTerm(page, 'fault');
|
||||
await page.getByLabel('Fault Management Object View').getByLabel('Search Input').fill('fault');
|
||||
|
||||
const faultCount = await page.locator('c-fault-mgmt__list').count();
|
||||
|
||||
expect(faultCount).toEqual(0);
|
||||
await expect(page.getByLabel('Fault triggered at')).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
|
@ -24,7 +24,7 @@ import { fileURLToPath } from 'url';
|
||||
|
||||
import {
|
||||
createDomainObjectWithDefaults,
|
||||
setIndependentTimeConductorBounds
|
||||
setFixedIndependentTimeConductorBounds
|
||||
} from '../../../../appActions.js';
|
||||
import { expect, test } from '../../../../pluginFixtures.js';
|
||||
|
||||
@ -78,8 +78,8 @@ test.describe('Flexible Layout', () => {
|
||||
// Expand the 'My Items' folder in the left tree
|
||||
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').first().click();
|
||||
// Add the Sine Wave Generator and Clock to the Flexible Layout
|
||||
await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first());
|
||||
await clockTreeItem.dragTo(page.locator('.c-fl__container.is-empty'));
|
||||
await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl-container.is-empty').first());
|
||||
await clockTreeItem.dragTo(page.locator('.c-fl-container.is-empty'));
|
||||
// Check that panes can be dragged while Flexible Layout is in Edit mode
|
||||
let dragWrapper = page
|
||||
.locator('.c-fl-container__frames-holder .c-fl-frame__drag-wrapper')
|
||||
@ -105,8 +105,8 @@ test.describe('Flexible Layout', () => {
|
||||
// Expand the 'My Items' folder in the left tree
|
||||
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').first().click();
|
||||
// Add the Sine Wave Generator and Clock to the Flexible Layout
|
||||
await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first());
|
||||
await clockTreeItem.dragTo(page.locator('.c-fl__container.is-empty'));
|
||||
await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl-container.is-empty').first());
|
||||
await clockTreeItem.dragTo(page.locator('.c-fl-container.is-empty'));
|
||||
|
||||
// Click on the first frame to select it
|
||||
await page.locator('.c-fl-container__frame').first().click();
|
||||
@ -122,7 +122,7 @@ test.describe('Flexible Layout', () => {
|
||||
expect(await page.locator('.c-fl--rows').count()).toEqual(0);
|
||||
|
||||
// Change the layout to rows orientation
|
||||
await page.getByTitle('Columns layout').click();
|
||||
await page.getByTitle('Switch to rows layout').click();
|
||||
|
||||
// Assert the layout is in rows orientation
|
||||
expect(await page.locator('.c-fl--rows').count()).toBeGreaterThan(0);
|
||||
@ -171,7 +171,7 @@ test.describe('Flexible Layout', () => {
|
||||
// Expand the 'My Items' folder in the left tree
|
||||
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').first().click();
|
||||
// Add the Sine Wave Generator to the Flexible Layout and save changes
|
||||
await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first());
|
||||
await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl-container.is-empty').first());
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
@ -202,7 +202,7 @@ test.describe('Flexible Layout', () => {
|
||||
// Expand the 'My Items' folder in the left tree
|
||||
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();
|
||||
// Add the Sine Wave Generator to the Flexible Layout and save changes
|
||||
await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first());
|
||||
await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl-container.is-empty').first());
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
@ -242,13 +242,13 @@ test.describe('Flexible Layout', () => {
|
||||
name: new RegExp(exampleImageryObject.name)
|
||||
});
|
||||
// Add the Sine Wave Generator to the Flexible Layout and save changes
|
||||
await exampleImageryTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first());
|
||||
await exampleImageryTreeItem.dragTo(page.locator('.c-fl-container.is-empty').first());
|
||||
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
// flip on independent time conductor
|
||||
await setIndependentTimeConductorBounds(page, {
|
||||
await setFixedIndependentTimeConductorBounds(page, {
|
||||
start: '2021-12-30 01:01:00.000Z',
|
||||
end: '2021-12-30 01:11:00.000Z'
|
||||
});
|
||||
@ -292,7 +292,7 @@ test.describe('Flexible Layout Toolbar Actions @localStorage', () => {
|
||||
await expect(page.getByRole('dialog', { name: 'Overlay' })).toContainText(
|
||||
'This action will permanently delete this container from this Flexible Layout. Do you want to continue?'
|
||||
);
|
||||
await page.getByRole('button', { name: 'OK', exact: true }).click();
|
||||
await page.getByRole('button', { name: 'Ok', exact: true }).click();
|
||||
expect(await containerHandles.count()).toEqual(2);
|
||||
});
|
||||
test('Remove Frame', async ({ page }) => {
|
||||
@ -302,16 +302,16 @@ test.describe('Flexible Layout Toolbar Actions @localStorage', () => {
|
||||
await expect(page.getByRole('dialog', { name: 'Overlay' })).toContainText(
|
||||
'This action will remove this frame from this Flexible Layout. Do you want to continue?'
|
||||
);
|
||||
await page.getByRole('button', { name: 'OK', exact: true }).click();
|
||||
await page.getByRole('button', { name: 'Ok', exact: true }).click();
|
||||
expect(await page.getByRole('group', { name: 'Frame' }).count()).toEqual(1);
|
||||
});
|
||||
test('Columns/Rows Layout Toggle', async ({ page }) => {
|
||||
await page.getByRole('columnheader', { name: 'Container Handle 1' }).click();
|
||||
const flexRows = page.getByLabel('Flexible Layout Row');
|
||||
expect(await flexRows.count()).toEqual(0);
|
||||
await page.getByTitle('Columns layout').click();
|
||||
await page.getByTitle('Switch to rows layout').click();
|
||||
expect(await flexRows.count()).toEqual(1);
|
||||
await page.getByTitle('Rows layout').click();
|
||||
await page.getByTitle('Switch to columns layout').click();
|
||||
expect(await flexRows.count()).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
@ -28,7 +28,9 @@ import { v4 as uuid } from 'uuid';
|
||||
|
||||
import {
|
||||
createDomainObjectWithDefaults,
|
||||
createExampleTelemetryObject
|
||||
createExampleTelemetryObject,
|
||||
setRealTimeMode,
|
||||
setStartOffset
|
||||
} from '../../../../appActions.js';
|
||||
import { expect, test } from '../../../../pluginFixtures.js';
|
||||
|
||||
@ -38,7 +40,7 @@ test.describe('Gauge', () => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
});
|
||||
|
||||
test('Can add and remove telemetry sources @unstable', async ({ page }) => {
|
||||
test('Can add and remove telemetry sources', async ({ page }) => {
|
||||
// Create the gauge with defaults
|
||||
const gauge = await createDomainObjectWithDefaults(page, { type: 'Gauge' });
|
||||
|
||||
@ -53,6 +55,7 @@ test.describe('Gauge', () => {
|
||||
// the SWG appears in the elements pool
|
||||
await page.goto(gauge.url);
|
||||
await page.getByLabel('Edit Object').click();
|
||||
await page.getByRole('tab', { name: 'Elements' }).click();
|
||||
await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeVisible();
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
@ -65,38 +68,35 @@ test.describe('Gauge', () => {
|
||||
});
|
||||
|
||||
// Verify that the 'Replace telemetry source' modal appears and accept it
|
||||
await expect
|
||||
.soft(
|
||||
page.locator(
|
||||
'text=This action will replace the current telemetry source. Do you want to continue?'
|
||||
)
|
||||
await expect(
|
||||
page.getByText(
|
||||
'This action will replace the current telemetry source. Do you want to continue?'
|
||||
)
|
||||
.toBeVisible();
|
||||
await page.click('text=Ok');
|
||||
).toBeVisible();
|
||||
await page.getByRole('button', { name: 'Ok', exact: true }).click();
|
||||
|
||||
// Navigate to the gauge and verify that the new SWG
|
||||
// appears in the elements pool and the old one is gone
|
||||
await page.goto(gauge.url);
|
||||
await page.getByLabel('Edit Object').click();
|
||||
await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeHidden();
|
||||
await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg2.name}`)).toBeVisible();
|
||||
await page.getByRole('tab', { name: 'Elements' }).click();
|
||||
await expect(page.getByLabel(`Preview ${swg1.name}`)).toBeHidden();
|
||||
await expect(page.getByLabel(`Preview ${swg2.name}`)).toBeVisible();
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
// Right click on the new SWG in the elements pool and delete it
|
||||
await page.locator(`#inspector-elements-tree >> text=${swg2.name}`).click({
|
||||
await page.getByLabel(`Preview ${swg2.name}`).click({
|
||||
button: 'right'
|
||||
});
|
||||
await page.locator('li[title="Remove this object from its containing object."]').click();
|
||||
await page.getByLabel('Remove').click();
|
||||
|
||||
// Verify that the 'Remove object' confirmation modal appears and accept it
|
||||
await expect
|
||||
.soft(
|
||||
page.locator(
|
||||
'text=Warning! This action will remove this object. Are you sure you want to continue?'
|
||||
)
|
||||
await expect(
|
||||
page.getByText(
|
||||
'Warning! This action will remove this object. Are you sure you want to continue?'
|
||||
)
|
||||
.toBeVisible();
|
||||
await page.click('text=Ok');
|
||||
).toBeVisible();
|
||||
await page.getByRole('button', { name: 'Ok', exact: true }).click();
|
||||
|
||||
// Verify that the elements pool shows no elements
|
||||
await expect(page.locator('text="No contained elements"')).toBeVisible();
|
||||
@ -110,11 +110,11 @@ test.describe('Gauge', () => {
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
|
||||
// Click the object specified by 'type'
|
||||
await page.click(`li[role='menuitem']:text("Gauge")`);
|
||||
await page.getByRole('menuitem', { name: 'Gauge' }).click();
|
||||
// FIXME: We need better selectors for these custom form controls
|
||||
const displayCurrentValueSwitch = page.locator('.c-toggle-switch__slider >> nth=0');
|
||||
await displayCurrentValueSwitch.setChecked(false);
|
||||
await page.click('button[aria-label="Save"]');
|
||||
await displayCurrentValueSwitch.uncheck();
|
||||
await page.getByLabel('Save').click();
|
||||
|
||||
// TODO: Verify changes in the UI
|
||||
});
|
||||
@ -126,24 +126,21 @@ test.describe('Gauge', () => {
|
||||
|
||||
// Create the gauge with defaults
|
||||
await createDomainObjectWithDefaults(page, { type: 'Gauge' });
|
||||
await page.click('button[title="More actions"]');
|
||||
await page.click('li[role="menuitem"]:has-text("Edit Properties")');
|
||||
await page.getByLabel('More actions').click();
|
||||
await page.getByRole('menuitem', { name: 'Edit Properties...' }).click();
|
||||
// FIXME: We need better selectors for these custom form controls
|
||||
const displayCurrentValueSwitch = page.locator('.c-toggle-switch__slider >> nth=0');
|
||||
await displayCurrentValueSwitch.setChecked(false);
|
||||
await page.click('button[aria-label="Save"]');
|
||||
await displayCurrentValueSwitch.uncheck();
|
||||
await page.getByLabel('Save').click();
|
||||
|
||||
// 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'
|
||||
type: 'Gauge',
|
||||
name: 'Gauge with no data'
|
||||
});
|
||||
|
||||
// Create a Sine Wave Generator in the Gauge with a loading delay
|
||||
@ -154,7 +151,7 @@ test.describe('Gauge', () => {
|
||||
await page.getByRole('menuitem', { name: /Edit Properties.../ }).click();
|
||||
|
||||
//Edit Example Telemetry Object to include 5s loading Delay
|
||||
await page.locator('[aria-label="Loading Delay \\(ms\\)"]').fill('5000');
|
||||
await page.getByLabel('Loading Delay (ms)', { exact: true }).fill('5000');
|
||||
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
@ -162,9 +159,64 @@ test.describe('Gauge', () => {
|
||||
await page.waitForURL(`**/${gauge.uuid}/*`);
|
||||
|
||||
// Nav to the Gauge
|
||||
await page.goto(gauge.url);
|
||||
const gaugeNoDataText = await page.locator('.js-dial-current-value tspan').textContent();
|
||||
expect(gaugeNoDataText).toBe('--');
|
||||
await page.goto(gauge.url, { waitUntil: 'domcontentloaded' });
|
||||
// Check that the value is not displayed
|
||||
//TODO https://github.com/nasa/openmct/issues/7790 update this locator
|
||||
await expect(page.getByTitle('Value is currently out of')).toHaveAttribute(
|
||||
'aria-valuenow',
|
||||
'--'
|
||||
);
|
||||
});
|
||||
|
||||
test('Gauge does not break when an object is missing', async ({ page }) => {
|
||||
// Set up error listeners
|
||||
const pageErrors = [];
|
||||
|
||||
// Listen for uncaught exceptions
|
||||
page.on('pageerror', (err) => {
|
||||
pageErrors.push(err.message);
|
||||
});
|
||||
|
||||
await setRealTimeMode(page);
|
||||
|
||||
// Create a Gauge
|
||||
const gauge = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Gauge',
|
||||
name: 'Gauge with missing object'
|
||||
});
|
||||
|
||||
// Create a Sine Wave Generator in the Gauge with a loading delay
|
||||
const missingSWG = await createExampleTelemetryObject(page, gauge.uuid);
|
||||
|
||||
// Remove the object from local storage
|
||||
await page.evaluate(
|
||||
([missingObject]) => {
|
||||
const mct = localStorage.getItem('mct');
|
||||
const mctObjects = JSON.parse(mct);
|
||||
delete mctObjects[missingObject.uuid];
|
||||
localStorage.setItem('mct', JSON.stringify(mctObjects));
|
||||
},
|
||||
[missingSWG]
|
||||
);
|
||||
|
||||
// Verify start bounds
|
||||
await expect(page.getByLabel('Start offset: 00:30:00')).toBeVisible();
|
||||
|
||||
// Nav to the Gauge
|
||||
await page.goto(gauge.url, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// adjust time bounds and ensure they are updated
|
||||
await setStartOffset(page, {
|
||||
startHours: '00',
|
||||
startMins: '45',
|
||||
startSecs: '00'
|
||||
});
|
||||
|
||||
// Verify start bounds changed
|
||||
await expect(page.getByLabel('Start offset: 00:45:00')).toBeVisible();
|
||||
|
||||
// // Verify no errors were thrown
|
||||
expect(pageErrors).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('Gauge enforces composition policy', async ({ page }) => {
|
||||
|
@ -25,10 +25,20 @@ This test suite is dedicated to tests which verify the basic operations surround
|
||||
but only assume that example imagery is present.
|
||||
*/
|
||||
|
||||
import { createDomainObjectWithDefaults, setRealTimeMode } from '../../../../appActions.js';
|
||||
import { waitForAnimations } from '../../../../baseFixtures.js';
|
||||
import {
|
||||
createDomainObjectWithDefaults,
|
||||
navigateToObjectWithRealTime,
|
||||
setRealTimeMode
|
||||
} from '../../../../appActions.js';
|
||||
import {
|
||||
createImageryViewWithShortDelay,
|
||||
FIVE_MINUTES,
|
||||
IMAGE_LOAD_DELAY,
|
||||
MOUSE_WHEEL_DELTA_Y,
|
||||
THIRTY_SECONDS
|
||||
} from '../../../../helper/imageryUtils.js';
|
||||
import { expect, test } from '../../../../pluginFixtures.js';
|
||||
const backgroundImageSelector = '.c-imagery__main-image__background-image';
|
||||
|
||||
const panHotkey = process.platform === 'linux' ? ['Shift', 'Alt'] : ['Alt'];
|
||||
const tagHotkey = ['Shift', 'Alt'];
|
||||
const expectedAltText = process.platform === 'linux' ? 'Shift+Alt drag to pan' : 'Alt drag to pan';
|
||||
@ -45,8 +55,10 @@ test.describe('Example Imagery Object', () => {
|
||||
|
||||
// Verify that the created object is focused
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText(exampleImagery.name);
|
||||
await page.locator('.c-imagery__main-image__bg').hover({ trial: true });
|
||||
await page.locator(backgroundImageSelector).waitFor();
|
||||
await page.getByLabel('Focused Image Element').hover({ trial: true });
|
||||
|
||||
// Wait for image thumbnail auto-scroll to complete
|
||||
await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport();
|
||||
});
|
||||
|
||||
test('Can use Mouse Wheel to zoom in and out of latest image', async ({ page }) => {
|
||||
@ -63,9 +75,10 @@ test.describe('Example Imagery Object', () => {
|
||||
|
||||
test('Can right click on image and open it in a new tab @2p', async ({ page, context }) => {
|
||||
// try to right click on image
|
||||
const backgroundImage = await page.locator(backgroundImageSelector);
|
||||
const backgroundImage = page.getByLabel('Focused Image Element');
|
||||
await backgroundImage.click({
|
||||
button: 'right',
|
||||
// Need force option here due to annotation overlay which blocks playwright's click
|
||||
// eslint-disable-next-line playwright/no-force-option
|
||||
force: true
|
||||
});
|
||||
@ -80,12 +93,9 @@ test.describe('Example Imagery Object', () => {
|
||||
const newPage = await pagePromise;
|
||||
await newPage.waitForLoadState();
|
||||
// expect new tab url to have jpg in it
|
||||
await expect(newPage.url()).toContain('.jpg');
|
||||
expect(newPage.url()).toContain('.jpg');
|
||||
});
|
||||
|
||||
// this requires CORS to be enabled in some fashion
|
||||
test.fixme('Can right click on image and save it as a file', async ({ page }) => {});
|
||||
|
||||
test('Can adjust image brightness/contrast by dragging the sliders', async ({
|
||||
page,
|
||||
browserName
|
||||
@ -105,27 +115,36 @@ test.describe('Example Imagery Object', () => {
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/6821'
|
||||
});
|
||||
|
||||
// Test independent fixed time with global fixed time
|
||||
// flip on independent time conductor
|
||||
await page.getByRole('switch', { name: 'Enable Independent Time Conductor' }).click();
|
||||
await page.getByLabel('Enable Independent Time Conductor').click();
|
||||
|
||||
// Adding in delay to address flakiness of ITC test-- button event handlers not registering in time
|
||||
await expect(page.locator('#independentTCToggle')).toBeChecked();
|
||||
await expect(page.locator('.c-compact-tc').first()).toBeVisible();
|
||||
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Independent Time Conductor Settings' })
|
||||
).toBeEnabled();
|
||||
await page.getByRole('button', { name: 'Independent Time Conductor Settings' }).click();
|
||||
await expect(page.getByLabel('Time Conductor Options')).toBeVisible();
|
||||
await page.getByLabel('Time Conductor Options').hover({ trial: true });
|
||||
|
||||
await page.getByRole('textbox', { name: 'Start date' }).hover({ trial: true });
|
||||
await page.getByRole('textbox', { name: 'Start date' }).fill('2021-12-30');
|
||||
await page.keyboard.press('Tab');
|
||||
await page.getByRole('textbox', { name: 'Start time' }).hover({ trial: true });
|
||||
await page.getByRole('textbox', { name: 'Start time' }).fill('01:01:00');
|
||||
await page.keyboard.press('Tab');
|
||||
await page.getByRole('textbox', { name: 'End date' }).hover({ trial: true });
|
||||
await page.getByRole('textbox', { name: 'End date' }).fill('2021-12-30');
|
||||
await page.keyboard.press('Tab');
|
||||
await page.getByRole('textbox', { name: 'End time' }).hover({ trial: true });
|
||||
await page.getByRole('textbox', { name: 'End time' }).fill('01:11:00');
|
||||
await page.keyboard.press('Tab');
|
||||
await page.keyboard.press('Enter');
|
||||
await page.getByRole('textbox', { name: 'End time' }).fill('01:11:00');
|
||||
await page.getByLabel('Submit time bounds').click();
|
||||
|
||||
// check image date
|
||||
// wait for image thumbnails to stabilize
|
||||
await page.getByLabel('Image Thumbnails', { exact: true }).hover({ trial: true });
|
||||
await expect(page.getByText('2021-12-30 01:01:00.000Z').first()).toBeVisible();
|
||||
|
||||
// flip it off
|
||||
@ -166,14 +185,11 @@ test.describe('Example Imagery Object', () => {
|
||||
});
|
||||
|
||||
test('Can use alt+drag to move around image once zoomed in', async ({ page }) => {
|
||||
const deltaYStep = 100; //equivalent to 1x zoom
|
||||
|
||||
await page.locator('.c-imagery__main-image__bg').hover({ trial: true });
|
||||
|
||||
// zoom in
|
||||
await page.mouse.wheel(0, deltaYStep * 2);
|
||||
await page.locator('.c-imagery__main-image__bg').hover({ trial: true });
|
||||
const zoomedBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
await page.mouse.wheel(0, MOUSE_WHEEL_DELTA_Y * 2);
|
||||
const zoomedBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
|
||||
const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;
|
||||
const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
|
||||
// move to the right
|
||||
@ -195,7 +211,7 @@ test.describe('Example Imagery Object', () => {
|
||||
await page.mouse.move(imageCenterX - 200, imageCenterY, 10);
|
||||
await page.mouse.up();
|
||||
await Promise.all(panHotkey.map((x) => page.keyboard.up(x)));
|
||||
const afterRightPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
const afterRightPanBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
|
||||
expect(zoomedBoundingBox.x).toBeGreaterThan(afterRightPanBoundingBox.x);
|
||||
|
||||
// pan left
|
||||
@ -204,7 +220,7 @@ test.describe('Example Imagery Object', () => {
|
||||
await page.mouse.move(imageCenterX, imageCenterY, 10);
|
||||
await page.mouse.up();
|
||||
await Promise.all(panHotkey.map((x) => page.keyboard.up(x)));
|
||||
const afterLeftPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
const afterLeftPanBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
|
||||
expect(afterRightPanBoundingBox.x).toBeLessThan(afterLeftPanBoundingBox.x);
|
||||
|
||||
// pan up
|
||||
@ -214,7 +230,7 @@ test.describe('Example Imagery Object', () => {
|
||||
await page.mouse.move(imageCenterX, imageCenterY + 200, 10);
|
||||
await page.mouse.up();
|
||||
await Promise.all(panHotkey.map((x) => page.keyboard.up(x)));
|
||||
const afterUpPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
const afterUpPanBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
|
||||
expect(afterUpPanBoundingBox.y).toBeGreaterThan(afterLeftPanBoundingBox.y);
|
||||
|
||||
// pan down
|
||||
@ -223,7 +239,7 @@ test.describe('Example Imagery Object', () => {
|
||||
await page.mouse.move(imageCenterX, imageCenterY - 200, 10);
|
||||
await page.mouse.up();
|
||||
await Promise.all(panHotkey.map((x) => page.keyboard.up(x)));
|
||||
const afterDownPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
const afterDownPanBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
|
||||
expect(afterDownPanBoundingBox.y).toBeLessThan(afterUpPanBoundingBox.y);
|
||||
});
|
||||
|
||||
@ -282,26 +298,43 @@ test.describe('Example Imagery Object', () => {
|
||||
await expect(page.getByText('Drilling')).toBeVisible();
|
||||
});
|
||||
|
||||
test('Can use + - buttons to zoom on the image @unstable', async ({ page }) => {
|
||||
test('Can use + - buttons to zoom on the image', async ({ page }) => {
|
||||
await buttonZoomOnImageAndAssert(page);
|
||||
});
|
||||
|
||||
test('Can use the reset button to reset the image @unstable', async ({ page }, testInfo) => {
|
||||
test('Can use the reset button to reset the image', async ({ page }) => {
|
||||
await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty(
|
||||
'style.transform',
|
||||
'scale(1) translate(0px, 0px)'
|
||||
);
|
||||
|
||||
// Get initial image dimensions
|
||||
const initialBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
const initialBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
|
||||
|
||||
// Zoom in twice via button
|
||||
await zoomIntoImageryByButton(page);
|
||||
await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty(
|
||||
'style.transform',
|
||||
'scale(2) translate(0px, 0px)'
|
||||
);
|
||||
await zoomIntoImageryByButton(page);
|
||||
await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty(
|
||||
'style.transform',
|
||||
'scale(3) translate(0px, 0px)'
|
||||
);
|
||||
|
||||
// Get and assert zoomed in image dimensions
|
||||
const zoomedInBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
expect.soft(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
|
||||
expect.soft(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);
|
||||
const zoomedInBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
|
||||
expect(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
|
||||
expect(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);
|
||||
|
||||
// Reset pan and zoom and assert against initial image dimensions
|
||||
await resetImageryPanAndZoom(page);
|
||||
const finalBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty(
|
||||
'style.transform',
|
||||
'scale(1) translate(0px, 0px)'
|
||||
);
|
||||
const finalBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
|
||||
expect(finalBoundingBox).toEqual(initialBoundingBox);
|
||||
});
|
||||
|
||||
@ -319,25 +352,25 @@ test.describe('Example Imagery Object', () => {
|
||||
});
|
||||
|
||||
test('Uses low fetch priority', async ({ page }) => {
|
||||
const priority = await page.locator('.js-imageryView-image').getAttribute('fetchpriority');
|
||||
expect(priority).toBe('low');
|
||||
const priority = page.locator('.js-imageryView-image');
|
||||
await expect(priority).toHaveAttribute('fetchpriority', 'low');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Example Imagery in Display Layout', () => {
|
||||
let displayLayout;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
displayLayout = await createDomainObjectWithDefaults(page, { type: 'Display Layout' });
|
||||
await page.goto(displayLayout.url);
|
||||
|
||||
await createImageryView(page);
|
||||
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText(
|
||||
'Unnamed Example Imagery'
|
||||
);
|
||||
// Create Example Imagery inside Display Layout
|
||||
await createImageryViewWithShortDelay(page, {
|
||||
name: 'Unnamed Example Imagery',
|
||||
parent: displayLayout.uuid
|
||||
});
|
||||
|
||||
await page.goto(displayLayout.url);
|
||||
});
|
||||
@ -390,12 +423,7 @@ test.describe('Example Imagery in Display Layout', () => {
|
||||
await expect.soft(pausePlayButton).toHaveClass(/is-paused/);
|
||||
});
|
||||
|
||||
test('Imagery View operations @unstable', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/5265'
|
||||
});
|
||||
|
||||
test('Imagery View operations', async ({ page }) => {
|
||||
// Edit mode
|
||||
await page.getByLabel('Edit Object').click();
|
||||
|
||||
@ -410,7 +438,9 @@ test.describe('Example Imagery in Display Layout', () => {
|
||||
await page.locator('div[title="Resize object width"] > input').click();
|
||||
await page.locator('div[title="Resize object width"] > input').fill('50');
|
||||
|
||||
await performImageryViewOperationsAndAssert(page);
|
||||
await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport();
|
||||
|
||||
await performImageryViewOperationsAndAssert(page, displayLayout);
|
||||
});
|
||||
|
||||
test('Resizing the layout changes thumbnail visibility and size', async ({ page }) => {
|
||||
@ -454,7 +484,10 @@ test.describe('Example Imagery in Display Layout', () => {
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/6709'
|
||||
});
|
||||
await createImageryView(page);
|
||||
await createImageryViewWithShortDelay(page, {
|
||||
name: 'Unnamed Example Imagery',
|
||||
parent: displayLayout.uuid
|
||||
});
|
||||
await page.goto(displayLayout.url);
|
||||
|
||||
const imageElements = page.locator('.c-imagery__main-image-wrapper');
|
||||
@ -491,71 +524,43 @@ test.describe('Example Imagery in Flexible layout', () => {
|
||||
flexibleLayout = await createDomainObjectWithDefaults(page, { type: 'Flexible Layout' });
|
||||
|
||||
// Create Example Imagery inside the Flexible Layout
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Example Imagery',
|
||||
await createImageryViewWithShortDelay(page, {
|
||||
name: 'Unnamed Example Imagery',
|
||||
parent: flexibleLayout.uuid
|
||||
});
|
||||
|
||||
// Navigate back to Flexible Layout
|
||||
await page.goto(flexibleLayout.url);
|
||||
// Wait for image thumbnail auto-scroll to complete
|
||||
await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport();
|
||||
});
|
||||
|
||||
test('Can double-click on the image to view large image', async ({ page }) => {
|
||||
// Double-click on the image to open large view
|
||||
const imageElement = await page.getByRole('button', { name: 'Image Wrapper' });
|
||||
const imageElement = page.getByRole('button', { name: 'Image Wrapper' });
|
||||
await imageElement.dblclick();
|
||||
|
||||
// Check if the large view is visible
|
||||
await page.getByRole('button', { name: 'Background Image', state: 'visible' });
|
||||
page.getByRole('button', { name: 'Focused Image Element', state: 'visible' });
|
||||
|
||||
// Close the large view
|
||||
await page.getByRole('button', { name: 'Close' }).click();
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
flexibleLayout = await createDomainObjectWithDefaults(page, { type: 'Flexible Layout' });
|
||||
await page.goto(flexibleLayout.url);
|
||||
|
||||
/* Create Sine Wave Generator with minimum Image Load Delay */
|
||||
// Click the Create button
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
|
||||
// Click text=Example Imagery
|
||||
await page.click('li[role="menuitem"]:has-text("Example Imagery")');
|
||||
|
||||
// Clear and set Image load delay to minimum value
|
||||
await page.locator('input[type="number"]').fill('');
|
||||
await page.locator('input[type="number"]').fill('5000');
|
||||
|
||||
// Click text=OK
|
||||
await Promise.all([
|
||||
page.waitForNavigation({ waitUntil: 'networkidle' }),
|
||||
page.click('button:has-text("OK")'),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText(
|
||||
'Unnamed Example Imagery'
|
||||
);
|
||||
|
||||
await page.goto(flexibleLayout.url);
|
||||
});
|
||||
test('Imagery View operations @unstable', async ({ page, browserName }) => {
|
||||
test('Imagery View operations', async ({ page, browserName }) => {
|
||||
test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox');
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/5326'
|
||||
});
|
||||
|
||||
await performImageryViewOperationsAndAssert(page);
|
||||
await performImageryViewOperationsAndAssert(page, flexibleLayout);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Example Imagery in Tabs View', () => {
|
||||
let tabsView;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
@ -567,28 +572,25 @@ test.describe('Example Imagery in Tabs View', () => {
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
|
||||
// Click text=Example Imagery
|
||||
await page.click('li[role="menuitem"]:has-text("Example Imagery")');
|
||||
await page.getByRole('menuitem', { name: 'Example Imagery' }).click();
|
||||
|
||||
// Clear and set Image load delay to minimum value
|
||||
await page.locator('input[type="number"]').fill('');
|
||||
await page.locator('input[type="number"]').fill('5000');
|
||||
await page.locator('input[type="number"]').clear();
|
||||
await page.locator('input[type="number"]').fill(`${IMAGE_LOAD_DELAY}`);
|
||||
|
||||
// Click text=OK
|
||||
await Promise.all([
|
||||
page.waitForNavigation({ waitUntil: 'networkidle' }),
|
||||
page.click('button:has-text("OK")'),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
await page.getByLabel('Save').click();
|
||||
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText(
|
||||
'Unnamed Example Imagery'
|
||||
);
|
||||
|
||||
await page.goto(tabsView.url);
|
||||
// Wait for image thumbnail auto-scroll to complete
|
||||
await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport();
|
||||
});
|
||||
test('Imagery View operations @unstable', async ({ page }) => {
|
||||
await performImageryViewOperationsAndAssert(page);
|
||||
|
||||
test('Imagery View operations', async ({ page }) => {
|
||||
await performImageryViewOperationsAndAssert(page, tabsView);
|
||||
});
|
||||
});
|
||||
|
||||
@ -648,24 +650,28 @@ test.describe('Example Imagery in Time Strip', () => {
|
||||
* 3. Can pan the image using the pan hotkey + mouse drag
|
||||
* 4. Clicking on the left arrow button pauses imagery and moves to the previous image
|
||||
* 5. Imagery is updated as new images stream in, regardless of pause status
|
||||
* 6. Old images are discarded when new images stream in
|
||||
* 7. Image brightness/contrast can be adjusted by dragging the sliders
|
||||
* 6. Old images are discarded when their timestamps fall out of bounds
|
||||
* 7. Multiple images can be discarded when their timestamps fall out of bounds
|
||||
* 8. Image brightness/contrast can be adjusted by dragging the sliders
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function performImageryViewOperationsAndAssert(page) {
|
||||
// Verify that imagery thumbnails use a thumbnail url
|
||||
const thumbnailImages = page.locator('.c-thumb__image');
|
||||
const mainImage = page.locator('.c-imagery__main-image__image');
|
||||
await expect(thumbnailImages.first()).toHaveAttribute('src', thumbnailUrlParamsRegexp);
|
||||
await expect(mainImage).not.toHaveAttribute('src', thumbnailUrlParamsRegexp);
|
||||
async function performImageryViewOperationsAndAssert(page, layoutObject) {
|
||||
await test.step('Verify that imagery thumbnails use a thumbnail url', async () => {
|
||||
const thumbnailImages = page.getByLabel('Image thumbnail from').locator('.c-thumb__image');
|
||||
const mainImage = page.locator('.c-imagery__main-image__image');
|
||||
await expect(thumbnailImages.first()).toHaveAttribute('src', thumbnailUrlParamsRegexp);
|
||||
await expect(mainImage).not.toHaveAttribute('src', thumbnailUrlParamsRegexp);
|
||||
});
|
||||
|
||||
// Click previous image button
|
||||
const previousImageButton = page.locator('.c-nav--prev');
|
||||
await previousImageButton.click();
|
||||
const previousImageButton = page.getByLabel('Previous image');
|
||||
await expect(previousImageButton).toBeVisible();
|
||||
await page.getByLabel('Image Wrapper').hover({ trial: true });
|
||||
|
||||
// Verify previous image
|
||||
const selectedImage = page.locator('.selected');
|
||||
await expect(selectedImage).toBeVisible();
|
||||
// Need to force click as the annotation canvas lies on top of the image
|
||||
// and fails the accessibility checks
|
||||
// eslint-disable-next-line playwright/no-force-option
|
||||
await previousImageButton.click({ force: true });
|
||||
|
||||
// Use the zoom buttons to zoom in and out
|
||||
await buttonZoomOnImageAndAssert(page);
|
||||
@ -680,45 +686,41 @@ async function performImageryViewOperationsAndAssert(page) {
|
||||
await mouseZoomOnImageAndAssert(page, -2);
|
||||
|
||||
// Click next image button
|
||||
const nextImageButton = page.locator('.c-nav--next');
|
||||
await nextImageButton.click();
|
||||
|
||||
const nextImageButton = page.getByLabel('Next image');
|
||||
await expect(nextImageButton).toBeVisible();
|
||||
await page.getByLabel('Image Wrapper').hover({ trial: true });
|
||||
// eslint-disable-next-line playwright/no-force-option
|
||||
await nextImageButton.click({ force: true });
|
||||
// set realtime mode
|
||||
await setRealTimeMode(page);
|
||||
await navigateToObjectWithRealTime(
|
||||
page,
|
||||
layoutObject.url,
|
||||
`${FIVE_MINUTES}`,
|
||||
`${THIRTY_SECONDS}`
|
||||
);
|
||||
// Verify previous image
|
||||
await expect(previousImageButton).toBeVisible();
|
||||
await page.getByLabel('Image Wrapper').hover({ trial: true });
|
||||
// eslint-disable-next-line playwright/no-force-option
|
||||
await previousImageButton.click({ force: true });
|
||||
await page.locator('.active').click();
|
||||
const selectedImage = page.locator('.selected');
|
||||
await expect(selectedImage).toBeVisible();
|
||||
|
||||
// Zoom in on next image
|
||||
await mouseZoomOnImageAndAssert(page, 2);
|
||||
|
||||
// Clicking on the left arrow should pause the imagery and go to previous image
|
||||
await previousImageButton.click();
|
||||
await expect(page.locator('.c-button.pause-play')).toHaveClass(/is-paused/);
|
||||
await expect(page.getByLabel('Pause automatic scrolling of image thumbnails')).toBeVisible();
|
||||
await expect(selectedImage).toBeVisible();
|
||||
|
||||
// The imagery view should be updated when new images come in
|
||||
const imageCount = await page.locator('.c-imagery__thumb').count();
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
const newImageCount = await page.locator('.c-imagery__thumb').count();
|
||||
|
||||
return newImageCount;
|
||||
},
|
||||
{
|
||||
message: 'verify that old images are discarded',
|
||||
timeout: 7 * 1000
|
||||
}
|
||||
)
|
||||
.toBe(imageCount);
|
||||
|
||||
// Verify selected image is still displayed
|
||||
await expect(selectedImage).toBeVisible();
|
||||
|
||||
// Unpause imagery
|
||||
await page.locator('.pause-play').click();
|
||||
|
||||
//Get background-image url from background-image css prop
|
||||
await assertBackgroundImageUrlFromBackgroundCss(page);
|
||||
|
||||
// Open the image filter menu
|
||||
await page.locator('[role=toolbar] button[title="Brightness and contrast"]').click();
|
||||
|
||||
@ -785,51 +787,12 @@ async function assertBackgroundImageBrightness(page, expected) {
|
||||
expect(actual).toBe(expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function assertBackgroundImageUrlFromBackgroundCss(page) {
|
||||
const backgroundImage = page.locator('.c-imagery__main-image__background-image');
|
||||
let backgroundImageUrl = await backgroundImage.evaluate((el) => {
|
||||
return window
|
||||
.getComputedStyle(el)
|
||||
.getPropertyValue('background-image')
|
||||
.match(/url\(([^)]+)\)/)[1];
|
||||
});
|
||||
let backgroundImageUrl1 = backgroundImageUrl.slice(1, -1); //forgive me, padre
|
||||
console.log('backgroundImageUrl1 ' + backgroundImageUrl1);
|
||||
|
||||
let backgroundImageUrl2;
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
// Verify next image has updated
|
||||
let backgroundImageUrlNext = await backgroundImage.evaluate((el) => {
|
||||
return window
|
||||
.getComputedStyle(el)
|
||||
.getPropertyValue('background-image')
|
||||
.match(/url\(([^)]+)\)/)[1];
|
||||
});
|
||||
backgroundImageUrl2 = backgroundImageUrlNext.slice(1, -1); //forgive me, padre
|
||||
|
||||
return backgroundImageUrl2;
|
||||
},
|
||||
{
|
||||
message: 'verify next image has updated',
|
||||
timeout: 7 * 1000
|
||||
}
|
||||
)
|
||||
.not.toBe(backgroundImageUrl1);
|
||||
console.log('backgroundImageUrl2 ' + backgroundImageUrl2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function panZoomAndAssertImageProperties(page) {
|
||||
const imageryHintsText = await page.locator('.c-imagery__hints').innerText();
|
||||
expect(expectedAltText).toEqual(imageryHintsText);
|
||||
const zoomedBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
await expect(page.locator('.c-imagery__hints')).toContainText(expectedAltText);
|
||||
const zoomedBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
|
||||
const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;
|
||||
const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
|
||||
|
||||
@ -839,7 +802,7 @@ async function panZoomAndAssertImageProperties(page) {
|
||||
await page.mouse.move(imageCenterX - 200, imageCenterY, 10);
|
||||
await page.mouse.up();
|
||||
await Promise.all(panHotkey.map((x) => page.keyboard.up(x)));
|
||||
const afterRightPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
const afterRightPanBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
|
||||
expect(zoomedBoundingBox.x).toBeGreaterThan(afterRightPanBoundingBox.x);
|
||||
|
||||
// Pan left
|
||||
@ -848,7 +811,7 @@ async function panZoomAndAssertImageProperties(page) {
|
||||
await page.mouse.move(imageCenterX, imageCenterY, 10);
|
||||
await page.mouse.up();
|
||||
await Promise.all(panHotkey.map((x) => page.keyboard.up(x)));
|
||||
const afterLeftPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
const afterLeftPanBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
|
||||
expect(afterRightPanBoundingBox.x).toBeLessThan(afterLeftPanBoundingBox.x);
|
||||
|
||||
// Pan up
|
||||
@ -858,7 +821,7 @@ async function panZoomAndAssertImageProperties(page) {
|
||||
await page.mouse.move(imageCenterX, imageCenterY + 200, 10);
|
||||
await page.mouse.up();
|
||||
await Promise.all(panHotkey.map((x) => page.keyboard.up(x)));
|
||||
const afterUpPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
const afterUpPanBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
|
||||
expect(afterUpPanBoundingBox.y).toBeGreaterThanOrEqual(afterLeftPanBoundingBox.y);
|
||||
|
||||
// Pan down
|
||||
@ -867,7 +830,7 @@ async function panZoomAndAssertImageProperties(page) {
|
||||
await page.mouse.move(imageCenterX, imageCenterY - 200, 10);
|
||||
await page.mouse.up();
|
||||
await Promise.all(panHotkey.map((x) => page.keyboard.up(x)));
|
||||
const afterDownPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
const afterDownPanBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
|
||||
expect(afterDownPanBoundingBox.y).toBeLessThanOrEqual(afterUpPanBoundingBox.y);
|
||||
}
|
||||
|
||||
@ -879,19 +842,20 @@ async function panZoomAndAssertImageProperties(page) {
|
||||
*/
|
||||
async function mouseZoomOnImageAndAssert(page, factor = 2) {
|
||||
// Zoom in
|
||||
const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox();
|
||||
const deltaYStep = 100; // equivalent to 1x zoom
|
||||
await page.mouse.wheel(0, deltaYStep * factor);
|
||||
const zoomedBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
await page.getByLabel('Focused Image Element').hover({ trial: true });
|
||||
const originalImageDimensions = await page.getByLabel('Focused Image Element').boundingBox();
|
||||
await page.mouse.wheel(0, MOUSE_WHEEL_DELTA_Y * factor);
|
||||
await waitForZoomAndPanTransitions(page);
|
||||
|
||||
const zoomedBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
|
||||
const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;
|
||||
const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
|
||||
|
||||
// center the mouse pointer
|
||||
await page.mouse.move(imageCenterX, imageCenterY);
|
||||
|
||||
// Wait for zoom animation to finish
|
||||
await page.locator('.c-imagery__main-image__bg').hover({ trial: true });
|
||||
const imageMouseZoomed = await page.locator(backgroundImageSelector).boundingBox();
|
||||
// Wait for zoom animation to finish and get the new image dimensions
|
||||
const imageMouseZoomed = await page.getByLabel('Focused Image Element').boundingBox();
|
||||
|
||||
if (factor > 0) {
|
||||
expect(imageMouseZoomed.height).toBeGreaterThan(originalImageDimensions.height);
|
||||
@ -908,30 +872,66 @@ async function mouseZoomOnImageAndAssert(page, factor = 2) {
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function buttonZoomOnImageAndAssert(page) {
|
||||
// Get initial image dimensions
|
||||
const initialBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
await test.step('Can zoom using buttons', async () => {
|
||||
// Lock the zoom and pan so it doesn't reset if a new image comes in
|
||||
await page.getByLabel('Focused Image Element').hover({ trial: true });
|
||||
const lockButton = page.getByRole('button', {
|
||||
name: 'Lock current zoom and pan across all images'
|
||||
});
|
||||
|
||||
// Zoom in twice via button
|
||||
await zoomIntoImageryByButton(page);
|
||||
await zoomIntoImageryByButton(page);
|
||||
await lockButton.isVisible();
|
||||
// if (!(await lockButton.isVisible())) {
|
||||
// await page.getByLabel('Focused Image Element').hover({ trial: true });
|
||||
// }
|
||||
await lockButton.click();
|
||||
|
||||
// Get and assert zoomed in image dimensions
|
||||
const zoomedInBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
expect(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
|
||||
expect(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);
|
||||
await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty(
|
||||
'style.transform',
|
||||
'scale(1) translate(0px, 0px)'
|
||||
);
|
||||
|
||||
// Zoom out once via button
|
||||
await zoomOutOfImageryByButton(page);
|
||||
// Get initial image dimensions
|
||||
const initialBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
|
||||
|
||||
// Get and assert zoomed out image dimensions
|
||||
const zoomedOutBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
expect(zoomedOutBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height);
|
||||
expect(zoomedOutBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width);
|
||||
// Zoom in twice via button
|
||||
await zoomIntoImageryByButton(page);
|
||||
await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty(
|
||||
'style.transform',
|
||||
'scale(2) translate(0px, 0px)'
|
||||
);
|
||||
await zoomIntoImageryByButton(page);
|
||||
await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty(
|
||||
'style.transform',
|
||||
'scale(3) translate(0px, 0px)'
|
||||
);
|
||||
|
||||
// Zoom out again via button, assert against the initial image dimensions
|
||||
await zoomOutOfImageryByButton(page);
|
||||
const finalBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
expect(finalBoundingBox).toEqual(initialBoundingBox);
|
||||
// Get and assert zoomed in image dimensions
|
||||
const zoomedInBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
|
||||
expect(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
|
||||
expect(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);
|
||||
|
||||
// Zoom out once via button
|
||||
await zoomOutOfImageryByButton(page);
|
||||
await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty(
|
||||
'style.transform',
|
||||
'scale(2) translate(0px, 0px)'
|
||||
);
|
||||
|
||||
// Get and assert zoomed out image dimensions
|
||||
const zoomedOutBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
|
||||
expect(zoomedOutBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height);
|
||||
expect(zoomedOutBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width);
|
||||
|
||||
// Zoom out again via button, assert against the initial image dimensions
|
||||
await zoomOutOfImageryByButton(page);
|
||||
await expect(page.getByLabel('Focused Image Element')).toHaveJSProperty(
|
||||
'style.transform',
|
||||
'scale(1) translate(0px, 0px)'
|
||||
);
|
||||
|
||||
const finalBoundingBox = await page.getByLabel('Focused Image Element').boundingBox();
|
||||
expect(finalBoundingBox).toEqual(initialBoundingBox);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -957,16 +957,11 @@ async function assertBackgroundImageContrast(page, expected) {
|
||||
*/
|
||||
async function zoomIntoImageryByButton(page) {
|
||||
// FIXME: There should only be one set of imagery buttons, but there are two?
|
||||
const zoomInBtn = page
|
||||
.locator("[role='toolbar'][aria-label='Image controls'] .t-btn-zoom-in")
|
||||
.nth(0);
|
||||
const backgroundImage = page.locator(backgroundImageSelector);
|
||||
if (!(await zoomInBtn.isVisible())) {
|
||||
await backgroundImage.hover({ trial: true });
|
||||
}
|
||||
|
||||
const zoomInBtn = page.getByRole('button', { name: 'Zoom in' });
|
||||
const backgroundImage = page.getByLabel('Focused Image Element');
|
||||
await backgroundImage.hover({ trial: true });
|
||||
await zoomInBtn.click();
|
||||
await waitForAnimations(backgroundImage);
|
||||
await waitForZoomAndPanTransitions(page);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -975,17 +970,11 @@ async function zoomIntoImageryByButton(page) {
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function zoomOutOfImageryByButton(page) {
|
||||
// FIXME: There should only be one set of imagery buttons, but there are two?
|
||||
const zoomOutBtn = page
|
||||
.locator("[role='toolbar'][aria-label='Image controls'] .t-btn-zoom-out")
|
||||
.nth(0);
|
||||
const backgroundImage = page.locator(backgroundImageSelector);
|
||||
if (!(await zoomOutBtn.isVisible())) {
|
||||
await backgroundImage.hover({ trial: true });
|
||||
}
|
||||
|
||||
const zoomOutBtn = page.getByRole('button', { name: 'Zoom out' });
|
||||
const backgroundImage = page.getByLabel('Focused Image Element');
|
||||
await backgroundImage.hover({ trial: true });
|
||||
await zoomOutBtn.click();
|
||||
await waitForAnimations(backgroundImage);
|
||||
await waitForZoomAndPanTransitions(page);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -994,38 +983,24 @@ async function zoomOutOfImageryByButton(page) {
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function resetImageryPanAndZoom(page) {
|
||||
// FIXME: There should only be one set of imagery buttons, but there are two?
|
||||
const panZoomResetBtn = page
|
||||
.locator("[role='toolbar'][aria-label='Image controls'] .t-btn-zoom-reset")
|
||||
.nth(0);
|
||||
const backgroundImage = page.locator(backgroundImageSelector);
|
||||
if (!(await panZoomResetBtn.isVisible())) {
|
||||
await backgroundImage.hover({ trial: true });
|
||||
}
|
||||
|
||||
const panZoomResetBtn = page.getByRole('button', { name: 'Remove zoom and pan' });
|
||||
await expect(panZoomResetBtn).toBeVisible();
|
||||
await panZoomResetBtn.hover({ trial: true });
|
||||
await panZoomResetBtn.click();
|
||||
await waitForAnimations(backgroundImage);
|
||||
|
||||
await waitForZoomAndPanTransitions(page);
|
||||
await expect(page.getByText('Alt drag to pan')).toBeHidden();
|
||||
await expect(page.locator('.c-thumb__viewable-area')).toBeHidden();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function createImageryView(page) {
|
||||
// Click the Create button
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
|
||||
// Click text=Example Imagery
|
||||
await page.click('li[role="menuitem"]:has-text("Example Imagery")');
|
||||
|
||||
// Clear and set Image load delay to minimum value
|
||||
await page.locator('input[type="number"]').fill('');
|
||||
await page.locator('input[type="number"]').fill('5000');
|
||||
|
||||
// Click text=OK
|
||||
await Promise.all([
|
||||
page.waitForNavigation({ waitUntil: 'networkidle' }),
|
||||
page.click('button:has-text("OK")'),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
async function waitForZoomAndPanTransitions(page) {
|
||||
// Wait for image to stabilize
|
||||
await page.getByLabel('Focused Image Element').hover({ trial: true });
|
||||
// Wait for zoom to end
|
||||
await expect(page.getByLabel('Focused Image Element')).not.toHaveClass(/is-zooming|is-panning/);
|
||||
// Wait for image to stabilize
|
||||
await page.getByLabel('Focused Image Element').hover({ trial: true });
|
||||
}
|
||||
|
@ -0,0 +1,489 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
This test suite is dedicated to testing how imagery functions over time.
|
||||
It only assumes that example imagery is present.
|
||||
It uses https://playwright.dev/docs/clock to have control over time
|
||||
*/
|
||||
|
||||
import {
|
||||
createDomainObjectWithDefaults,
|
||||
navigateToObjectWithRealTime,
|
||||
setRealTimeMode,
|
||||
setStartOffset
|
||||
} from '../../../../appActions.js';
|
||||
import { MISSION_TIME } from '../../../../constants.js';
|
||||
import {
|
||||
createImageryViewWithShortDelay,
|
||||
FIVE_MINUTES,
|
||||
IMAGE_LOAD_DELAY,
|
||||
THIRTY_SECONDS
|
||||
} from '../../../../helper/imageryUtils.js';
|
||||
import { expect, test } from '../../../../pluginFixtures.js';
|
||||
|
||||
test.describe('Example Imagery Object with Controlled Clock @clock', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// We mock the clock so that we don't need to wait for time driven events
|
||||
// to verify functionality.
|
||||
await page.clock.install({ time: MISSION_TIME });
|
||||
await page.clock.resume();
|
||||
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Create a default 'Example Imagery' object
|
||||
// Click the Create button
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
|
||||
// Click text=Example Imagery
|
||||
await page.getByRole('menuitem', { name: 'Example Imagery' }).click();
|
||||
|
||||
// Clear and set Image load delay to minimum value
|
||||
await page.locator('input[type="number"]').clear();
|
||||
await page.locator('input[type="number"]').fill(`${IMAGE_LOAD_DELAY}`);
|
||||
|
||||
await page.getByLabel('Save').click();
|
||||
|
||||
// Verify that the created object is focused
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText(
|
||||
'Unnamed Example Imagery'
|
||||
);
|
||||
await page.getByLabel('Focused Image Element').hover({ trial: true });
|
||||
|
||||
// set realtime mode
|
||||
await setRealTimeMode(page);
|
||||
await setStartOffset(page, { startMins: '05' });
|
||||
});
|
||||
|
||||
test('Imagery Time Bounding', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/5265'
|
||||
});
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/7825'
|
||||
});
|
||||
|
||||
// verify that old images are discarded
|
||||
const lastImageInBounds = page.getByLabel('Image thumbnail from').first();
|
||||
const lastImageTimestamp = await lastImageInBounds.getAttribute('title');
|
||||
expect(lastImageTimestamp).not.toBeNull();
|
||||
|
||||
// go forward in time to ensure old images are discarded
|
||||
await page.clock.fastForward(IMAGE_LOAD_DELAY);
|
||||
await page.clock.resume();
|
||||
await expect(page.getByLabel(lastImageTimestamp)).toBeHidden();
|
||||
|
||||
// go way forward in time to ensure multiple images are discarded
|
||||
const IMAGES_TO_DISCARD_COUNT = 5;
|
||||
|
||||
const lastImageToDiscard = page
|
||||
.getByLabel('Image thumbnail from')
|
||||
.nth(IMAGES_TO_DISCARD_COUNT - 1);
|
||||
const lastImageToDiscardTimestamp = await lastImageToDiscard.getAttribute('title');
|
||||
expect(lastImageToDiscardTimestamp).not.toBeNull();
|
||||
|
||||
const imageAfterLastImageToDiscard = page
|
||||
.getByLabel('Image thumbnail from')
|
||||
.nth(IMAGES_TO_DISCARD_COUNT);
|
||||
const imageAfterLastImageToDiscardTimestamp =
|
||||
await imageAfterLastImageToDiscard.getAttribute('title');
|
||||
expect(imageAfterLastImageToDiscardTimestamp).not.toBeNull();
|
||||
|
||||
await page.clock.fastForward(IMAGE_LOAD_DELAY * IMAGES_TO_DISCARD_COUNT);
|
||||
await page.clock.resume();
|
||||
|
||||
await expect(page.getByLabel(lastImageToDiscardTimestamp)).toBeHidden();
|
||||
await expect(page.getByLabel(imageAfterLastImageToDiscardTimestamp)).toBeVisible();
|
||||
});
|
||||
|
||||
test('Get background-image url from background-image css prop', async ({ page }) => {
|
||||
await assertBackgroundImageUrlFromBackgroundCss(page);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Example Imagery in Display Layout with Controlled Clock @clock', () => {
|
||||
let displayLayout;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// We mock the clock so that we don't need to wait for time driven events
|
||||
// to verify functionality.
|
||||
await page.clock.install({ time: MISSION_TIME });
|
||||
await page.clock.resume();
|
||||
|
||||
// Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
displayLayout = await createDomainObjectWithDefaults(page, { type: 'Display Layout' });
|
||||
|
||||
// Create Example Imagery inside Display Layout
|
||||
await createImageryViewWithShortDelay(page, {
|
||||
name: 'Unnamed Example Imagery',
|
||||
parent: displayLayout.uuid
|
||||
});
|
||||
|
||||
// set realtime mode
|
||||
await navigateToObjectWithRealTime(
|
||||
page,
|
||||
displayLayout.url,
|
||||
`${FIVE_MINUTES}`,
|
||||
`${THIRTY_SECONDS}`
|
||||
);
|
||||
});
|
||||
|
||||
test('Imagery Time Bounding', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/5265'
|
||||
});
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/7825'
|
||||
});
|
||||
|
||||
// Edit mode
|
||||
await page.getByLabel('Edit Object').click();
|
||||
|
||||
// Click on example imagery to expose toolbar
|
||||
await page.locator('.c-so-view__header').click();
|
||||
|
||||
// Adjust object height
|
||||
await page.locator('div[title="Resize object height"] > input').click();
|
||||
await page.locator('div[title="Resize object height"] > input').fill('50');
|
||||
|
||||
// Adjust object width
|
||||
await page.locator('div[title="Resize object width"] > input').click();
|
||||
await page.locator('div[title="Resize object width"] > input').fill('50');
|
||||
|
||||
await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport();
|
||||
|
||||
// verify that old images are discarded
|
||||
const lastImageInBounds = page.getByLabel('Image thumbnail from').first();
|
||||
const lastImageTimestamp = await lastImageInBounds.getAttribute('title');
|
||||
expect(lastImageTimestamp).not.toBeNull();
|
||||
|
||||
// go forward in time to ensure old images are discarded
|
||||
await page.clock.fastForward(IMAGE_LOAD_DELAY);
|
||||
await page.clock.resume();
|
||||
await expect(page.getByLabel(lastImageTimestamp)).toBeHidden();
|
||||
|
||||
// go way forward in time to ensure multiple images are discarded
|
||||
const IMAGES_TO_DISCARD_COUNT = 5;
|
||||
|
||||
const lastImageToDiscard = page
|
||||
.getByLabel('Image thumbnail from')
|
||||
.nth(IMAGES_TO_DISCARD_COUNT - 1);
|
||||
const lastImageToDiscardTimestamp = await lastImageToDiscard.getAttribute('title');
|
||||
expect(lastImageToDiscardTimestamp).not.toBeNull();
|
||||
|
||||
const imageAfterLastImageToDiscard = page
|
||||
.getByLabel('Image thumbnail from')
|
||||
.nth(IMAGES_TO_DISCARD_COUNT);
|
||||
const imageAfterLastImageToDiscardTimestamp =
|
||||
await imageAfterLastImageToDiscard.getAttribute('title');
|
||||
expect(imageAfterLastImageToDiscardTimestamp).not.toBeNull();
|
||||
|
||||
await page.clock.fastForward(IMAGE_LOAD_DELAY * IMAGES_TO_DISCARD_COUNT);
|
||||
await page.clock.resume();
|
||||
|
||||
await expect(page.getByLabel(lastImageToDiscardTimestamp)).toBeHidden();
|
||||
await expect(page.getByLabel(imageAfterLastImageToDiscardTimestamp)).toBeVisible();
|
||||
});
|
||||
|
||||
test('Get background-image url from background-image css prop @clock', async ({ page }) => {
|
||||
await assertBackgroundImageUrlFromBackgroundCss(page);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Example Imagery in Flexible layout with Controlled Clock @clock', () => {
|
||||
let flexibleLayout;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// We mock the clock so that we don't need to wait for time driven events
|
||||
// to verify functionality.
|
||||
await page.clock.install({ time: MISSION_TIME });
|
||||
await page.clock.resume();
|
||||
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
flexibleLayout = await createDomainObjectWithDefaults(page, { type: 'Flexible Layout' });
|
||||
|
||||
// Create Example Imagery inside the Flexible Layout
|
||||
await createImageryViewWithShortDelay(page, {
|
||||
name: 'Unnamed Example Imagery',
|
||||
parent: flexibleLayout.uuid
|
||||
});
|
||||
|
||||
// set realtime mode
|
||||
await navigateToObjectWithRealTime(
|
||||
page,
|
||||
flexibleLayout.url,
|
||||
`${FIVE_MINUTES}`,
|
||||
`${THIRTY_SECONDS}`
|
||||
);
|
||||
|
||||
// Wait for image thumbnail auto-scroll to complete
|
||||
await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport();
|
||||
});
|
||||
|
||||
test('Imagery Time Bounding @clock', async ({ page, browserName }) => {
|
||||
test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox');
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/5326'
|
||||
});
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/7825'
|
||||
});
|
||||
|
||||
// verify that old images are discarded
|
||||
const lastImageInBounds = page.getByLabel('Image thumbnail from').first();
|
||||
const lastImageTimestamp = await lastImageInBounds.getAttribute('title');
|
||||
expect(lastImageTimestamp).not.toBeNull();
|
||||
|
||||
// go forward in time to ensure old images are discarded
|
||||
await page.clock.fastForward(IMAGE_LOAD_DELAY);
|
||||
await page.clock.resume();
|
||||
await expect(page.getByLabel(lastImageTimestamp)).toBeHidden();
|
||||
|
||||
// go way forward in time to ensure multiple images are discarded
|
||||
const IMAGES_TO_DISCARD_COUNT = 5;
|
||||
|
||||
const lastImageToDiscard = page
|
||||
.getByLabel('Image thumbnail from')
|
||||
.nth(IMAGES_TO_DISCARD_COUNT - 1);
|
||||
const lastImageToDiscardTimestamp = await lastImageToDiscard.getAttribute('title');
|
||||
expect(lastImageToDiscardTimestamp).not.toBeNull();
|
||||
|
||||
const imageAfterLastImageToDiscard = page
|
||||
.getByLabel('Image thumbnail from')
|
||||
.nth(IMAGES_TO_DISCARD_COUNT);
|
||||
const imageAfterLastImageToDiscardTimestamp =
|
||||
await imageAfterLastImageToDiscard.getAttribute('title');
|
||||
expect(imageAfterLastImageToDiscardTimestamp).not.toBeNull();
|
||||
|
||||
await page.clock.fastForward(IMAGE_LOAD_DELAY * IMAGES_TO_DISCARD_COUNT);
|
||||
await page.clock.resume();
|
||||
|
||||
await expect(page.getByLabel(lastImageToDiscardTimestamp)).toBeHidden();
|
||||
await expect(page.getByLabel(imageAfterLastImageToDiscardTimestamp)).toBeVisible();
|
||||
});
|
||||
|
||||
test('Get background-image url from background-image css prop @clock', async ({ page }) => {
|
||||
await assertBackgroundImageUrlFromBackgroundCss(page);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Example Imagery in Tabs View with Controlled Clock @clock', () => {
|
||||
let timeStripObject;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// We mock the clock so that we don't need to wait for time driven events
|
||||
// to verify functionality.
|
||||
await page.clock.install({ time: MISSION_TIME });
|
||||
await page.clock.resume();
|
||||
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
timeStripObject = await createDomainObjectWithDefaults(page, { type: 'Tabs View' });
|
||||
await page.goto(timeStripObject.url);
|
||||
|
||||
/* Create Example Imagery with minimum Image Load Delay */
|
||||
// Click the Create button
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
|
||||
// Click text=Example Imagery
|
||||
await page.getByRole('menuitem', { name: 'Example Imagery' }).click();
|
||||
|
||||
// Clear and set Image load delay to minimum value
|
||||
await page.locator('input[type="number"]').clear();
|
||||
await page.locator('input[type="number"]').fill(`${IMAGE_LOAD_DELAY}`);
|
||||
|
||||
await page.getByLabel('Save').click();
|
||||
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText(
|
||||
'Unnamed Example Imagery'
|
||||
);
|
||||
|
||||
// set realtime mode
|
||||
await navigateToObjectWithRealTime(
|
||||
page,
|
||||
timeStripObject.url,
|
||||
`${FIVE_MINUTES}`,
|
||||
`${THIRTY_SECONDS}`
|
||||
);
|
||||
|
||||
// Wait for image thumbnail auto-scroll to complete
|
||||
await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport();
|
||||
});
|
||||
|
||||
test('Imagery Time Bounding @clock', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/5265'
|
||||
});
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/7825'
|
||||
});
|
||||
|
||||
// verify that old images are discarded
|
||||
const lastImageInBounds = page.getByLabel('Image thumbnail from').first();
|
||||
const lastImageTimestamp = await lastImageInBounds.getAttribute('title');
|
||||
expect(lastImageTimestamp).not.toBeNull();
|
||||
|
||||
// go forward in time to ensure old images are discarded
|
||||
await page.clock.fastForward(IMAGE_LOAD_DELAY);
|
||||
await page.clock.resume();
|
||||
await expect(page.getByLabel(lastImageTimestamp)).toBeHidden();
|
||||
|
||||
// go way forward in time to ensure multiple images are discarded
|
||||
const IMAGES_TO_DISCARD_COUNT = 5;
|
||||
|
||||
const lastImageToDiscard = page
|
||||
.getByLabel('Image thumbnail from')
|
||||
.nth(IMAGES_TO_DISCARD_COUNT - 1);
|
||||
const lastImageToDiscardTimestamp = await lastImageToDiscard.getAttribute('title');
|
||||
expect(lastImageToDiscardTimestamp).not.toBeNull();
|
||||
|
||||
const imageAfterLastImageToDiscard = page
|
||||
.getByLabel('Image thumbnail from')
|
||||
.nth(IMAGES_TO_DISCARD_COUNT);
|
||||
const imageAfterLastImageToDiscardTimestamp =
|
||||
await imageAfterLastImageToDiscard.getAttribute('title');
|
||||
expect(imageAfterLastImageToDiscardTimestamp).not.toBeNull();
|
||||
|
||||
await page.clock.fastForward(IMAGE_LOAD_DELAY * IMAGES_TO_DISCARD_COUNT);
|
||||
await page.clock.resume();
|
||||
|
||||
await expect(page.getByLabel(lastImageToDiscardTimestamp)).toBeHidden();
|
||||
await expect(page.getByLabel(imageAfterLastImageToDiscardTimestamp)).toBeVisible();
|
||||
});
|
||||
|
||||
test('Get background-image url from background-image css prop @clock', async ({ page }) => {
|
||||
await assertBackgroundImageUrlFromBackgroundCss(page);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Example Imagery in Time Strip with Controlled Clock @clock', () => {
|
||||
let timeStripObject;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// We mock the clock so that we don't need to wait for time driven events
|
||||
// to verify functionality.
|
||||
await page.clock.install({ time: MISSION_TIME });
|
||||
await page.clock.resume();
|
||||
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
timeStripObject = await createDomainObjectWithDefaults(page, { type: 'Time Strip' });
|
||||
await page.goto(timeStripObject.url);
|
||||
|
||||
/* Create Example Imagery with minimum Image Load Delay */
|
||||
// Click the Create button
|
||||
await page.getByRole('button', { name: 'Create' }).click();
|
||||
|
||||
// Click text=Example Imagery
|
||||
await page.getByRole('menuitem', { name: 'Example Imagery' }).click();
|
||||
|
||||
// Clear and set Image load delay to minimum value
|
||||
await page.locator('input[type="number"]').clear();
|
||||
await page.locator('input[type="number"]').fill(`${IMAGE_LOAD_DELAY}`);
|
||||
|
||||
await page.getByLabel('Save').click();
|
||||
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText(
|
||||
'Unnamed Example Imagery'
|
||||
);
|
||||
|
||||
// set realtime mode
|
||||
await navigateToObjectWithRealTime(
|
||||
page,
|
||||
timeStripObject.url,
|
||||
`${FIVE_MINUTES}`,
|
||||
`${THIRTY_SECONDS}`
|
||||
);
|
||||
|
||||
// Wait for image thumbnail auto-scroll to complete
|
||||
await expect(page.getByLabel('wrapper-').last()).toBeInViewport();
|
||||
});
|
||||
|
||||
test('Imagery Time Bounding @clock', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/5265'
|
||||
});
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/7825'
|
||||
});
|
||||
|
||||
// verify that old images are discarded
|
||||
const lastImageInBounds = page.getByLabel('wrapper-').first();
|
||||
const lastImageTimestamp = await lastImageInBounds.getAttribute('aria-label');
|
||||
expect(lastImageTimestamp).not.toBeNull();
|
||||
|
||||
// go forward in time to ensure old images are discarded
|
||||
await page.clock.fastForward(IMAGE_LOAD_DELAY);
|
||||
await page.clock.resume();
|
||||
await expect(page.getByLabel(lastImageTimestamp)).toBeHidden();
|
||||
|
||||
// go way forward in time to ensure multiple images are discarded
|
||||
const IMAGES_TO_DISCARD_COUNT = 5;
|
||||
|
||||
const lastImageToDiscard = page.getByLabel('wrapper-').nth(IMAGES_TO_DISCARD_COUNT - 1);
|
||||
const lastImageToDiscardTimestamp = await lastImageToDiscard.getAttribute('aria-label');
|
||||
expect(lastImageToDiscardTimestamp).not.toBeNull();
|
||||
|
||||
const imageAfterLastImageToDiscard = page.getByLabel('wrapper-').nth(IMAGES_TO_DISCARD_COUNT);
|
||||
const imageAfterLastImageToDiscardTimestamp =
|
||||
await imageAfterLastImageToDiscard.getAttribute('aria-label');
|
||||
expect(imageAfterLastImageToDiscardTimestamp).not.toBeNull();
|
||||
|
||||
await page.clock.fastForward(IMAGE_LOAD_DELAY * IMAGES_TO_DISCARD_COUNT);
|
||||
await page.clock.resume();
|
||||
|
||||
await expect(page.getByLabel(lastImageToDiscardTimestamp)).toBeHidden();
|
||||
await expect(page.getByLabel(imageAfterLastImageToDiscardTimestamp)).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function assertBackgroundImageUrlFromBackgroundCss(page) {
|
||||
const backgroundImage = page.getByLabel('Focused Image Element');
|
||||
const backgroundImageUrl = await backgroundImage.evaluate((el) => {
|
||||
return window
|
||||
.getComputedStyle(el)
|
||||
.getPropertyValue('background-image')
|
||||
.match(/url\(([^)]+)\)/)[1];
|
||||
});
|
||||
|
||||
// go forward in time to ensure old images are discarded
|
||||
await page.clock.fastForward(IMAGE_LOAD_DELAY);
|
||||
await page.clock.resume();
|
||||
await expect(backgroundImage).not.toHaveJSProperty('background-image', backgroundImageUrl);
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
* This test suite verifies modifying the image location of the example imagery object.
|
||||
*/
|
||||
|
||||
import { createDomainObjectWithDefaults } from '../../../../appActions.js';
|
||||
import { expect, test } from '../../../../pluginFixtures.js';
|
||||
|
||||
test.describe('Example Imagery Object Custom Images', () => {
|
||||
let exampleImagery;
|
||||
test.beforeEach(async ({ page }) => {
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Create a default 'Example Imagery' object
|
||||
exampleImagery = await createDomainObjectWithDefaults(page, {
|
||||
name: 'Example Imagery',
|
||||
type: 'Example Imagery'
|
||||
});
|
||||
|
||||
// Verify that the created object is focused
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText(exampleImagery.name);
|
||||
await page.getByLabel('Focused Image Element').hover({ trial: true });
|
||||
|
||||
// Wait for image thumbnail auto-scroll to complete
|
||||
await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport();
|
||||
});
|
||||
// this requires CORS to be enabled in some fashion
|
||||
test.fixme('Can right click on image and save it as a file', async ({ page }) => {});
|
||||
test('Can provide a custom image location for the example imagery object', async ({ page }) => {
|
||||
// Modify Example Imagery to create a really stable image which will never let us down
|
||||
await page.getByRole('button', { name: 'More actions' }).click();
|
||||
await page.getByRole('menuitem', { name: 'Edit Properties...' }).click();
|
||||
await page
|
||||
.locator('#imageLocation-textarea')
|
||||
.fill(
|
||||
'https://raw.githubusercontent.com/nasa/openmct/554f77c42fec81cf0f63e62b278012cb08d82af9/e2e/test-data/rick.jpg,https://raw.githubusercontent.com/nasa/openmct/554f77c42fec81cf0f63e62b278012cb08d82af9/e2e/test-data/rick.jpg'
|
||||
);
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Wait for the thumbnails to finish their scroll animation
|
||||
// (Wait until the rightmost thumbnail is in view)
|
||||
await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport();
|
||||
|
||||
await expect(page.getByLabel('Image Wrapper')).toBeVisible();
|
||||
});
|
||||
test.fixme('Can provide a custom image with spaces in name', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/7903'
|
||||
});
|
||||
await page.goto(exampleImagery.url, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Modify Example Imagery to create a really stable image which will never let us down
|
||||
await page.getByRole('button', { name: 'More actions' }).click();
|
||||
await page.getByRole('menuitem', { name: 'Edit Properties...' }).click();
|
||||
await page
|
||||
.locator('#imageLocation-textarea')
|
||||
.fill(
|
||||
'https://raw.githubusercontent.com/nasa/openmct/d8c64f183400afb70137221fc1a035e091bea912/e2e/test-data/rick%20space%20roll.jpg'
|
||||
);
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Wait for the thumbnails to finish their scroll animation
|
||||
// (Wait until the rightmost thumbnail is in view)
|
||||
await expect(page.getByLabel('Image Thumbnail from').last()).toBeInViewport();
|
||||
|
||||
await expect(page.getByLabel('Image Wrapper')).toBeVisible();
|
||||
});
|
||||
});
|
@ -26,10 +26,7 @@ This test suite is dedicated to tests which verify the basic operations surround
|
||||
|
||||
import fs from 'fs/promises';
|
||||
|
||||
import {
|
||||
createDomainObjectWithDefaults,
|
||||
openObjectTreeContextMenu
|
||||
} from '../../../../appActions.js';
|
||||
import { createDomainObjectWithDefaults } from '../../../../appActions.js';
|
||||
import { expect, test } from '../../../../baseFixtures.js';
|
||||
import { navigateToFaultManagementWithExample } from '../../../../helper/faultUtils.js';
|
||||
|
||||
@ -51,7 +48,10 @@ test.describe('ExportAsJSON', () => {
|
||||
await page.goto(folder.url);
|
||||
|
||||
// Open context menu and initiate download
|
||||
await openObjectTreeContextMenu(page, folder.url);
|
||||
await page.getByLabel('Show selected item in tree').click();
|
||||
await page.getByRole('treeitem', { name: 'Expand e2e folder folder' }).click({
|
||||
button: 'right'
|
||||
});
|
||||
const [download] = await Promise.all([
|
||||
page.waitForEvent('download'), // Waits for the download event
|
||||
page.getByLabel('Export as JSON').click() // Triggers the download
|
||||
@ -105,7 +105,12 @@ test.describe('ExportAsJSON', () => {
|
||||
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
|
||||
await page.getByLabel('Show selected item in tree').click();
|
||||
await page.getByRole('treeitem', { name: 'Collapse e2e folder folder' }).click({
|
||||
button: 'right'
|
||||
});
|
||||
|
||||
// Open context menu and initiate download
|
||||
const [download] = await Promise.all([
|
||||
page.waitForEvent('download'), // Waits for the download event
|
||||
@ -141,18 +146,18 @@ test.describe('ExportAsJSON Disabled Actions', () => {
|
||||
});
|
||||
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 expect(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);
|
||||
await expect(page.getByLabel('Export as JSON')).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
test.describe('ExportAsJSON ProgressBar @couchdb', () => {
|
||||
let folder;
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
// Perform actions to create the domain object
|
||||
folder = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder'
|
||||
|
@ -36,7 +36,7 @@ test.describe('Testing numeric data with inspector data visualization (i.e., dat
|
||||
});
|
||||
|
||||
test('Can click on telemetry and see data in inspector @2p', async ({ page, context }) => {
|
||||
const initStartBounds = await page.getByLabel('Start bounds').textContent();
|
||||
const initStartBounds = page.getByLabel('Start bounds');
|
||||
const initEndBounds = await page.getByLabel('End bounds').textContent();
|
||||
const exampleDataVisualizationSource = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Example Data Visualization Source'
|
||||
@ -54,8 +54,8 @@ test.describe('Testing numeric data with inspector data visualization (i.e., dat
|
||||
|
||||
await page.goto(exampleDataVisualizationSource.url);
|
||||
|
||||
await page.getByRole('tab', { name: 'Data Visualization' }).click();
|
||||
await page.getByRole('cell', { name: /First Sine Wave Generator/ }).click();
|
||||
await page.getByRole('tab', { name: 'Data Visualization' }).click();
|
||||
await expect(page.getByText('Numeric Data')).toBeVisible();
|
||||
await expect(
|
||||
page.locator('span.plot-series-name', { hasText: 'First Sine Wave Generator Hz' })
|
||||
@ -63,6 +63,7 @@ test.describe('Testing numeric data with inspector data visualization (i.e., dat
|
||||
await expect(page.locator('.js-series-data-loaded')).toBeVisible();
|
||||
|
||||
await page.getByRole('cell', { name: /Second Sine Wave Generator/ }).click();
|
||||
await page.getByRole('tab', { name: 'Data Visualization' }).click();
|
||||
await expect(page.getByText('Numeric Data')).toBeVisible();
|
||||
await expect(
|
||||
page.locator('span.plot-series-name', { hasText: 'Second Sine Wave Generator Hz' })
|
||||
@ -77,11 +78,15 @@ test.describe('Testing numeric data with inspector data visualization (i.e., dat
|
||||
// ensure our new tab's title is correct
|
||||
const newPage = await pagePromise;
|
||||
await newPage.waitForLoadState();
|
||||
await page.getByRole('tab', { name: 'Data Visualization' }).click();
|
||||
|
||||
// expect new tab title to contain 'Second Sine Wave Generator'
|
||||
await expect(newPage).toHaveTitle('Second Sine Wave Generator');
|
||||
|
||||
// Verify that "Open in New Tab" preserves the time bounds
|
||||
expect(initStartBounds).toEqual(await newPage.getByLabel('Start bounds').textContent());
|
||||
await expect(initStartBounds).toHaveText(
|
||||
await newPage.getByLabel('Start bounds').textContent()
|
||||
);
|
||||
expect(initEndBounds).toEqual(await newPage.getByLabel('End bounds').textContent());
|
||||
});
|
||||
});
|
||||
|
@ -22,7 +22,7 @@
|
||||
|
||||
import {
|
||||
createDomainObjectWithDefaults,
|
||||
openObjectTreeContextMenu,
|
||||
navigateToObjectWithRealTime,
|
||||
setFixedTimeMode,
|
||||
setRealTimeMode,
|
||||
setStartOffset
|
||||
@ -53,122 +53,119 @@ test.describe('Testing LAD table configuration', () => {
|
||||
test('in edit mode, LAD Tables provide ability to hide columns', async ({ page }) => {
|
||||
// Edit LAD table
|
||||
await page.getByLabel('Edit Object').click();
|
||||
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: 'Units' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'Type', exact: true })).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();
|
||||
await expect(page.getByRole('cell', { name: 'CRITICAL' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'SEVERE' })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Timestamp', exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Units' })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Type', exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit WATCH' })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit WARNING' })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit DISTRESS' })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit CRITICAL' })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit SEVERE' })).toBeVisible();
|
||||
|
||||
// hide timestamp column
|
||||
await page.getByLabel('Timestamp', { exact: true }).uncheck();
|
||||
await expect(page.getByRole('cell', { name: 'Timestamp', exact: true })).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: 'WATCH' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'WARNING' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'DISTRESS' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'CRITICAL' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'SEVERE' })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Timestamp', exact: true })).toBeHidden();
|
||||
await expect(page.getByRole('columnheader', { name: 'Units' })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Type', exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit WATCH' })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit WARNING' })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit DISTRESS' })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit CRITICAL' })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit SEVERE' })).toBeVisible();
|
||||
|
||||
// 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 expect(page.getByRole('cell', { name: 'Units' })).toBeHidden();
|
||||
await expect(page.getByRole('cell', { name: 'Type', exact: true })).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();
|
||||
await expect(page.getByRole('cell', { name: 'CRITICAL' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'SEVERE' })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Timestamp', exact: true })).toBeHidden();
|
||||
await expect(page.getByRole('columnheader', { name: 'Units' })).toBeHidden();
|
||||
await expect(page.getByRole('columnheader', { name: 'Type', exact: true })).toBeHidden();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit WATCH' })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit WARNING' })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit DISTRESS' })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit CRITICAL' })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit SEVERE' })).toBeVisible();
|
||||
|
||||
// hide WATCH column
|
||||
await page.getByLabel('WATCH').uncheck();
|
||||
await expect(page.getByRole('cell', { name: 'Timestamp', exact: true })).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: 'WATCH' })).toBeHidden();
|
||||
await expect(page.getByRole('cell', { name: 'WARNING' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'DISTRESS' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'CRITICAL' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'SEVERE' })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Timestamp', exact: true })).toBeHidden();
|
||||
await expect(page.getByRole('columnheader', { name: 'Units' })).toBeHidden();
|
||||
await expect(page.getByRole('columnheader', { name: 'Type', exact: true })).toBeHidden();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit WATCH' })).toBeHidden();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit WARNING' })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit DISTRESS' })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit CRITICAL' })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit SEVERE' })).toBeVisible();
|
||||
|
||||
// save and reload and verify they columns are still hidden
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.getByRole('button', { name: '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: 'Units' })).toBeHidden();
|
||||
await expect(page.getByRole('cell', { name: 'Type', exact: true })).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();
|
||||
await expect(page.getByRole('cell', { name: 'CRITICAL' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'SEVERE' })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Timestamp', exact: true })).toBeHidden();
|
||||
await expect(page.getByRole('columnheader', { name: 'Units' })).toBeHidden();
|
||||
await expect(page.getByRole('columnheader', { name: 'Type', exact: true })).toBeHidden();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit WATCH' })).toBeHidden();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit WARNING' })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit DISTRESS' })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit CRITICAL' })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit SEVERE' })).toBeVisible();
|
||||
|
||||
// Edit LAD table
|
||||
await page.getByLabel('Edit Object').click();
|
||||
await page.getByRole('tab', { name: 'LAD Table Configuration' }).click();
|
||||
|
||||
// show timestamp column
|
||||
await page.getByLabel('Timestamp', { exact: true }).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: 'WATCH' })).toBeHidden();
|
||||
await expect(page.getByRole('cell', { name: 'WARNING' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'DISTRESS' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'CRITICAL' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'SEVERE' })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Units' })).toBeHidden();
|
||||
await expect(page.getByRole('columnheader', { name: 'Type', exact: true })).toBeHidden();
|
||||
await expect(page.getByRole('columnheader', { name: 'Timestamp', exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit WATCH' })).toBeHidden();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit WARNING' })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit DISTRESS' })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit CRITICAL' })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit SEVERE' })).toBeVisible();
|
||||
|
||||
// save and reload and make sure timestamp is still visible
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
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: 'WATCH' })).toBeHidden();
|
||||
await expect(page.getByRole('cell', { name: 'WARNING' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'DISTRESS' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'CRITICAL' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'SEVERE' })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Units' })).toBeHidden();
|
||||
await expect(page.getByRole('columnheader', { name: 'Type', exact: true })).toBeHidden();
|
||||
await expect(page.getByRole('columnheader', { name: 'Timestamp', exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit WATCH' })).toBeHidden();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit WARNING' })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit DISTRESS' })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit CRITICAL' })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit SEVERE' })).toBeVisible();
|
||||
|
||||
// Edit LAD table
|
||||
await page.getByLabel('Edit Object').click();
|
||||
await page.getByRole('tab', { name: 'LAD Table Configuration' }).click();
|
||||
|
||||
// show units, type, and WATCH columns
|
||||
await page.getByLabel('Units').check();
|
||||
await page.getByLabel('Type', { exact: true }).check();
|
||||
await page.getByLabel('WATCH').check();
|
||||
await expect(page.getByRole('cell', { name: 'Timestamp', exact: true })).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: 'WATCH' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'WARNING' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'DISTRESS' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'CRITICAL' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'SEVERE' })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Timestamp', exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Units' })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Type', exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit WATCH' })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit WARNING' })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit DISTRESS' })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit CRITICAL' })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit SEVERE' })).toBeVisible();
|
||||
|
||||
// save and reload and make sure all columns are still visible
|
||||
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: 'Units' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'Type', exact: true })).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();
|
||||
await expect(page.getByRole('cell', { name: 'CRITICAL' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'SEVERE' })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Timestamp', exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Units' })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Type', exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit WATCH' })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit WARNING' })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit DISTRESS' })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit CRITICAL' })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit SEVERE' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('When adding something without Units, do not show Units column', async ({ page }) => {
|
||||
@ -182,17 +179,16 @@ test.describe('Testing LAD table configuration', () => {
|
||||
|
||||
// Edit LAD table
|
||||
await page.getByLabel('Edit Object').click();
|
||||
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: 'Units' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'Type', exact: true })).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();
|
||||
await expect(page.getByRole('cell', { name: 'CRITICAL' })).toBeVisible();
|
||||
await expect(page.getByRole('cell', { name: 'SEVERE' })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Timestamp', exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Units' })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Type', exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit WATCH' })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit WARNING' })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit DISTRESS' })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit CRITICAL' })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit SEVERE' })).toBeVisible();
|
||||
|
||||
// save and reload and verify they columns are still hidden
|
||||
await page.getByLabel('Save').click();
|
||||
@ -201,25 +197,25 @@ test.describe('Testing LAD table configuration', () => {
|
||||
// Remove Sine Wave Generator
|
||||
openObjectTreeContextMenu(page, sineWaveObject.url);
|
||||
await page.getByRole('menuitem', { name: /Remove/ }).click();
|
||||
await page.getByRole('button', { name: 'OK', exact: true }).click();
|
||||
await page.getByRole('button', { name: 'Ok', exact: true }).click();
|
||||
|
||||
// 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: 'Units' })).toBeHidden();
|
||||
await expect(page.getByRole('cell', { name: 'WATCH' })).toBeHidden();
|
||||
await expect(page.getByRole('cell', { name: 'WARNING' })).toBeHidden();
|
||||
await expect(page.getByRole('cell', { name: 'DISTRESS' })).toBeHidden();
|
||||
await expect(page.getByRole('cell', { name: 'CRITICAL' })).toBeHidden();
|
||||
await expect(page.getByRole('cell', { name: 'SEVERE' })).toBeHidden();
|
||||
await expect(page.getByRole('columnheader', { name: 'Timestamp', exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Type', exact: true })).toBeVisible();
|
||||
await expect(page.getByRole('columnheader', { name: 'Units' })).toBeHidden();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit WATCH' })).toBeHidden();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit WARNING' })).toBeHidden();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit DISTRESS' })).toBeHidden();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit CRITICAL' })).toBeHidden();
|
||||
await expect(page.getByRole('columnheader', { name: 'Limit SEVERE' })).toBeHidden();
|
||||
});
|
||||
|
||||
test("LAD Tables don't allow selection of rows but does show context click menus", async ({
|
||||
page
|
||||
}) => {
|
||||
const cell = await page.locator('.js-first-data');
|
||||
const cell = page.locator('.js-first-data');
|
||||
const userSelectable = await cell.evaluate((el) => {
|
||||
return window.getComputedStyle(el).getPropertyValue('user-select');
|
||||
});
|
||||
@ -237,19 +233,21 @@ test.describe('Testing LAD table configuration', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Testing LAD table @unstable', () => {
|
||||
test.describe('Testing LAD table', () => {
|
||||
let sineWaveObject;
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
await setRealTimeMode(page);
|
||||
|
||||
// Create Sine Wave Generator
|
||||
sineWaveObject = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
name: 'Test Sine Wave Generator'
|
||||
});
|
||||
|
||||
// Switch to real time mode by navigating directly to the URL
|
||||
await navigateToObjectWithRealTime(page, sineWaveObject.url);
|
||||
});
|
||||
test('telemetry value exactly matches latest telemetry value received in real time', async ({
|
||||
test('telemetry value exactly matches latest telemetry value received in realtime mode', async ({
|
||||
page
|
||||
}) => {
|
||||
// Create LAD table
|
||||
@ -261,23 +259,23 @@ test.describe('Testing LAD table @unstable', () => {
|
||||
await page.getByLabel('Edit Object').click();
|
||||
|
||||
// Expand the 'My Items' folder in the left tree
|
||||
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();
|
||||
await page.getByLabel('Expand My Items').click();
|
||||
// Add the Sine Wave Generator to the LAD table and save changes
|
||||
await page.dragAndDrop('text=Test Sine Wave Generator', '.c-lad-table-wrapper');
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.getByLabel('Preview Test Sine Wave').dragTo(page.locator('#lad-table-drop-area'));
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
// Subscribe to the Sine Wave Generator data
|
||||
// On getting data, check if the value found in the LAD table is the most recent value
|
||||
// from the Sine Wave Generator
|
||||
const getTelemValuePromise = await subscribeToTelemetry(page, sineWaveObject.uuid);
|
||||
const getTelemValuePromise = subscribeToTelemetry(page, sineWaveObject.uuid);
|
||||
const subscribeTelemValue = await getTelemValuePromise;
|
||||
const ladTableValuePromise = await page.waitForSelector(`text="${subscribeTelemValue}"`);
|
||||
const ladTableValue = await ladTableValuePromise.textContent();
|
||||
await expect(page.getByLabel('lad value')).toHaveText(subscribeTelemValue);
|
||||
const ladTableValue = await page.getByText(subscribeTelemValue).textContent();
|
||||
|
||||
expect(ladTableValue).toBe(subscribeTelemValue);
|
||||
expect(ladTableValue).toEqual(subscribeTelemValue);
|
||||
});
|
||||
test('telemetry value exactly matches latest telemetry value received in fixed time', async ({
|
||||
test('telemetry value exactly matches latest telemetry value received in fixed time mode', async ({
|
||||
page
|
||||
}) => {
|
||||
// Create LAD table
|
||||
@ -289,25 +287,23 @@ test.describe('Testing LAD table @unstable', () => {
|
||||
await page.getByLabel('Edit Object').click();
|
||||
|
||||
// Expand the 'My Items' folder in the left tree
|
||||
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();
|
||||
await page.getByLabel('Expand My Items').click();
|
||||
// Add the Sine Wave Generator to the LAD table and save changes
|
||||
await page.dragAndDrop('text=Test Sine Wave Generator', '.c-lad-table-wrapper');
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.getByLabel('Preview Test Sine Wave').dragTo(page.locator('#lad-table-drop-area'));
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
// Subscribe to the Sine Wave Generator data
|
||||
const getTelemValuePromise = await subscribeToTelemetry(page, sineWaveObject.uuid);
|
||||
const getTelemValuePromise = subscribeToTelemetry(page, sineWaveObject.uuid);
|
||||
// Set an offset of 1 minute and then change the time mode to fixed to set a 1 minute historical window
|
||||
await setStartOffset(page, { mins: '1' });
|
||||
await setRealTimeMode(page);
|
||||
await setStartOffset(page, { startMins: '01' });
|
||||
await setFixedTimeMode(page);
|
||||
|
||||
// On getting data, check if the value found in the LAD table is the most recent value
|
||||
// from the Sine Wave Generator
|
||||
const subscribeTelemValue = await getTelemValuePromise;
|
||||
const ladTableValuePromise = await page.waitForSelector(`text="${subscribeTelemValue}"`);
|
||||
const ladTableValue = await ladTableValuePromise.textContent();
|
||||
|
||||
expect(ladTableValue).toBe(subscribeTelemValue);
|
||||
await expect(page.getByLabel('lad value')).toHaveText(subscribeTelemValue);
|
||||
});
|
||||
});
|
||||
|
||||
@ -338,3 +334,18 @@ async function subscribeToTelemetry(page, objectIdentifier) {
|
||||
|
||||
return getTelemValuePromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the given `domainObject`'s context menu from the object tree.
|
||||
* Expands the path to the object and scrolls to it if necessary.
|
||||
*
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} url the url to the object
|
||||
*/
|
||||
async function openObjectTreeContextMenu(page, url) {
|
||||
await page.goto(url);
|
||||
await page.getByLabel('Show selected item in tree').click();
|
||||
await page.locator('.is-navigated-object').click({
|
||||
button: 'right'
|
||||
});
|
||||
}
|
||||
|
@ -76,7 +76,7 @@ test.describe('LAD Table', () => {
|
||||
// Right-click the SWG treeitem context menu and click 'Remove' and confirm
|
||||
await page.getByRole('treeitem', { name: swg.name }).click({ button: 'right' });
|
||||
await page.getByRole('menuitem', { name: 'Remove' }).click();
|
||||
await page.getByRole('button', { name: 'OK', exact: true }).click();
|
||||
await page.getByRole('button', { name: 'Ok', exact: true }).click();
|
||||
|
||||
// Assert that the SWG is no longer in the tree and the table is empty
|
||||
await expect(page.getByRole('treeitem', { name: swg.name })).toBeHidden();
|
||||
|
@ -26,7 +26,11 @@ This test suite is dedicated to tests which verify the basic operations surround
|
||||
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
import { createDomainObjectWithDefaults } from '../../../../appActions.js';
|
||||
import {
|
||||
createDomainObjectWithDefaults,
|
||||
renameCurrentObjectFromBrowseBar
|
||||
} from '../../../../appActions.js';
|
||||
import { copy, paste, selectAll } from '../../../../helper/hotkeys/hotkeys.js';
|
||||
import * as nbUtils from '../../../../helper/notebookUtils.js';
|
||||
import { expect, streamToString, test } from '../../../../pluginFixtures.js';
|
||||
|
||||
@ -258,7 +262,7 @@ test.describe('Notebook export tests', () => {
|
||||
const exportedText = await streamToString(readStream);
|
||||
expect(exportedText).toContain('Foo bar entry');
|
||||
});
|
||||
test.fixme('can export multiple notebook entries as text ', async ({ page }) => {});
|
||||
test.fixme('can export multiple notebook entries as text', async ({ page }) => {});
|
||||
test.fixme('can export all notebook entry metdata', async ({ page }) => {});
|
||||
test.fixme('can export all notebook tags', async ({ page }) => {});
|
||||
test.fixme('can export all notebook snapshots', async ({ page }) => {});
|
||||
@ -295,7 +299,7 @@ test.describe('Notebook entry tests', () => {
|
||||
await expect(page.getByLabel('Notebook Entry Input')).toBeVisible();
|
||||
await expect(page.getByLabel('Notebook Entry', { exact: true })).toHaveClass(/is-selected/);
|
||||
});
|
||||
test('When an object is dropped into a notebook, a new entry is created and it should be focused @unstable', async ({
|
||||
test('When an object is dropped into a notebook, a new entry is created and it should be focused', async ({
|
||||
page
|
||||
}) => {
|
||||
// Create Overlay Plot
|
||||
@ -319,7 +323,7 @@ test.describe('Notebook entry tests', () => {
|
||||
await expect(embed).toHaveClass(/icon-plot-overlay/);
|
||||
expect(embedName).toBe(overlayPlot.name);
|
||||
});
|
||||
test('When an object is dropped into a notebooks existing entry, it should be focused @unstable', async ({
|
||||
test('When an object is dropped into a notebooks existing entry, it should be focused', async ({
|
||||
page
|
||||
}) => {
|
||||
// Create Overlay Plot
|
||||
@ -353,19 +357,19 @@ test.describe('Notebook entry tests', () => {
|
||||
await page.goto(notebookObject.url);
|
||||
|
||||
await nbUtils.enterTextEntry(page, 'First Entry');
|
||||
await page.hover('text="First Entry"');
|
||||
await page.click('button[title="Delete this entry"]');
|
||||
await page.getByRole('button', { name: 'Ok' }).filter({ hasText: 'Ok' }).click();
|
||||
await expect(page.locator('text="First Entry"')).toBeHidden();
|
||||
await page.getByLabel('Notebook Entry', { exact: true }).hover();
|
||||
await page.getByLabel('Delete this entry').click();
|
||||
await page.getByRole('button', { name: 'Ok', exact: true }).click();
|
||||
await expect(page.getByText('First Entry')).toBeHidden();
|
||||
await nbUtils.enterTextEntry(page, 'Another First Entry');
|
||||
await nbUtils.enterTextEntry(page, 'Second Entry');
|
||||
await nbUtils.enterTextEntry(page, 'Third Entry');
|
||||
await page.hover('[aria-label="Notebook Entry"] >> nth=2');
|
||||
await page.click('button[title="Delete this entry"] >> nth=2');
|
||||
await page.getByRole('button', { name: 'Ok' }).filter({ hasText: 'Ok' }).click();
|
||||
await expect(page.locator('text="Third Entry"')).toBeHidden();
|
||||
await expect(page.locator('text="Another First Entry"')).toBeVisible();
|
||||
await expect(page.locator('text="Second Entry"')).toBeVisible();
|
||||
await page.getByLabel('Notebook Entry', { exact: true }).nth(2).hover();
|
||||
await page.getByLabel('Delete this entry').nth(2).click();
|
||||
await page.getByRole('button', { name: 'Ok', exact: true }).click();
|
||||
await expect(page.getByText('Third Entry')).toBeHidden();
|
||||
await expect(page.getByText('Another First Entry')).toBeVisible();
|
||||
await expect(page.getByText('Second Entry')).toBeVisible();
|
||||
});
|
||||
test('when a valid link is entered into a notebook entry, it becomes clickable when viewing', async ({
|
||||
page
|
||||
@ -382,7 +386,7 @@ test.describe('Notebook entry tests', () => {
|
||||
|
||||
const validLink = page.locator(`a[href="${TEST_LINK}"]`);
|
||||
|
||||
expect(await validLink.count()).toBe(1);
|
||||
await expect(validLink).toHaveCount(1);
|
||||
|
||||
// Start waiting for popup before clicking. Note no await.
|
||||
const popupPromise = page.waitForEvent('popup');
|
||||
@ -409,7 +413,7 @@ test.describe('Notebook entry tests', () => {
|
||||
|
||||
const invalidLink = page.locator(`a[href="${TEST_LINK}"]`);
|
||||
|
||||
expect(await invalidLink.count()).toBe(0);
|
||||
await expect(invalidLink).toHaveCount(0);
|
||||
});
|
||||
test('when a link is entered, but it is not in the whitelisted urls, it does not become clickable when viewing', async ({
|
||||
page
|
||||
@ -426,7 +430,7 @@ test.describe('Notebook entry tests', () => {
|
||||
|
||||
const invalidLink = page.locator(`a[href="${TEST_LINK}"]`);
|
||||
|
||||
expect(await invalidLink.count()).toBe(0);
|
||||
await expect(invalidLink).toHaveCount(0);
|
||||
});
|
||||
test('when a valid link with a subdomain and a valid domain in the whitelisted urls is entered into a notebook entry, it becomes clickable when viewing', async ({
|
||||
page
|
||||
@ -443,7 +447,7 @@ test.describe('Notebook entry tests', () => {
|
||||
|
||||
const validLink = page.locator(`a[href="${INVALID_TEST_LINK}"]`);
|
||||
|
||||
expect(await validLink.count()).toBe(1);
|
||||
await expect(validLink).toHaveCount(1);
|
||||
});
|
||||
test('when a valid secure link is entered into a notebook entry, it becomes clickable when viewing', async ({
|
||||
page
|
||||
@ -460,7 +464,7 @@ test.describe('Notebook entry tests', () => {
|
||||
|
||||
const validLink = page.locator(`a[href="${TEST_LINK}"]`);
|
||||
|
||||
expect(await validLink.count()).toBe(1);
|
||||
await expect(validLink).toHaveCount(1);
|
||||
|
||||
// Start waiting for popup before clicking. Note no await.
|
||||
const popupPromise = page.waitForEvent('popup');
|
||||
@ -493,7 +497,7 @@ test.describe('Notebook entry tests', () => {
|
||||
const unsanitizedLink = page.locator(`a[href="${TEST_LINK_BAD}"]`);
|
||||
|
||||
expect.soft(await sanitizedLink.count()).toBe(1);
|
||||
expect(await unsanitizedLink.count()).toBe(0);
|
||||
await expect(unsanitizedLink).toHaveCount(0);
|
||||
});
|
||||
test('Can add markdown to a notebook entry', async ({ page }) => {
|
||||
await page.goto(notebookObject.url);
|
||||
@ -546,4 +550,110 @@ test.describe('Notebook entry tests', () => {
|
||||
);
|
||||
await expect(secondLineOfBlockquoteText).toBeVisible();
|
||||
});
|
||||
|
||||
/**
|
||||
* Paste into notebook entry tests
|
||||
*/
|
||||
test('Can paste text into a notebook entry', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/7686'
|
||||
});
|
||||
const TEST_TEXT = 'This is a test';
|
||||
const iterations = 20;
|
||||
const EXPECTED_TEXT = TEST_TEXT.repeat(iterations);
|
||||
|
||||
await page.goto(notebookObject.url);
|
||||
|
||||
await nbUtils.addNotebookEntry(page);
|
||||
await nbUtils.enterTextInLastEntry(page, TEST_TEXT);
|
||||
await selectAll(page);
|
||||
await copy(page);
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
await paste(page);
|
||||
}
|
||||
await nbUtils.commitEntry(page);
|
||||
|
||||
await expect(page.locator(`text="${EXPECTED_TEXT}"`)).toBeVisible();
|
||||
});
|
||||
|
||||
test('Prevents pasting text into selected notebook entry if not editing', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/7686'
|
||||
});
|
||||
const TEST_TEXT = 'This is a test';
|
||||
|
||||
await page.goto(notebookObject.url);
|
||||
|
||||
await nbUtils.addNotebookEntry(page);
|
||||
await nbUtils.enterTextInLastEntry(page, TEST_TEXT);
|
||||
await selectAll(page);
|
||||
await copy(page);
|
||||
await paste(page);
|
||||
await nbUtils.commitEntry(page);
|
||||
|
||||
// This should not paste text into the entry
|
||||
await paste(page);
|
||||
|
||||
await expect(await page.locator(`text="${TEST_TEXT.repeat(1)}"`).count()).toEqual(1);
|
||||
await expect(await page.locator(`text="${TEST_TEXT.repeat(2)}"`).count()).toEqual(0);
|
||||
});
|
||||
|
||||
test('When changing the name of a notebook in the browse bar, new notebook changes are not lost', async ({
|
||||
page
|
||||
}) => {
|
||||
const TEST_TEXT = 'Do not lose me!';
|
||||
const FIRST_NEW_NAME = 'New Name';
|
||||
const SECOND_NEW_NAME = 'Second New Name';
|
||||
|
||||
await page.goto(notebookObject.url);
|
||||
|
||||
await page.getByLabel('Expand My Items folder').click();
|
||||
|
||||
await renameCurrentObjectFromBrowseBar(page, FIRST_NEW_NAME);
|
||||
|
||||
// verify the name change in tree and browse bar
|
||||
await verifyNameChange(page, FIRST_NEW_NAME);
|
||||
|
||||
// enter one entry
|
||||
await enterAndCommitTextEntry(page, TEST_TEXT);
|
||||
|
||||
// verify the entry is present
|
||||
await expect(await page.locator(`text="${TEST_TEXT}"`).count()).toEqual(1);
|
||||
|
||||
// change the name
|
||||
await renameCurrentObjectFromBrowseBar(page, SECOND_NEW_NAME);
|
||||
|
||||
// verify the name change in tree and browse bar
|
||||
await verifyNameChange(page, SECOND_NEW_NAME);
|
||||
|
||||
// verify the entry is still present
|
||||
await expect(await page.locator(`text="${TEST_TEXT}"`).count()).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Enter text into the last notebook entry and commit it.
|
||||
*
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} text
|
||||
*/
|
||||
async function enterAndCommitTextEntry(page, text) {
|
||||
await nbUtils.addNotebookEntry(page);
|
||||
await nbUtils.enterTextInLastEntry(page, text);
|
||||
await nbUtils.commitEntry(page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the name change in the tree and browse bar.
|
||||
*
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} newName
|
||||
*/
|
||||
async function verifyNameChange(page, newName) {
|
||||
await expect(
|
||||
page.getByRole('treeitem').locator('.is-navigated-object .c-tree__item__name')
|
||||
).toHaveText(newName);
|
||||
await expect(page.getByLabel('Browse bar object name')).toHaveText(newName);
|
||||
}
|
||||
|
@ -76,7 +76,7 @@ test.describe('Snapshot image tests', () => {
|
||||
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 expect(page.getByRole('img', { name: 'favicon-96x96.png thumbnail' })).toHaveCount(2);
|
||||
|
||||
await page.locator('.c-snapshot.c-ne__embed').first().getByTitle('More actions').click();
|
||||
|
||||
@ -86,7 +86,7 @@ test.describe('Snapshot image tests', () => {
|
||||
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);
|
||||
await expect(page.getByRole('img', { name: 'favicon-96x96.png thumbnail' })).toHaveCount(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -65,9 +65,11 @@ test.describe('Tagging in Notebooks @addInit', () => {
|
||||
});
|
||||
test('Can add tags with blank entry', async ({ page }) => {
|
||||
await createDomainObjectWithDefaults(page, { type: 'Notebook' });
|
||||
await page.getByRole('tab', { name: 'Annotations' }).click();
|
||||
|
||||
await enterTextEntry(page, '');
|
||||
|
||||
await page.getByRole('tab', { name: 'Annotations' }).click();
|
||||
|
||||
await page.hover(`button:has-text("Add Tag")`);
|
||||
await page.locator(`button:has-text("Add Tag")`).click();
|
||||
|
||||
|
@ -24,16 +24,22 @@
|
||||
This test suite is dedicated to tests which verify the basic operations surrounding Notebooks with CouchDB.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Disable no-networkidle eslint rule until we can engineer more deterministic network-event
|
||||
* driven tests.
|
||||
*/
|
||||
/* eslint-disable playwright/no-networkidle */
|
||||
|
||||
import { createDomainObjectWithDefaults } from '../../../../appActions.js';
|
||||
import * as nbUtils from '../../../../helper/notebookUtils.js';
|
||||
import { expect, test } from '../../../../pluginFixtures.js';
|
||||
|
||||
test.describe('Notebook Tests with CouchDB @couchdb', () => {
|
||||
test.describe('Notebook Tests with CouchDB @couchdb @network', () => {
|
||||
let testNotebook;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
//Navigate to baseURL
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
// Navigate to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
// Create Notebook
|
||||
testNotebook = await createDomainObjectWithDefaults(page, { type: 'Notebook' });
|
||||
@ -41,8 +47,6 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => {
|
||||
});
|
||||
|
||||
test('Inspect Notebook Entry Network Requests', async ({ page }) => {
|
||||
//Ensure we're on the annotations Tab in the inspector
|
||||
await page.getByText('Annotations').click();
|
||||
// Expand sidebar
|
||||
await page.locator('.c-notebook__toggle-nav-button').click();
|
||||
|
||||
@ -55,7 +59,7 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => {
|
||||
// Waits for the next request with the specified url
|
||||
page.waitForRequest(`**/openmct/${testNotebook.uuid}`),
|
||||
// Triggers the request
|
||||
page.click('[aria-label="Add Page"]')
|
||||
page.getByLabel('Add Page').click()
|
||||
]);
|
||||
// Ensures that there are no other network requests
|
||||
await page.waitForLoadState('networkidle');
|
||||
@ -63,7 +67,7 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => {
|
||||
// Assert that only two requests are made
|
||||
// Network Requests are:
|
||||
// 1) The actual POST to create the page
|
||||
expect(notebookElementsRequests.length).toBe(1);
|
||||
expect(notebookElementsRequests).toHaveLength(1);
|
||||
|
||||
// Assert on request object
|
||||
expect(notebookUrlRequest.postDataJSON().metadata.name).toBe(testNotebook.name);
|
||||
@ -80,6 +84,9 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => {
|
||||
await page.waitForLoadState('networkidle');
|
||||
expect(notebookElementsRequests.length).toBeLessThanOrEqual(2);
|
||||
|
||||
//Ensure we're on the annotations Tab in the inspector
|
||||
await page.getByText('Annotations').click();
|
||||
|
||||
// Add some tags
|
||||
// Network Requests are for each tag creation are:
|
||||
// 1) Getting the original path of the parent object
|
||||
@ -120,8 +127,8 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => {
|
||||
expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(12);
|
||||
|
||||
// Add two more pages
|
||||
await page.click('[aria-label="Add Page"]');
|
||||
await page.click('[aria-label="Add Page"]');
|
||||
await page.getByLabel('Add Page').click();
|
||||
await page.getByLabel('Add Page').click();
|
||||
|
||||
// Add three entries
|
||||
await nbUtils.enterTextEntry(page, 'First Entry');
|
||||
@ -174,8 +181,8 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => {
|
||||
type: 'issue',
|
||||
description: 'https://github.com/akhenry/openmct-yamcs/issues/69'
|
||||
});
|
||||
await page.getByText('Annotations').click();
|
||||
await nbUtils.enterTextEntry(page, 'First Entry');
|
||||
await page.getByText('Annotations').click();
|
||||
|
||||
// Add three tags
|
||||
await addTagAndAwaitNetwork(page, 'Science');
|
||||
|
@ -20,7 +20,6 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import { openObjectTreeContextMenu } from '../../../../appActions.js';
|
||||
import {
|
||||
dragAndDropEmbed,
|
||||
enterTextEntry,
|
||||
@ -51,27 +50,22 @@ test.describe('Restricted Notebook', () => {
|
||||
const restrictedNotebookTreeObject = page.locator(`a:has-text("${notebook.name}")`);
|
||||
|
||||
// notebook tree object exists
|
||||
expect.soft(await restrictedNotebookTreeObject.count()).toEqual(1);
|
||||
await expect(restrictedNotebookTreeObject).toHaveCount(1);
|
||||
|
||||
// Click Remove Text
|
||||
await page.locator('li[role="menuitem"]:has-text("Remove")').click();
|
||||
|
||||
// Click 'OK' on confirmation window and wait for save banner to appear
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('button:has-text("OK")').click(),
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
// Click 'Ok' on confirmation window
|
||||
await page.locator('button:has-text("OK")').click();
|
||||
|
||||
// has been deleted
|
||||
expect(await restrictedNotebookTreeObject.count()).toEqual(0);
|
||||
await expect(restrictedNotebookTreeObject).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('Can be locked if at least one page has one entry @addInit', async ({ page }) => {
|
||||
await enterTextEntry(page, TEST_TEXT);
|
||||
|
||||
const commitButton = page.locator('button:has-text("Commit Entries")');
|
||||
expect(await commitButton.count()).toEqual(1);
|
||||
await expect(page.getByLabel('Commit Entries')).toHaveCount(1);
|
||||
});
|
||||
});
|
||||
|
||||
@ -86,20 +80,18 @@ test.describe('Restricted Notebook with at least one entry and with the page loc
|
||||
await page.locator('button.c-notebook__toggle-nav-button').click();
|
||||
});
|
||||
|
||||
test('Locked page should now be in a locked state @addInit @unstable', async ({
|
||||
page
|
||||
}, testInfo) => {
|
||||
test('Locked page should now be in a locked state @addInit', async ({ page }, testInfo) => {
|
||||
// eslint-disable-next-line playwright/no-skipped-test
|
||||
test.skip(testInfo.project === 'chrome-beta', 'Test is unreliable on chrome-beta');
|
||||
// main lock message on page
|
||||
const lockMessage = page.locator(
|
||||
'text=This page has been committed and cannot be modified or removed'
|
||||
);
|
||||
expect.soft(await lockMessage.count()).toEqual(1);
|
||||
await expect(lockMessage).toHaveCount(1);
|
||||
|
||||
// lock icon on page in sidebar
|
||||
const pageLockIcon = page.locator('ul.c-notebook__pages li div.icon-lock');
|
||||
expect.soft(await pageLockIcon.count()).toEqual(1);
|
||||
await expect(pageLockIcon).toHaveCount(1);
|
||||
|
||||
// no way to remove a restricted notebook with a locked page
|
||||
await openObjectTreeContextMenu(page, notebook.url);
|
||||
@ -119,17 +111,14 @@ test.describe('Restricted Notebook with at least one entry and with the page loc
|
||||
await page.getByText('Unnamed Page').nth(1).fill(TEST_TEXT_NAME);
|
||||
|
||||
// expect to be able to rename unlocked pages
|
||||
const newPageElement = page.getByText(TEST_TEXT_NAME);
|
||||
const newPageCount = await newPageElement.count();
|
||||
await newPageElement.press('Enter'); // exit contenteditable state
|
||||
expect.soft(newPageCount).toEqual(1);
|
||||
await page.getByText(TEST_TEXT_NAME).press('Enter'); // exit contenteditable state
|
||||
await expect(page.locator('div').filter({ hasText: /^Test Page$/ })).toHaveCount(1);
|
||||
|
||||
// enter test text
|
||||
await enterTextEntry(page, TEST_TEXT);
|
||||
|
||||
// expect new page to be lockable
|
||||
const commitButton = page.getByRole('button', { name: ' Commit Entries' });
|
||||
expect.soft(await commitButton.count()).toEqual(1);
|
||||
await expect(page.getByLabel('Commit Entries')).toHaveCount(1);
|
||||
|
||||
// Click the context menu button for the new page
|
||||
await page.getByTitle('Open context menu').click();
|
||||
@ -140,7 +129,7 @@ test.describe('Restricted Notebook with at least one entry and with the page loc
|
||||
|
||||
// deleted page, should no longer exist
|
||||
const deletedPageElement = page.getByText(TEST_TEXT_NAME);
|
||||
expect(await deletedPageElement.count()).toEqual(0);
|
||||
await expect(deletedPageElement).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
|
||||
@ -173,7 +162,7 @@ test.describe('can export restricted notebook as text', () => {
|
||||
await startAndAddRestrictedNotebookObject(page);
|
||||
});
|
||||
|
||||
test('basic functionality ', async ({ page }) => {
|
||||
test('basic functionality', async ({ page }) => {
|
||||
await enterTextEntry(page, `Foo bar entry`);
|
||||
// Click on 3 Dot Menu
|
||||
await page.locator('button[title="More actions"]').click();
|
||||
@ -190,8 +179,23 @@ test.describe('can export restricted notebook as text', () => {
|
||||
expect(exportedText).toContain('Foo bar entry');
|
||||
});
|
||||
|
||||
test.fixme('can export multiple notebook entries as text ', async ({ page }) => {});
|
||||
test.fixme('can export multiple notebook entries as text', async ({ page }) => {});
|
||||
test.fixme('can export all notebook entry metdata', async ({ page }) => {});
|
||||
test.fixme('can export all notebook tags', async ({ page }) => {});
|
||||
test.fixme('can export all notebook snapshots', async ({ page }) => {});
|
||||
});
|
||||
|
||||
/**
|
||||
* Open the given `domainObject`'s context menu from the object tree.
|
||||
* Expands the path to the object and scrolls to it if necessary.
|
||||
*
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} url the url to the object
|
||||
*/
|
||||
async function openObjectTreeContextMenu(page, url) {
|
||||
await page.goto(url);
|
||||
await page.getByLabel('Show selected item in tree').click();
|
||||
await page.locator('.is-navigated-object').click({
|
||||
button: 'right'
|
||||
});
|
||||
}
|
||||
|
@ -0,0 +1,72 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import { expect, test } from '../../../../pluginFixtures.js';
|
||||
|
||||
test.describe('The performance indicator', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
await page.evaluate(() => {
|
||||
const openmct = window.openmct;
|
||||
openmct.install(openmct.plugins.PerformanceIndicator());
|
||||
});
|
||||
});
|
||||
|
||||
test('can be installed', ({ page }) => {
|
||||
const performanceIndicator = page.getByTitle('Performance Indicator');
|
||||
expect(performanceIndicator).toBeDefined();
|
||||
});
|
||||
|
||||
test('Shows a numerical FPS value', async ({ page }) => {
|
||||
// Frames Per Second. We need to wait at least 1 second to get a value.
|
||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||
await page.waitForTimeout(1000);
|
||||
await expect(page.getByTitle('Performance Indicator')).toHaveText(/\d\d? fps/);
|
||||
});
|
||||
|
||||
test('Supports showing optional extended performance information in an overlay for debugging', async ({
|
||||
page
|
||||
}) => {
|
||||
const performanceMeasurementLabel = 'Some measurement';
|
||||
const performanceMeasurementValue = 'Some value';
|
||||
|
||||
await page.evaluate(
|
||||
({ performanceMeasurementLabel: label, performanceMeasurementValue: value }) => {
|
||||
const openmct = window.openmct;
|
||||
openmct.performance.measurements.set(label, value);
|
||||
},
|
||||
{ performanceMeasurementLabel, performanceMeasurementValue }
|
||||
);
|
||||
const performanceIndicator = page.getByTitle('Performance Indicator');
|
||||
await performanceIndicator.click();
|
||||
//Performance overlay is a crude debugging tool, it's evaluated once per second.
|
||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||
await page.waitForTimeout(1000);
|
||||
const performanceOverlay = page.getByTitle('Performance Overlay');
|
||||
await expect(performanceOverlay).toBeVisible();
|
||||
await expect(performanceOverlay).toHaveText(new RegExp(`${performanceMeasurementLabel}.*`));
|
||||
await expect(performanceOverlay).toHaveText(new RegExp(`.*${performanceMeasurementValue}`));
|
||||
|
||||
//Confirm that it disappears if we click on it again.
|
||||
await performanceIndicator.click();
|
||||
await expect(performanceOverlay).toBeHidden();
|
||||
});
|
||||
});
|
@ -24,7 +24,10 @@
|
||||
Testsuite for plot autoscale.
|
||||
*/
|
||||
|
||||
import { createDomainObjectWithDefaults } from '../../../../appActions.js';
|
||||
import {
|
||||
createDomainObjectWithDefaults,
|
||||
navigateToObjectWithFixedTimeBounds
|
||||
} from '../../../../appActions.js';
|
||||
import { expect, test } from '../../../../pluginFixtures.js';
|
||||
test.use({
|
||||
viewport: {
|
||||
@ -51,9 +54,7 @@ test.describe('Autoscale', () => {
|
||||
});
|
||||
|
||||
// Switch to fixed time, start: 2022-03-28 22:00:00.000 UTC, end: 2022-03-28 22:00:30.000 UTC
|
||||
await page.goto(
|
||||
`${overlayPlot.url}?tc.mode=fixed&tc.startBound=1648591200000&tc.endBound=1648591230000&tc.timeSystem=utc&view=plot-overlay`
|
||||
);
|
||||
await navigateToObjectWithFixedTimeBounds(page, overlayPlot.url, 1648591200000, 1648591230000);
|
||||
|
||||
await testYTicks(page, ['-1.00', '-0.50', '0.00', '0.50', '1.00']);
|
||||
|
||||
@ -61,20 +62,23 @@ test.describe('Autoscale', () => {
|
||||
await page.getByLabel('Edit Object').click();
|
||||
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
await turnOffAutoscale(page);
|
||||
|
||||
await setUserDefinedMinAndMax(page, '-2', '2');
|
||||
// turn off autoscale
|
||||
await page.getByRole('checkbox', { name: 'Auto scale' }).uncheck();
|
||||
|
||||
await page.getByLabel('Y Axis 1 Minimum value').fill('-2');
|
||||
await page.getByLabel('Y Axis 1 Maximum value').fill('2');
|
||||
|
||||
// save
|
||||
await page.click('button[title="Save"]');
|
||||
await page.getByLabel('Save').click();
|
||||
await Promise.all([
|
||||
page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
page.locator('.c-message-banner__message').hover({ trial: true })
|
||||
]);
|
||||
//Wait until Save Banner is gone
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached' });
|
||||
await page.locator('.c-message-banner__message').waitFor({ state: 'detached' });
|
||||
|
||||
// Make sure that after turning off autoscale, the user entered range values are reflected in the ticks.
|
||||
await testYTicks(page, [
|
||||
@ -127,26 +131,6 @@ test.describe('Autoscale', () => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function turnOffAutoscale(page) {
|
||||
// uncheck autoscale
|
||||
await page.getByRole('checkbox', { name: 'Auto scale' }).uncheck();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} min
|
||||
* @param {string} max
|
||||
*/
|
||||
async function setUserDefinedMinAndMax(page, min, max) {
|
||||
// set minimum value
|
||||
await page.getByRole('spinbutton').first().fill(min);
|
||||
// set maximum value
|
||||
await page.getByRole('spinbutton').nth(1).fill(max);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
|
@ -25,19 +25,50 @@ Tests to verify log plot functionality. Note this test suite if very much under
|
||||
necessarily be used for reference when writing new tests in this area.
|
||||
*/
|
||||
|
||||
import { setTimeConductorBounds } from '../../../../appActions.js';
|
||||
import { createDomainObjectWithDefaults, setTimeConductorBounds } from '../../../../appActions.js';
|
||||
import { expect, test } from '../../../../pluginFixtures.js';
|
||||
|
||||
test.describe('Log plot tests', () => {
|
||||
test('Log Plot ticks are functionally correct in regular and log mode and after refresh', async ({
|
||||
page,
|
||||
openmctConfig
|
||||
}) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
//Test is slow and should be split in the future
|
||||
test.slow();
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// fresh page with time range from 2022-03-29 22:00:00.000Z to 2022-03-29 22:00:30.000Z
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
await makeOverlayPlot(page, myItemsFolderName);
|
||||
// Set a specific time range for consistency, otherwise it will change
|
||||
// on every test to a range based on the current time.
|
||||
const startDate = '2022-03-29';
|
||||
const startTime = '22:00:00';
|
||||
const endDate = '2022-03-29';
|
||||
const endTime = '22:00:30';
|
||||
|
||||
await setTimeConductorBounds(page, { startDate, startTime, endDate, endTime });
|
||||
|
||||
const overlayPlot = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Overlay Plot',
|
||||
name: 'Unnamed Overlay Plot'
|
||||
});
|
||||
|
||||
// create a sinewave generator
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
name: 'Unnamed Sine Wave Generator',
|
||||
parent: overlayPlot.uuid
|
||||
});
|
||||
|
||||
await page.getByLabel('More actions').click();
|
||||
await page.getByLabel('Edit Properties...').click();
|
||||
|
||||
// set amplitude to 6, offset 4, data rate 2 hz
|
||||
await page.getByLabel('Amplitude', { exact: true }).fill('6');
|
||||
await page.getByLabel('Offset', { exact: true }).fill('4');
|
||||
await page.getByLabel('Data Rate (hz)', { exact: true }).fill('2');
|
||||
|
||||
await page.getByLabel('Save').click();
|
||||
|
||||
await page.goto(overlayPlot.url);
|
||||
});
|
||||
test('Log Plot ticks are functionally correct in regular and log mode and after refresh', async ({
|
||||
page
|
||||
}) => {
|
||||
await testRegularTicks(page);
|
||||
await enableEditMode(page);
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
@ -47,98 +78,35 @@ test.describe('Log plot tests', () => {
|
||||
await testRegularTicks(page);
|
||||
await enableLogMode(page);
|
||||
await testLogTicks(page);
|
||||
await saveOverlayPlot(page);
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
await testLogTicks(page);
|
||||
});
|
||||
|
||||
// Leaving test as 'TODO' for now.
|
||||
// NOTE: Not eligible for community contributions.
|
||||
test.fixme(
|
||||
'Verify that log mode option is reflected in import/export JSON',
|
||||
async ({ page, openmctConfig }) => {
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
test.fixme('Verify that log mode option is reflected in import/export JSON', async ({ page }) => {
|
||||
await enableEditMode(page);
|
||||
await enableLogMode(page);
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
await makeOverlayPlot(page, myItemsFolderName);
|
||||
await enableEditMode(page);
|
||||
await enableLogMode(page);
|
||||
await saveOverlayPlot(page);
|
||||
// TODO ...export, delete the overlay, then import it...
|
||||
|
||||
// TODO ...export, delete the overlay, then import it...
|
||||
//await testLogTicks(page);
|
||||
|
||||
//await testLogTicks(page);
|
||||
|
||||
// TODO, the plot is slightly at different position that in the other test, so this fails.
|
||||
// ...We can fix it by copying all steps from the first test...
|
||||
// await testLogPlotPixels(page);
|
||||
}
|
||||
);
|
||||
// TODO, the plot is slightly at different position that in the other test, so this fails.
|
||||
// ...We can fix it by copying all steps from the first test...
|
||||
// await testLogPlotPixels(page);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Makes an overlay plot with a sine wave generator and clicks on the overlay plot in the sidebar so it is the active thing displayed.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} myItemsFolderName
|
||||
*/
|
||||
async function makeOverlayPlot(page, myItemsFolderName) {
|
||||
// fresh page with time range from 2022-03-29 22:00:00.000Z to 2022-03-29 22:00:30.000Z
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Set a specific time range for consistency, otherwise it will change
|
||||
// on every test to a range based on the current time.
|
||||
|
||||
const start = '2022-03-29 22:00:00.000Z';
|
||||
const end = '2022-03-29 22:00:30.000Z';
|
||||
|
||||
await setTimeConductorBounds(page, start, end);
|
||||
|
||||
// create overlay plot
|
||||
|
||||
await page.locator('button.c-create-button').click();
|
||||
await page.locator('li[role="menuitem"]:has-text("Overlay Plot")').click();
|
||||
// Click OK button and wait for Navigate event
|
||||
await Promise.all([
|
||||
page.waitForLoadState(),
|
||||
await page.getByRole('button', { name: 'Save' }).click(),
|
||||
// Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
|
||||
// save the overlay plot
|
||||
await saveOverlayPlot(page);
|
||||
|
||||
// create a sinewave generator
|
||||
|
||||
await page.locator('button.c-create-button').click();
|
||||
await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click();
|
||||
|
||||
// set amplitude to 6, offset 4, data rate 2 hz
|
||||
|
||||
await page.getByLabel('Amplitude').fill('6');
|
||||
await page.getByLabel('Offset').fill('4');
|
||||
await page.getByLabel('Data Rate (hz)').fill('2');
|
||||
|
||||
// Click OK button and wait for Navigate event
|
||||
await Promise.all([
|
||||
page.waitForLoadState(),
|
||||
await page.getByRole('button', { name: 'Save' }).click(),
|
||||
// Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
|
||||
// click on overlay plot
|
||||
await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click();
|
||||
await Promise.all([
|
||||
page.waitForLoadState(),
|
||||
page.locator('text=Unnamed Overlay Plot').first().click()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function testRegularTicks(page) {
|
||||
const yTicks = page.locator('.gl-plot-y-tick-label');
|
||||
expect(await yTicks.count()).toBe(7);
|
||||
await expect(yTicks).toHaveCount(7);
|
||||
await expect(yTicks.nth(0)).toHaveText('-2');
|
||||
await expect(yTicks.nth(1)).toHaveText('0');
|
||||
await expect(yTicks.nth(2)).toHaveText('2');
|
||||
@ -153,7 +121,7 @@ async function testRegularTicks(page) {
|
||||
*/
|
||||
async function testLogTicks(page) {
|
||||
const yTicks = page.locator('.gl-plot-y-tick-label');
|
||||
expect(await yTicks.count()).toBe(9);
|
||||
await expect(yTicks).toHaveCount(9);
|
||||
await expect(yTicks.nth(0)).toHaveText('-2.98');
|
||||
await expect(yTicks.nth(1)).toHaveText('-1.51');
|
||||
await expect(yTicks.nth(2)).toHaveText('-0.58');
|
||||
@ -190,26 +158,6 @@ async function disableLogMode(page) {
|
||||
await page.getByRole('checkbox', { name: 'Log mode' }).uncheck();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function saveOverlayPlot(page) {
|
||||
// save overlay plot
|
||||
await page
|
||||
.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button')
|
||||
.nth(1)
|
||||
.click();
|
||||
|
||||
await Promise.all([
|
||||
page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
//Wait until Save Banner is gone
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached' });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
|
@ -52,8 +52,8 @@ test.describe('Overlay Plot', () => {
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
|
||||
// navigate to plot series color palette
|
||||
await page.click('.l-browse-bar__actions__edit');
|
||||
await page.locator('li.c-tree__item.menus-to-left .c-disclosure-triangle').click();
|
||||
await page.getByLabel('Edit Object').click();
|
||||
await page.getByLabel('Expand Sine Wave Generator:').click();
|
||||
await page.locator('.c-click-swatch--menu').click();
|
||||
await page.locator('.c-palette__item[style="background: rgb(255, 166, 61);"]').click();
|
||||
// gets color for swatch located in legend
|
||||
@ -91,32 +91,38 @@ test.describe('Overlay Plot', () => {
|
||||
// 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');
|
||||
await expect(page.getByLabel('Expand by Default')).toHaveText(/No/);
|
||||
|
||||
expect(await page.getByLabel('Plot Legend Item').count()).toBe(3);
|
||||
await expect(page.getByLabel('Plot Legend Item')).toHaveCount(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();
|
||||
|
||||
await page.getByRole('tab', { name: 'Config' }).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('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 page.getByRole('tab', { name: 'Config' }).click();
|
||||
|
||||
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('Expand by Default')).toHaveText(/Yes/);
|
||||
await expect(page.getByLabel('Plot Legend Item')).toHaveCount(3);
|
||||
});
|
||||
|
||||
@ -136,23 +142,16 @@ test.describe('Overlay Plot', () => {
|
||||
await page.goto(overlayPlot.url);
|
||||
|
||||
// Assert that no limit lines are shown by default
|
||||
await page.waitForSelector('.js-limit-area', { state: 'attached' });
|
||||
expect(await page.locator('.c-plot-limit-line').count()).toBe(0);
|
||||
await page.locator('.js-limit-area').waitFor({ state: 'attached' });
|
||||
await expect(page.locator('.c-plot-limit-line')).toHaveCount(0);
|
||||
|
||||
// Enter edit mode
|
||||
await page.getByLabel('Edit Object').click();
|
||||
|
||||
// Expand the "Sine Wave Generator" plot series options and enable limit lines
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
await page
|
||||
.getByRole('list', { name: 'Plot Series Properties' })
|
||||
.locator('span')
|
||||
.first()
|
||||
.click();
|
||||
await page
|
||||
.getByRole('list', { name: 'Plot Series Properties' })
|
||||
.locator('[title="Display limit lines"]~div input')
|
||||
.check();
|
||||
await page.getByLabel('Expand Sine Wave Generator:').click();
|
||||
await page.getByLabel('Limit lines').check();
|
||||
|
||||
await assertLimitLinesExistAndAreVisible(page);
|
||||
|
||||
@ -207,29 +206,21 @@ test.describe('Overlay Plot', () => {
|
||||
await page.goto(overlayPlot.url);
|
||||
|
||||
// Assert that no limit lines are shown by default
|
||||
await page.waitForSelector('.js-limit-area', { state: 'attached' });
|
||||
expect(await page.locator('.c-plot-limit-line').count()).toBe(0);
|
||||
await expect(page.locator('.js-limit-area')).toBeAttached();
|
||||
await expect(page.locator('.c-plot-limit-line')).toHaveCount(0);
|
||||
|
||||
// Enter edit mode
|
||||
await page.getByLabel('Edit Object').click();
|
||||
|
||||
// Expand the "Sine Wave Generator" plot series options and enable limit lines
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
await page
|
||||
.getByRole('list', { name: 'Plot Series Properties' })
|
||||
.locator('span')
|
||||
.first()
|
||||
.click();
|
||||
await page
|
||||
.getByRole('list', { name: 'Plot Series Properties' })
|
||||
.getByRole('checkbox', { name: 'Limit lines' })
|
||||
.check();
|
||||
await page.getByLabel('Expand Sine Wave Generator:').click();
|
||||
await page.getByLabel('Limit lines').check();
|
||||
|
||||
await assertLimitLinesExistAndAreVisible(page);
|
||||
|
||||
// Save (exit edit mode)
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.locator('li[title="Save and Finish Editing"]').click();
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
const initialCoords = await assertLimitLinesExistAndAreVisible(page);
|
||||
// Resize the chart container by showing the snapshot pane.
|
||||
@ -324,32 +315,26 @@ 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 swgA = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
parent: overlayPlot.uuid
|
||||
});
|
||||
|
||||
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.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.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);
|
||||
}
|
||||
);
|
||||
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, {
|
||||
@ -372,7 +357,7 @@ test.describe('Overlay Plot', () => {
|
||||
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 page.getByRole('button', { name: 'Ok', exact: true }).click();
|
||||
await expect(swgAElementsPoolItem).toBeHidden();
|
||||
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
@ -402,7 +387,7 @@ async function assertLimitLinesExistAndAreVisible(page) {
|
||||
// Wait for plot series data to load
|
||||
await waitForPlotsToRender(page);
|
||||
// Wait for limit lines to be created
|
||||
await page.waitForSelector('.js-limit-area', { state: 'attached' });
|
||||
await page.locator('.js-limit-area').waitFor({ 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();
|
||||
|
149
e2e/tests/functional/plugins/plot/plotControls.e2e.spec.js
Normal file
149
e2e/tests/functional/plugins/plot/plotControls.e2e.spec.js
Normal file
@ -0,0 +1,149 @@
|
||||
/*****************************************************************************
|
||||
* 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 rendering and interaction of plots.
|
||||
*
|
||||
*/
|
||||
|
||||
import {
|
||||
createDomainObjectWithDefaults,
|
||||
getCanvasPixels,
|
||||
setEndOffset,
|
||||
setRealTimeMode,
|
||||
setStartOffset
|
||||
} from '../../../../appActions.js';
|
||||
import { expect, test } from '../../../../pluginFixtures.js';
|
||||
|
||||
test.describe('Plot Controls', () => {
|
||||
let overlayPlot;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Open a browser, navigate to the main page, and wait until all networkevents to resolve
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
overlayPlot = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Overlay Plot'
|
||||
});
|
||||
|
||||
// Create an overlay plot with a sine wave generator
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
parent: overlayPlot.uuid
|
||||
});
|
||||
await page.goto(`${overlayPlot.url}`);
|
||||
});
|
||||
|
||||
test("Plots don't purge data when paused", async ({ page }) => {
|
||||
// Set realtime mode with 2 second window
|
||||
const startOffset = {
|
||||
startMins: '00',
|
||||
startSecs: '01'
|
||||
};
|
||||
|
||||
const endOffset = {
|
||||
endMins: '00',
|
||||
endSecs: '01'
|
||||
};
|
||||
|
||||
// Switch to real-time mode
|
||||
await setRealTimeMode(page);
|
||||
|
||||
// Set start time offset
|
||||
await setStartOffset(page, startOffset);
|
||||
|
||||
// Set end time offset
|
||||
await setEndOffset(page, endOffset);
|
||||
// Edit the overlay plot and turn off auto scale, setting the min and max to -1 and 1
|
||||
// enter edit mode
|
||||
await page.getByLabel('Edit Object').click();
|
||||
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
|
||||
// turn off autoscale
|
||||
await page.getByRole('checkbox', { name: 'Auto scale' }).uncheck();
|
||||
|
||||
await page.getByLabel('Y Axis 1 Minimum value').fill('-1');
|
||||
await page.getByLabel('Y Axis 1 Maximum value').fill('1');
|
||||
|
||||
// save
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
// hover over plot for plot controls
|
||||
await page.getByLabel('Plot Canvas').hover();
|
||||
// click on pause control
|
||||
await page.getByTitle('Pause incoming real-time data').click();
|
||||
// expect plot to be paused
|
||||
await expect(page.getByTitle('Resume displaying real-time data')).toBeVisible();
|
||||
// Wait for 2 seconds to stabilize plot data - future timestamp
|
||||
// eslint-disable-next-line
|
||||
await page.waitForTimeout(2000);
|
||||
// Capture the # of plot points
|
||||
const plotPixels = await getCanvasPixels(page, 'canvas');
|
||||
const plotPixelSizeAtPause = plotPixels.length;
|
||||
// Wait 2 seconds
|
||||
// eslint-disable-next-line
|
||||
await page.waitForTimeout(2000);
|
||||
// Capture the # of plot points
|
||||
const plotPixelsAfterWait = await getCanvasPixels(page, 'canvas');
|
||||
const plotPixelSizeAfterWait = plotPixelsAfterWait.length;
|
||||
// Expect before and after plot points to match
|
||||
await expect(plotPixelSizeAtPause).toEqual(plotPixelSizeAfterWait);
|
||||
});
|
||||
|
||||
/*
|
||||
Test to verify that switching a plot's time context from global to
|
||||
its own independent time context and then back to global context works correctly.
|
||||
|
||||
After switching from fixed time mode (ITC) to real time mode (global context),
|
||||
the pause control for the plot should be available, indicating that it is following the right context.
|
||||
*/
|
||||
test('Plots follow the right time context', async ({ page }) => {
|
||||
// Set global time conductor to real-time mode
|
||||
await setRealTimeMode(page);
|
||||
|
||||
// hover over plot for plot controls
|
||||
await page.getByLabel('Plot Canvas').hover();
|
||||
// Ensure pause control is visible since global time conductor is in Real time mode.
|
||||
await expect(page.getByTitle('Pause incoming real-time data')).toBeVisible();
|
||||
|
||||
// Toggle independent time conductor ON
|
||||
await page.getByLabel('Enable Independent Time Conductor').click();
|
||||
|
||||
// Bring up the independent time conductor popup and switch to fixed time mode
|
||||
await page.getByLabel('Independent Time Conductor Settings').click();
|
||||
await page.getByLabel('Independent Time Conductor Mode Menu').click();
|
||||
await page.getByRole('menuitem', { name: /Fixed Timespan/ }).click();
|
||||
|
||||
// hover over plot for plot controls
|
||||
await page.getByLabel('Plot Canvas').hover();
|
||||
// Ensure pause control is no longer visible since the plot is following the independent time context
|
||||
await expect(page.getByTitle('Pause incoming real-time data')).toBeHidden();
|
||||
|
||||
// Toggle independent time conductor OFF - Note that the global time conductor is still in Real time mode
|
||||
await page.getByLabel('Disable Independent Time Conductor').click();
|
||||
|
||||
// hover over plot for plot controls
|
||||
await page.getByLabel('Plot Canvas').hover();
|
||||
// Ensure pause control is visible since the global time conductor is in real time mode
|
||||
await expect(page.getByTitle('Pause incoming real-time data')).toBeVisible();
|
||||
});
|
||||
});
|
@ -0,0 +1,58 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2025, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
* This test suite is dedicated to testing the rendering and interaction of plots.
|
||||
*
|
||||
*/
|
||||
|
||||
import { createDomainObjectWithDefaults } from '../../../../appActions.js';
|
||||
import { expect, test } from '../../../../pluginFixtures.js';
|
||||
|
||||
test.describe('Plot Controls in compact mode', () => {
|
||||
let timeStrip;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Open a browser, navigate to the main page, and wait until all networkevents to resolve
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
timeStrip = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Time Strip'
|
||||
});
|
||||
|
||||
// Create an overlay plot with a sine wave generator
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
parent: timeStrip.uuid
|
||||
});
|
||||
await page.goto(`${timeStrip.url}`);
|
||||
});
|
||||
|
||||
test('Plots show cursor guides', async ({ page }) => {
|
||||
// hover over plot for plot controls
|
||||
await page.getByLabel('Plot Canvas').hover();
|
||||
// click on cursor guides control
|
||||
await page.getByTitle('Toggle cursor guides').click();
|
||||
await page.getByLabel('Plot Canvas').hover();
|
||||
await expect(page.getByLabel('Vertical cursor guide')).toBeVisible();
|
||||
await expect(page.getByLabel('Horizontal cursor guide')).toBeVisible();
|
||||
});
|
||||
});
|
@ -25,7 +25,11 @@
|
||||
*
|
||||
*/
|
||||
|
||||
import { createDomainObjectWithDefaults, getCanvasPixels } from '../../../../appActions.js';
|
||||
import {
|
||||
createDomainObjectWithDefaults,
|
||||
getCanvasPixels,
|
||||
setRealTimeMode
|
||||
} from '../../../../appActions.js';
|
||||
import { expect, test } from '../../../../pluginFixtures.js';
|
||||
|
||||
test.describe('Plot Rendering', () => {
|
||||
@ -50,13 +54,37 @@ test.describe('Plot Rendering', () => {
|
||||
createMineFolderRequests.push(req);
|
||||
});
|
||||
expect(createMineFolderRequests.length).toEqual(0);
|
||||
await page.getByLabel('Plot Canvas').hover();
|
||||
});
|
||||
|
||||
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('Time conductor synchronizes with plot time range when that plot control is clicked', async ({
|
||||
page
|
||||
}) => {
|
||||
// Navigate to Sine Wave Generator
|
||||
await page.goto(sineWaveGeneratorObject.url);
|
||||
// Switch to real-time mode
|
||||
await setRealTimeMode(page);
|
||||
|
||||
// hover over plot for plot controls
|
||||
await page.getByLabel('Plot Canvas').hover();
|
||||
// click on pause control
|
||||
await page.getByTitle('Pause incoming real-time data').click();
|
||||
|
||||
// expect plot to be paused
|
||||
await expect(page.getByTitle('Resume displaying real-time data')).toBeVisible();
|
||||
|
||||
// hover over plot for plot controls
|
||||
await page.getByLabel('Plot Canvas').hover();
|
||||
// click on synchronize with time conductor
|
||||
await page.getByTitle('Synchronize Time Conductor').click();
|
||||
|
||||
await page.getByRole('button', { name: 'Ok', exact: true }).click();
|
||||
|
||||
//confirm that you're now in fixed mode with the correct range
|
||||
await expect(page.getByLabel('Time Conductor Mode')).toHaveText('Fixed Timespan');
|
||||
});
|
||||
|
||||
test('Plot is rendered when infinity values exist', async ({ page }) => {
|
||||
// Edit Plot
|
||||
await editSineWaveToUseInfinityOption(page, sineWaveGeneratorObject);
|
||||
|
||||
|
244
e2e/tests/functional/plugins/plot/plotViewActions.e2e.spec.js
Normal file
244
e2e/tests/functional/plugins/plot/plotViewActions.e2e.spec.js
Normal file
@ -0,0 +1,244 @@
|
||||
/*****************************************************************************
|
||||
* 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.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
Tests to verify log plot functionality when objects are missing
|
||||
*/
|
||||
|
||||
import { createDomainObjectWithDefaults } from '../../../../appActions.js';
|
||||
import { expect, test } from '../../../../pluginFixtures.js';
|
||||
|
||||
const SWG_NAME = 'Sine Wave Generator';
|
||||
const OVERLAY_PLOT_NAME = 'Overlay Plot';
|
||||
const STACKED_PLOT_NAME = 'Stacked Plot';
|
||||
|
||||
test.describe('For a default Plot View, Plot View Action:', () => {
|
||||
let download;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
const plot = await createDomainObjectWithDefaults(page, {
|
||||
type: SWG_NAME,
|
||||
name: SWG_NAME
|
||||
});
|
||||
|
||||
await page.goto(plot.url);
|
||||
|
||||
// Set up dialog handler before clicking the export button
|
||||
await page.getByLabel('More actions').click();
|
||||
});
|
||||
|
||||
test.afterEach(async ({ page }) => {
|
||||
if (download) {
|
||||
await download.cancel();
|
||||
}
|
||||
});
|
||||
|
||||
test('Export as PNG, will suggest the correct default filename', async ({ page }) => {
|
||||
// Start waiting for download before clicking. Note no await.
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
|
||||
// trigger the download
|
||||
await page.getByLabel('Export as PNG').click();
|
||||
|
||||
download = await downloadPromise;
|
||||
|
||||
// Verify the filename contains the expected pattern
|
||||
expect(download.suggestedFilename()).toBe(`${SWG_NAME} - plot.png`);
|
||||
});
|
||||
|
||||
test('Export as JPG, will suggest the correct default filename', async ({ page }) => {
|
||||
// Start waiting for download before clicking. Note no await.
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
|
||||
// trigger the download
|
||||
await page.getByLabel('Export as JPG').click();
|
||||
|
||||
download = await downloadPromise;
|
||||
|
||||
// Verify the filename contains the expected pattern
|
||||
expect(download.suggestedFilename()).toBe(`${SWG_NAME} - plot.jpeg`);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('For an Overlay Plot View, Plot View Action:', () => {
|
||||
let download;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
const overlayPlot = await createDomainObjectWithDefaults(page, {
|
||||
type: OVERLAY_PLOT_NAME,
|
||||
name: OVERLAY_PLOT_NAME
|
||||
});
|
||||
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: SWG_NAME,
|
||||
name: SWG_NAME,
|
||||
parent: overlayPlot.uuid
|
||||
});
|
||||
|
||||
await page.goto(overlayPlot.url);
|
||||
|
||||
// Set up dialog handler before clicking the export button
|
||||
await page.getByLabel('More actions').click();
|
||||
});
|
||||
|
||||
test.afterEach(async ({ page }) => {
|
||||
if (download) {
|
||||
await download.cancel();
|
||||
}
|
||||
});
|
||||
|
||||
test('Export as PNG, will suggest the correct default filename', async ({ page }) => {
|
||||
// Start waiting for download before clicking. Note no await.
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
|
||||
// trigger the download
|
||||
await page.getByLabel('Export as PNG').click();
|
||||
|
||||
download = await downloadPromise;
|
||||
|
||||
// Verify the filename contains the expected pattern
|
||||
expect(download.suggestedFilename()).toBe(`${OVERLAY_PLOT_NAME} - plot.png`);
|
||||
});
|
||||
|
||||
test('Export as JPG, will suggest the correct default filename', async ({ page }) => {
|
||||
// Start waiting for download before clicking. Note no await.
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
|
||||
// trigger the download
|
||||
await page.getByLabel('Export as JPG').click();
|
||||
|
||||
download = await downloadPromise;
|
||||
|
||||
// Verify the filename contains the expected pattern
|
||||
expect(download.suggestedFilename()).toBe(`${OVERLAY_PLOT_NAME} - plot.jpeg`);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('For a Stacked Plot View, Plot View Action:', () => {
|
||||
let download;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
const stackedPlot = await createDomainObjectWithDefaults(page, {
|
||||
type: STACKED_PLOT_NAME,
|
||||
name: STACKED_PLOT_NAME
|
||||
});
|
||||
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: SWG_NAME,
|
||||
name: SWG_NAME,
|
||||
parent: stackedPlot.uuid
|
||||
});
|
||||
|
||||
await page.goto(stackedPlot.url);
|
||||
|
||||
// Set up dialog handler before clicking the export button
|
||||
await page.getByLabel('More actions').click();
|
||||
});
|
||||
|
||||
test.afterEach(async ({ page }) => {
|
||||
if (download) {
|
||||
await download.cancel();
|
||||
}
|
||||
});
|
||||
|
||||
test('Export as PNG, will suggest the correct default filename', async ({ page }) => {
|
||||
// Start waiting for download before clicking. Note no await.
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
|
||||
// trigger the download
|
||||
await page.getByLabel('Export as PNG').click();
|
||||
|
||||
download = await downloadPromise;
|
||||
|
||||
// Verify the filename contains the expected pattern
|
||||
expect(download.suggestedFilename()).toBe(`${STACKED_PLOT_NAME} - stacked-plot.png`);
|
||||
});
|
||||
|
||||
test('Export as JPG, will suggest the correct default filename', async ({ page }) => {
|
||||
// Start waiting for download before clicking. Note no await.
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
|
||||
// trigger the download
|
||||
await page.getByLabel('Export as JPG').click();
|
||||
|
||||
download = await downloadPromise;
|
||||
|
||||
// Verify the filename contains the expected pattern
|
||||
expect(download.suggestedFilename()).toBe(`${STACKED_PLOT_NAME} - stacked-plot.jpeg`);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Plot View Action:', () => {
|
||||
let download;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
const plot = await createDomainObjectWithDefaults(page, {
|
||||
type: SWG_NAME,
|
||||
name: `!@#${SWG_NAME}!@#><`
|
||||
});
|
||||
|
||||
await page.goto(plot.url);
|
||||
|
||||
// Set up dialog handler before clicking the export button
|
||||
await page.getByLabel('More actions').click();
|
||||
});
|
||||
|
||||
test.afterEach(async ({ page }) => {
|
||||
if (download) {
|
||||
await download.cancel();
|
||||
}
|
||||
});
|
||||
|
||||
test('Export as PNG saved filenames will not include invalid characters', async ({ page }) => {
|
||||
// Start waiting for download before clicking. Note no await.
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
|
||||
// trigger the download
|
||||
await page.getByLabel('Export as PNG').click();
|
||||
|
||||
download = await downloadPromise;
|
||||
|
||||
// Verify the filename contains the expected pattern
|
||||
expect(download.suggestedFilename()).toBe(`${SWG_NAME} - plot.png`);
|
||||
});
|
||||
|
||||
test('Export as JPG saved filenames will not include invalid characters', async ({ page }) => {
|
||||
// Start waiting for download before clicking. Note no await.
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
|
||||
// trigger the download
|
||||
await page.getByLabel('Export as JPG').click();
|
||||
|
||||
download = await downloadPromise;
|
||||
|
||||
// Verify the filename contains the expected pattern
|
||||
expect(download.suggestedFilename()).toBe(`${SWG_NAME} - plot.jpeg`);
|
||||
});
|
||||
});
|
@ -50,7 +50,7 @@ test.describe('Plots work in Previews', () => {
|
||||
});
|
||||
const layoutGridHolder = page.getByLabel('Test Display Layout Layout Grid');
|
||||
await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder);
|
||||
await page.getByLabel('Save').click();
|
||||
await page.getByLabel('Save', { exact: true }).click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
// right click on the plot and select view large
|
||||
@ -67,7 +67,7 @@ test.describe('Plots work in Previews', () => {
|
||||
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.getByLabel('Save', { exact: true }).click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
await expect(
|
||||
page.getByLabel('Test Display Layout Layout', { exact: true }).getByLabel('Plot Canvas')
|
||||
|
@ -53,7 +53,7 @@ test.describe('Scatter Plot', () => {
|
||||
await page.goto(scatterPlot.url);
|
||||
await page.getByLabel('Edit Object').click();
|
||||
await page.getByRole('tab', { name: 'Elements' }).click();
|
||||
await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeVisible();
|
||||
await expect(page.getByLabel(`Preview ${swg1.name}`)).toBeVisible();
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
@ -65,14 +65,12 @@ test.describe('Scatter Plot', () => {
|
||||
});
|
||||
|
||||
// Verify that the 'Replace telemetry source' modal appears and accept it
|
||||
await expect
|
||||
.soft(
|
||||
page.locator(
|
||||
'text=This action will replace the current telemetry source. Do you want to continue?'
|
||||
)
|
||||
await expect(
|
||||
page.getByText(
|
||||
'This action will replace the current telemetry source. Do you want to continue?'
|
||||
)
|
||||
.toBeVisible();
|
||||
await page.click('text=Ok');
|
||||
).toBeVisible();
|
||||
await page.getByRole('button', { name: 'Ok', exact: true }).click();
|
||||
|
||||
// Navigate to the scatter plot and verify that the new SWG
|
||||
// appears in the elements pool and the old one is gone
|
||||
@ -81,27 +79,25 @@ test.describe('Scatter Plot', () => {
|
||||
|
||||
// Click the "Elements" tab
|
||||
await page.getByRole('tab', { name: 'Elements' }).click();
|
||||
await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeHidden();
|
||||
await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg2.name}`)).toBeVisible();
|
||||
await expect(page.getByLabel(`Preview ${swg1.name}`)).toBeHidden();
|
||||
await expect(page.getByLabel(`Preview ${swg2.name}`)).toBeVisible();
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
|
||||
// Right click on the new SWG in the elements pool and delete it
|
||||
await page.locator(`#inspector-elements-tree >> text=${swg2.name}`).click({
|
||||
await page.getByLabel(`Preview ${swg2.name}`).click({
|
||||
button: 'right'
|
||||
});
|
||||
await page.locator('li[title="Remove this object from its containing object."]').click();
|
||||
await page.getByLabel('Remove').click();
|
||||
|
||||
// Verify that the 'Remove object' confirmation modal appears and accept it
|
||||
await expect
|
||||
.soft(
|
||||
page.locator(
|
||||
'text=Warning! This action will remove this object. Are you sure you want to continue?'
|
||||
)
|
||||
await expect(
|
||||
page.getByText(
|
||||
'Warning! This action will remove this object. Are you sure you want to continue?'
|
||||
)
|
||||
.toBeVisible();
|
||||
await page.click('text=Ok');
|
||||
).toBeVisible();
|
||||
await page.getByRole('button', { name: 'Ok', exact: true }).click();
|
||||
|
||||
// Verify that the elements pool shows no elements
|
||||
await expect(page.locator('text="No contained elements"')).toBeVisible();
|
||||
await expect(page.getByText('No contained elements')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
@ -39,19 +39,23 @@ test.describe('Stacked Plot', () => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
stackedPlot = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Stacked Plot'
|
||||
type: 'Stacked Plot',
|
||||
name: 'Stacked Plot'
|
||||
});
|
||||
|
||||
swgA = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
name: 'Sine Wave Generator A',
|
||||
parent: stackedPlot.uuid
|
||||
});
|
||||
swgB = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
name: 'Sine Wave Generator B',
|
||||
parent: stackedPlot.uuid
|
||||
});
|
||||
swgC = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
name: 'Sine Wave Generator C',
|
||||
parent: stackedPlot.uuid
|
||||
});
|
||||
});
|
||||
@ -78,7 +82,7 @@ test.describe('Stacked Plot', () => {
|
||||
.getByRole('menuitem')
|
||||
.filter({ hasText: /Remove/ })
|
||||
.click();
|
||||
await page.getByRole('button').filter({ hasText: 'OK' }).click();
|
||||
await page.getByRole('button').filter({ hasText: 'Ok' }).click();
|
||||
|
||||
await expect(page.locator('#inspector-elements-tree .js-elements-pool__item')).toHaveCount(2);
|
||||
|
||||
@ -148,84 +152,126 @@ test.describe('Stacked Plot', () => {
|
||||
}) => {
|
||||
await page.goto(stackedPlot.url);
|
||||
|
||||
// Click on the 1st plot
|
||||
await page
|
||||
.getByLabel('Stacked Plot Item Sine Wave Generator A')
|
||||
.getByLabel('Plot Canvas')
|
||||
.click();
|
||||
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
|
||||
// Click on the 1st plot
|
||||
await page.locator(`[aria-label="Stacked Plot Item ${swgA.name}"] canvas`).nth(1).click();
|
||||
|
||||
// Assert that the inspector shows the Y Axis properties for swgA
|
||||
await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText(
|
||||
'Plot Series'
|
||||
);
|
||||
await expect(page.getByRole('heading', { name: 'Plot Series' })).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible();
|
||||
await expect(
|
||||
page.locator('[aria-label="Plot Series Properties"] .c-object-label')
|
||||
).toContainText(swgA.name);
|
||||
page.getByLabel('Inspector Views').getByText('Sine Wave Generator A', { exact: true })
|
||||
).toBeVisible();
|
||||
|
||||
// Click on the 2nd plot
|
||||
await page.locator(`[aria-label="Stacked Plot Item ${swgB.name}"] canvas`).nth(1).click();
|
||||
await page
|
||||
.getByLabel('Stacked Plot Item Sine Wave Generator B')
|
||||
.getByLabel('Plot Canvas')
|
||||
.click();
|
||||
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
|
||||
// Assert that the inspector shows the Y Axis properties for swgB
|
||||
await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText(
|
||||
'Plot Series'
|
||||
);
|
||||
await expect(page.getByRole('heading', { name: 'Plot Series' })).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible();
|
||||
await expect(
|
||||
page.locator('[aria-label="Plot Series Properties"] .c-object-label')
|
||||
).toContainText(swgB.name);
|
||||
page.getByLabel('Inspector Views').getByText('Sine Wave Generator B', { exact: true })
|
||||
).toBeVisible();
|
||||
|
||||
// Click on the 3rd plot
|
||||
await page.locator(`[aria-label="Stacked Plot Item ${swgC.name}"] canvas`).nth(1).click();
|
||||
await page
|
||||
.getByLabel('Stacked Plot Item Sine Wave Generator C')
|
||||
.getByLabel('Plot Canvas')
|
||||
.click();
|
||||
|
||||
// Assert that the inspector shows the Y Axis properties for swgC
|
||||
await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText(
|
||||
'Plot Series'
|
||||
);
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
|
||||
// Assert that the inspector shows the Y Axis properties for swgB
|
||||
await expect(page.getByRole('heading', { name: 'Plot Series' })).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible();
|
||||
await expect(
|
||||
page.locator('[aria-label="Plot Series Properties"] .c-object-label')
|
||||
).toContainText(swgC.name);
|
||||
page.getByLabel('Inspector Views').getByText('Sine Wave Generator C', { exact: true })
|
||||
).toBeVisible();
|
||||
|
||||
// Go into edit mode
|
||||
await page.getByLabel('Edit Object').click();
|
||||
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
// await page.getByRole('tab', { name: 'Config' }).click();
|
||||
|
||||
// Click on canvas for the 1st plot
|
||||
await page.locator(`[aria-label="Stacked Plot Item ${swgA.name}"]`).click();
|
||||
// Click on the 1st plot
|
||||
await page.getByLabel('Stacked Plot Item Sine Wave Generator A').click();
|
||||
|
||||
// Assert that the inspector shows the Y Axis properties for swgA
|
||||
await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText(
|
||||
'Plot Series'
|
||||
);
|
||||
await expect(page.getByRole('heading', { name: 'Plot Series' })).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible();
|
||||
await expect(
|
||||
page.locator('[aria-label="Plot Series Properties"] .c-object-label')
|
||||
).toContainText(swgA.name);
|
||||
page.getByLabel('Inspector Views').getByText('Sine Wave Generator A', { exact: true })
|
||||
).toBeVisible();
|
||||
|
||||
//Click on canvas for the 2nd plot
|
||||
await page.locator(`[aria-label="Stacked Plot Item ${swgB.name}"]`).click();
|
||||
// Click on the 2nd plot
|
||||
await page.getByLabel('Stacked Plot Item Sine Wave Generator B').click();
|
||||
|
||||
// Assert that the inspector shows the Y Axis properties for swgB
|
||||
await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText(
|
||||
'Plot Series'
|
||||
);
|
||||
await expect(page.getByRole('heading', { name: 'Plot Series' })).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible();
|
||||
await expect(
|
||||
page.locator('[aria-label="Plot Series Properties"] .c-object-label')
|
||||
).toContainText(swgB.name);
|
||||
page.getByLabel('Inspector Views').getByText('Sine Wave Generator B', { exact: true })
|
||||
).toBeVisible();
|
||||
|
||||
//Click on canvas for the 3rd plot
|
||||
await page.locator(`[aria-label="Stacked Plot Item ${swgC.name}"]`).click();
|
||||
// Click on the 3rd plot
|
||||
await page.getByLabel('Stacked Plot Item Sine Wave Generator C').click();
|
||||
|
||||
// Assert that the inspector shows the Y Axis properties for swgC
|
||||
await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText(
|
||||
'Plot Series'
|
||||
);
|
||||
await expect(page.getByRole('heading', { name: 'Plot Series' })).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible();
|
||||
await expect(
|
||||
page.locator('[aria-label="Plot Series Properties"] .c-object-label')
|
||||
).toContainText(swgC.name);
|
||||
page.getByLabel('Inspector Views').getByText('Sine Wave Generator C', { exact: true })
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('Changing properties of an immutable child plot are applied correctly', async ({ page }) => {
|
||||
await page.goto(stackedPlot.url);
|
||||
|
||||
// Go into edit mode
|
||||
await page.getByLabel('Edit Object').click();
|
||||
|
||||
// Click on canvas for the 1st plot
|
||||
await page.getByLabel(`Stacked Plot Item ${swgA.name}`).click();
|
||||
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
|
||||
// Expand config for the series
|
||||
await page.getByLabel('Expand Sine Wave Generator A Plot Series Options').click();
|
||||
|
||||
// turn off alarm markers
|
||||
await page.getByLabel('Alarm Markers').uncheck();
|
||||
|
||||
// save
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
// reload page and waitForPlotsToRender
|
||||
await page.reload();
|
||||
await waitForPlotsToRender(page);
|
||||
|
||||
// Click on canvas for the 1st plot
|
||||
await page.getByLabel(`Stacked Plot Item ${swgA.name}`).click();
|
||||
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
|
||||
// Expand config for the series
|
||||
await page.getByLabel('Expand Sine Wave Generator A Plot Series Options').click();
|
||||
|
||||
// Assert that alarm markers are still turned off
|
||||
await expect(
|
||||
page
|
||||
.getByTitle('Display markers visually denoting points in alarm.')
|
||||
.getByRole('cell', { name: 'Disabled' })
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('the legend toggles between aggregate and per child', async ({ page }) => {
|
||||
@ -238,7 +284,7 @@ test.describe('Stacked Plot', () => {
|
||||
|
||||
await page.getByRole('tab', { name: 'Config' }).click();
|
||||
|
||||
let legendProperties = await page.locator('[aria-label="Legend Properties"]');
|
||||
const legendProperties = page.getByLabel('Legend Properties');
|
||||
await legendProperties.locator('[title="Display legends per sub plot."]~div input').uncheck();
|
||||
|
||||
await assertAggregateLegendIsVisible(page);
|
||||
@ -317,11 +363,9 @@ async function assertAggregateLegendIsVisible(page) {
|
||||
// Wait for plot series data to load
|
||||
await waitForPlotsToRender(page);
|
||||
// Wait for plot legend to be shown
|
||||
await page.waitForSelector('.js-stacked-plot-legend', { state: 'attached' });
|
||||
await expect(page.locator('.js-stacked-plot-legend')).toBeVisible();
|
||||
// There should be 3 legend items
|
||||
expect(
|
||||
await page
|
||||
.locator('.js-stacked-plot-legend .c-plot-legend__wrapper div.plot-legend-item')
|
||||
.count()
|
||||
).toBe(3);
|
||||
await expect(
|
||||
page.locator('.js-stacked-plot-legend .c-plot-legend__wrapper div.plot-legend-item')
|
||||
).toHaveCount(3);
|
||||
}
|
||||
|
@ -89,7 +89,7 @@ test.describe('Plot Tagging', () => {
|
||||
await setRealTimeMode(page);
|
||||
|
||||
// Search for Science Tag
|
||||
await page.getByRole('searchbox', { name: 'Search Input' });
|
||||
await page.getByRole('searchbox', { name: 'Search Input' }).click();
|
||||
await page.getByRole('searchbox', { name: 'Search Input' }).fill('sc');
|
||||
|
||||
// Click on the search object result
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2023, United States Government
|
||||
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
@ -42,19 +42,21 @@ test.describe('Reload action', () => {
|
||||
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
parent: alphaTable.uuid,
|
||||
customParameters: {
|
||||
'[aria-label="Data Rate (hz)"]': '0.001'
|
||||
}
|
||||
parent: alphaTable.uuid
|
||||
});
|
||||
await page.getByLabel('More actions').click();
|
||||
await page.getByRole('menuitem', { name: /Edit Properties/ }).click();
|
||||
await page.getByLabel('Data Rate (hz)', { exact: true }).fill('0.001');
|
||||
await page.getByLabel('Save').click();
|
||||
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
parent: betaTable.uuid,
|
||||
customParameters: {
|
||||
'[aria-label="Data Rate (hz)"]': '0.001'
|
||||
}
|
||||
parent: betaTable.uuid
|
||||
});
|
||||
await page.getByLabel('More actions').click();
|
||||
await page.getByRole('menuitem', { name: /Edit Properties/ }).click();
|
||||
await page.getByLabel('Data Rate (hz)', { exact: true }).fill('0.001');
|
||||
await page.getByLabel('Save').click();
|
||||
|
||||
await page.goto(displayLayout.url);
|
||||
|
||||
@ -63,20 +65,26 @@ test.describe('Reload action', () => {
|
||||
|
||||
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
|
||||
.getByLabel('Main Tree')
|
||||
.getByLabel(`Preview ${alphaTable.name}`)
|
||||
.dragTo(page.getByLabel('Layout Grid'), {
|
||||
targetPosition: { x: 0, y: 0 }
|
||||
});
|
||||
|
||||
await page.dragAndDrop(`text='Beta Table'`, '.l-layout__grid-holder', {
|
||||
targetPosition: { x: 0, y: 250 }
|
||||
});
|
||||
await page
|
||||
.getByLabel('Main Tree')
|
||||
.getByLabel(`Preview ${betaTable.name}`)
|
||||
.dragTo(page.getByLabel('Layout Grid'), {
|
||||
targetPosition: { x: 0, y: 250 }
|
||||
});
|
||||
|
||||
await page.locator('button[title="Save"]').click();
|
||||
await page.getByLabel('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
|
||||
const beforeReloadAlphaTelemetryValue = page
|
||||
.getByLabel('Alpha Table table content')
|
||||
.getByLabel('wavelengths table cell')
|
||||
.first()
|
||||
|
@ -0,0 +1,163 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
/*
|
||||
This test suite is dedicated to tests which verify the basic operations surrounding conditionSets and styling
|
||||
*/
|
||||
|
||||
import {
|
||||
createDomainObjectWithDefaults,
|
||||
linkParameterToObject,
|
||||
setRealTimeMode
|
||||
} from '../../../../appActions.js';
|
||||
import { MISSION_TIME } from '../../../../constants.js';
|
||||
import { expect, test } from '../../../../pluginFixtures.js';
|
||||
|
||||
test.describe('Conditionally Styling, using a Condition Set', () => {
|
||||
let stateGenerator;
|
||||
let conditionSet;
|
||||
let displayLayout;
|
||||
const STATE_CHANGE_INTERVAL = '1';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Install the clock and set the time to the mission time such that the state generator will be controllable
|
||||
await page.clock.install({ time: MISSION_TIME });
|
||||
await page.clock.resume();
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
// Create Condition Set, State Generator, and Display Layout
|
||||
conditionSet = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Condition Set',
|
||||
name: 'Test Condition Set'
|
||||
});
|
||||
stateGenerator = await createDomainObjectWithDefaults(page, {
|
||||
type: 'State Generator',
|
||||
name: 'One Second State Generator'
|
||||
});
|
||||
// edit the state generator to have a 1 second update rate
|
||||
await page.getByTitle('More actions').click();
|
||||
await page.getByRole('menuitem', { name: 'Edit Properties...' }).click();
|
||||
await page.getByLabel('State Duration (seconds)', { exact: true }).fill(STATE_CHANGE_INTERVAL);
|
||||
await page.getByLabel('Save').click();
|
||||
|
||||
displayLayout = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Display Layout',
|
||||
name: 'Test Display Layout'
|
||||
});
|
||||
});
|
||||
|
||||
test('Conditional styling, using a Condition Set, will style correctly based on the output @clock', async ({
|
||||
page
|
||||
}) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/7840'
|
||||
});
|
||||
|
||||
// set up the condition set to use the state generator
|
||||
await page.goto(conditionSet.url, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Add the State Generator to the Condition Set by dragging from the main tree
|
||||
await page.getByLabel('Show selected item in tree').click();
|
||||
await page
|
||||
.getByRole('tree', {
|
||||
name: 'Main Tree'
|
||||
})
|
||||
.getByRole('treeitem', {
|
||||
name: stateGenerator.name
|
||||
})
|
||||
.dragTo(page.locator('#conditionCollection'));
|
||||
|
||||
// Add the state generator to the first criterion such that there is a condition named 'OFF' when the state generator is off
|
||||
await page.getByLabel('Add Condition').click();
|
||||
await page
|
||||
.getByLabel('Criterion Telemetry Selection')
|
||||
.selectOption({ label: stateGenerator.name });
|
||||
await page.getByLabel('Criterion Metadata Selection').selectOption({ label: 'State' });
|
||||
await page.getByLabel('Criterion Comparison Selection').selectOption({ label: 'is' });
|
||||
await page.getByLabel('Condition Name Input').first().fill('OFF');
|
||||
await page.getByLabel('Save').click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
await linkParameterToObject(page, stateGenerator.name, displayLayout.name);
|
||||
|
||||
//Add a box to the display layout
|
||||
await page.goto(displayLayout.url, { waitUntil: 'domcontentloaded' });
|
||||
await page.getByLabel('Edit Object').click();
|
||||
|
||||
//Add a box to the display layout and move it to the right
|
||||
//TEMP: Click the layout such that the state generator is deselected
|
||||
await page.getByLabel('Test Display Layout Layout Grid').locator('div').nth(1).click();
|
||||
await page.getByLabel('Add Drawing Object').click();
|
||||
await page.getByText('Box').click();
|
||||
await page.getByLabel('X:').click();
|
||||
await page.getByLabel('X:').fill('10');
|
||||
await page.getByLabel('X:').press('Enter');
|
||||
|
||||
// set up conditional styling such that the box is red when the state generator condition is 'OFF'
|
||||
await page.getByRole('tab', { name: 'Styles' }).click();
|
||||
await page.getByRole('button', { name: 'Use Conditional Styling...' }).click();
|
||||
await page.getByLabel('Modal Overlay').getByLabel('Expand My Items folder').click();
|
||||
await page.getByLabel('Modal Overlay').getByLabel(`Preview ${conditionSet.name}`).click();
|
||||
await page.getByText('Ok').click();
|
||||
await page.getByLabel('Set background color').first().click();
|
||||
await page.getByLabel('#ff0000').click();
|
||||
await page.getByLabel('Save', { exact: true }).click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
await setRealTimeMode(page);
|
||||
|
||||
//Pause at a time when the state generator is 'OFF' which is 20 minutes in the future
|
||||
await page.clock.pauseAt(new Date(MISSION_TIME + 1200000));
|
||||
|
||||
const redBG = 'background-color: rgb(255, 0, 0);';
|
||||
const defaultBG = 'background-color: rgb(102, 102, 102);';
|
||||
const textElement = page.getByLabel('Alpha-numeric telemetry value').locator('div:first-child');
|
||||
const styledElement = page.getByLabel('Box', { exact: true });
|
||||
|
||||
await page.clock.resume();
|
||||
|
||||
// Check if the style is red when text is 'OFF'
|
||||
await expect(textElement).toHaveText('OFF');
|
||||
await waitForStyleChange(styledElement, redBG);
|
||||
|
||||
// Fast forward to the next state change
|
||||
await page.clock.fastForward(STATE_CHANGE_INTERVAL * 1000);
|
||||
|
||||
// Check if the style is not red when text is 'ON'
|
||||
await expect(textElement).toHaveText('ON');
|
||||
await waitForStyleChange(styledElement, defaultBG);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Wait for the style of an element to change to the expected style.
|
||||
* @param {import('@playwright/test').Locator} element - The element to check.
|
||||
* @param {string} expectedStyle - The expected style to wait for.
|
||||
* @param {number} timeout - The timeout in milliseconds.
|
||||
*/
|
||||
async function waitForStyleChange(element, expectedStyle, timeout = 0) {
|
||||
await expect(async () => {
|
||||
const style = await element.getAttribute('style');
|
||||
|
||||
// eslint-disable-next-line playwright/prefer-web-first-assertions
|
||||
expect(style).toBe(expectedStyle);
|
||||
}).toPass({ timeout: 1000 }); // timeout allows for the style to be applied
|
||||
}
|
@ -0,0 +1,114 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2024, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
import {
|
||||
createDomainObjectWithDefaults,
|
||||
navigateToObjectWithRealTime
|
||||
} from '../../../../../appActions.js';
|
||||
import { expect, test } from '../../../../../pluginFixtures.js';
|
||||
|
||||
const TINY_IMAGE_BASE64 =
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII';
|
||||
|
||||
test.describe('Display Layout Conditional Styling', () => {
|
||||
test.use({
|
||||
storageState: fileURLToPath(
|
||||
new URL('../../../../../test-data/condition_set_storage.json', import.meta.url)
|
||||
)
|
||||
});
|
||||
|
||||
let displayLayout;
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
displayLayout = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Display Layout',
|
||||
name: 'Test Display Layout'
|
||||
});
|
||||
});
|
||||
|
||||
test('Image Drawing Object can have visibility toggled conditionally', async ({ page }) => {
|
||||
await page.getByLabel('Edit Object').click();
|
||||
|
||||
// Add Image Drawing Object to the layout
|
||||
await page.getByLabel('Add Drawing Object').click();
|
||||
await page.getByLabel('Image').click();
|
||||
await page.getByLabel('Image URL').fill(TINY_IMAGE_BASE64);
|
||||
await page.getByText('Ok').click();
|
||||
|
||||
// Use the "Test Condition Set" for conditional styling on the image
|
||||
await page.getByRole('tab', { name: 'Styles' }).click();
|
||||
await page.getByRole('button', { name: 'Use Conditional Styling...' }).click();
|
||||
await page.getByLabel('Modal Overlay').getByLabel('Expand My Items folder').click();
|
||||
await page.getByLabel('Modal Overlay').getByLabel('Preview Test Condition Set').click();
|
||||
await page.getByText('Ok').click();
|
||||
|
||||
// Set the image to be hidden when the condition is met
|
||||
await page.getByTitle('Visible').first().click();
|
||||
await page.getByLabel('Save Style').first().click();
|
||||
await page.getByLabel('Save', { exact: true }).click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
// Switch to real-time mode and verify that the image toggles visibility
|
||||
await navigateToObjectWithRealTime(page, displayLayout.url);
|
||||
await expect(page.getByLabel('Image View')).toBeVisible();
|
||||
await expect(page.getByLabel('Image View')).toBeHidden();
|
||||
|
||||
// Reload the page and verify that the image toggles visibility
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
await expect(page.getByLabel('Image View')).toBeVisible();
|
||||
await expect(page.getByLabel('Image View')).toBeHidden();
|
||||
});
|
||||
|
||||
test('Alphanumeric object can have visibility toggled conditionally', async ({ page }) => {
|
||||
await page.getByLabel('Edit Object').click();
|
||||
|
||||
// Add Alphanumeric Object to the layout
|
||||
await page.getByLabel('Expand My Items folder').click();
|
||||
await page.getByLabel('Expand Test Condition Set').click();
|
||||
await page.getByLabel('Preview VIPER Rover Heading').dragTo(page.getByLabel('Layout Grid'));
|
||||
|
||||
// Use the "Test Condition Set" for conditional styling on the alphanumeric
|
||||
await page.getByRole('tab', { name: 'Styles' }).click();
|
||||
await page.getByRole('button', { name: 'Use Conditional Styling...' }).click();
|
||||
await page.getByLabel('Modal Overlay').getByLabel('Expand My Items folder').click();
|
||||
await page.getByLabel('Modal Overlay').getByLabel('Preview Test Condition Set').click();
|
||||
await page.getByText('Ok').click();
|
||||
|
||||
// Set the alphanumeric to be hidden when the condition is met
|
||||
await page.getByTitle('Visible').first().click();
|
||||
await page.getByLabel('Save Style').first().click();
|
||||
await page.getByLabel('Save', { exact: true }).click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
// Switch to real-time mode and verify that the image toggles visibility
|
||||
await navigateToObjectWithRealTime(page, displayLayout.url);
|
||||
await expect(page.getByLabel('Alpha-numeric telemetry', { exact: true })).toBeVisible();
|
||||
await expect(page.getByLabel('Alpha-numeric telemetry', { exact: true })).toBeHidden();
|
||||
|
||||
// Reload the page and verify that the alphanumeric toggles visibility
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
await expect(page.getByLabel('Alpha-numeric telemetry', { exact: true })).toBeVisible();
|
||||
await expect(page.getByLabel('Alpha-numeric telemetry', { exact: true })).toBeHidden();
|
||||
});
|
||||
});
|
@ -466,7 +466,7 @@ test.describe('Flexible Layout styling', () => {
|
||||
page.getByLabel('Flexible Layout Column')
|
||||
);
|
||||
await page.getByLabel('Cancel Editing').click();
|
||||
await page.getByRole('button', { name: 'OK', exact: true }).click();
|
||||
await page.getByRole('button', { name: 'Ok', exact: true }).click();
|
||||
await checkStyles(
|
||||
hexToRGB(defaultBorderTargetColor),
|
||||
NO_STYLE_RGBA,
|
||||
|
@ -45,6 +45,8 @@ const setFontFamily = '"Andale Mono", sans-serif';
|
||||
|
||||
test.describe('Stacked Plot styling', () => {
|
||||
let stackedPlot;
|
||||
let overlayPlot1;
|
||||
let overlayPlot2;
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
@ -54,17 +56,30 @@ test.describe('Stacked Plot styling', () => {
|
||||
name: 'StackedPlot1'
|
||||
});
|
||||
|
||||
// create two overlay plots
|
||||
overlayPlot1 = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Overlay Plot',
|
||||
name: 'Overlay Plot 1',
|
||||
parent: stackedPlot.uuid
|
||||
});
|
||||
|
||||
overlayPlot2 = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Overlay Plot',
|
||||
name: 'Overlay Plot 2',
|
||||
parent: stackedPlot.uuid
|
||||
});
|
||||
|
||||
// Create two SWGs and attach them to the Stacked Plot
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
name: 'Sine Wave Generator 1',
|
||||
parent: stackedPlot.uuid
|
||||
parent: overlayPlot1.uuid
|
||||
});
|
||||
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
name: 'Sine Wave Generator 2',
|
||||
parent: stackedPlot.uuid
|
||||
parent: overlayPlot2.uuid
|
||||
});
|
||||
});
|
||||
|
||||
@ -138,21 +153,21 @@ test.describe('Stacked Plot styling', () => {
|
||||
NO_STYLE_RGBA,
|
||||
NO_STYLE_RGBA,
|
||||
hexToRGB(setTextColor),
|
||||
page.getByLabel('Stacked Plot Item Sine Wave Generator 1')
|
||||
page.getByLabel('Stacked Plot Item Overlay Plot 1')
|
||||
);
|
||||
|
||||
await checkStyles(
|
||||
NO_STYLE_RGBA,
|
||||
NO_STYLE_RGBA,
|
||||
hexToRGB(setTextColor),
|
||||
page.getByLabel('Stacked Plot Item Sine Wave Generator 2')
|
||||
page.getByLabel('Stacked Plot Item Overlay Plot 2')
|
||||
);
|
||||
|
||||
await checkFontStyles(
|
||||
setFontSize,
|
||||
setFontWeight,
|
||||
setFontFamily,
|
||||
page.getByLabel('Stacked Plot Item Sine Wave Generator 1')
|
||||
page.getByLabel('Stacked Plot Item Overlay Plot 1')
|
||||
);
|
||||
});
|
||||
|
||||
@ -169,19 +184,19 @@ test.describe('Stacked Plot styling', () => {
|
||||
|
||||
await page.getByRole('tab', { name: 'Styles' }).click();
|
||||
|
||||
//Check default styles for SWG1 and SWG2
|
||||
//Check default styles for overlayPlot1 and overlayPlot2
|
||||
await checkStyles(
|
||||
NO_STYLE_RGBA,
|
||||
NO_STYLE_RGBA,
|
||||
hexToRGB(defaultTextColor),
|
||||
page.getByLabel('Stacked Plot Item Sine Wave Generator 1')
|
||||
page.getByLabel('Stacked Plot Item Overlay Plot 1')
|
||||
);
|
||||
|
||||
await checkStyles(
|
||||
NO_STYLE_RGBA,
|
||||
NO_STYLE_RGBA,
|
||||
hexToRGB(defaultTextColor),
|
||||
page.getByLabel('Stacked Plot Item Sine Wave Generator 2')
|
||||
page.getByLabel('Stacked Plot Item Overlay Plot 2')
|
||||
);
|
||||
|
||||
// Set styles using setStyles function on StackedPlot1 but not StackedPlot2
|
||||
@ -190,11 +205,11 @@ test.describe('Stacked Plot styling', () => {
|
||||
setBorderColor,
|
||||
setBackgroundColor,
|
||||
setTextColor,
|
||||
page.getByLabel('Stacked Plot Item Sine Wave Generator 1')
|
||||
page.getByLabel('Stacked Plot Item Overlay Plot 1')
|
||||
);
|
||||
|
||||
//Set Font Styles on SWG1 but not SWG2
|
||||
await page.getByLabel('Stacked Plot Item Sine Wave Generator 1').click();
|
||||
await page.getByLabel('Stacked Plot Item Overlay Plot 1').click();
|
||||
//Set Font Size to 72
|
||||
await page.getByLabel('Set Font Size').click();
|
||||
await page.getByRole('menuitem', { name: '72px' }).click();
|
||||
|
@ -57,7 +57,7 @@ test.describe('Tabs View', () => {
|
||||
await page.goto(tabsView.url);
|
||||
|
||||
// select first tab
|
||||
await page.getByLabel(`${table.name} tab`, { exact: true }).click();
|
||||
await page.getByLabel(`${table.name} tab - selected`, { exact: true }).click();
|
||||
// ensure table header visible
|
||||
await expect(page.getByRole('searchbox', { name: 'message filter input' })).toBeVisible();
|
||||
|
||||
@ -92,6 +92,38 @@ test.describe('Tabs View', () => {
|
||||
// no canvas (i.e., sine wave generator) in the document should be visible
|
||||
await expect(page.locator('canvas[id=webglContext]')).toBeHidden();
|
||||
});
|
||||
|
||||
test('Changing the displayed tab should not be persisted if the view is locked', async ({
|
||||
page
|
||||
}) => {
|
||||
await page.goto(tabsView.url);
|
||||
//lock the view
|
||||
await page.getByLabel('Unlocked for editing, click to lock.', { exact: true }).click();
|
||||
// get the initial tab index
|
||||
const initialTab = page.getByLabel(/- selected/);
|
||||
// switch to a different tab in the view
|
||||
const swgTab = page.getByLabel(`${sineWaveGenerator.name} tab`, { exact: true });
|
||||
await swgTab.click();
|
||||
await page.getByLabel(`${sineWaveGenerator.name} Object View`).isVisible();
|
||||
// navigate away from the tabbed view and back
|
||||
await page.getByRole('treeitem', { name: 'My Items' }).click();
|
||||
await page.goto(tabsView.url);
|
||||
// check that the initial tab is displayed
|
||||
const lockedSelectedTab = page.getByLabel(/- selected/);
|
||||
await expect(lockedSelectedTab).toHaveText(await initialTab.textContent());
|
||||
|
||||
//unlock the view
|
||||
await page.getByLabel('Locked for editing. Click to unlock.', { exact: true }).click();
|
||||
// switch to a different tab in the view
|
||||
await swgTab.click();
|
||||
await page.getByLabel(`${sineWaveGenerator.name} Object View`).isVisible();
|
||||
// navigate away from the tabbed view and back
|
||||
await page.getByRole('treeitem', { name: 'My Items' }).click();
|
||||
await page.goto(tabsView.url);
|
||||
// check that the newly selected tab is displayed
|
||||
const unlockedSelectedTab = page.getByLabel(/- selected/);
|
||||
await expect(unlockedSelectedTab).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Tabs View CRUD', () => {
|
||||
@ -114,8 +146,8 @@ test.describe('Tabs View CRUD', () => {
|
||||
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 expect(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.getByLabel('Eager Load Tabs')).toBeChecked();
|
||||
});
|
||||
});
|
||||
|
@ -22,8 +22,8 @@
|
||||
|
||||
import {
|
||||
createDomainObjectWithDefaults,
|
||||
setTimeConductorBounds,
|
||||
setTimeConductorMode
|
||||
navigateToObjectWithRealTime,
|
||||
setTimeConductorBounds
|
||||
} from '../../../../appActions.js';
|
||||
import { expect, test } from '../../../../pluginFixtures.js';
|
||||
|
||||
@ -39,12 +39,52 @@ test.describe('Telemetry Table', () => {
|
||||
type: 'Sine Wave Generator',
|
||||
parent: table.uuid
|
||||
});
|
||||
await page.goto(table.url);
|
||||
await setTimeConductorMode(page, false);
|
||||
await navigateToObjectWithRealTime(page, table.url);
|
||||
const rows = page.getByLabel('table content').getByLabel('Table Row');
|
||||
await expect(rows).toHaveCount(50);
|
||||
});
|
||||
|
||||
test('on load, auto scrolls to top for descending, and to bottom for ascending', async ({
|
||||
page
|
||||
}) => {
|
||||
const sineWaveGenerator = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
parent: table.uuid
|
||||
});
|
||||
|
||||
// verify in telemetry table object view
|
||||
await navigateToObjectWithRealTime(page, table.url);
|
||||
|
||||
expect(await getScrollPosition(page)).toBe(0);
|
||||
|
||||
// verify in telemetry table view
|
||||
await page.goto(sineWaveGenerator.url);
|
||||
await page.getByLabel('Open the View Switcher Menu').click();
|
||||
await page.getByText('Telemetry Table', { exact: true }).click();
|
||||
|
||||
expect(await getScrollPosition(page)).toBe(0);
|
||||
|
||||
// navigate back to table
|
||||
await page.goto(table.url);
|
||||
|
||||
// go into edit mode
|
||||
await page.getByLabel('Edit Object').click();
|
||||
|
||||
// change sort direction
|
||||
await page.locator('thead div').filter({ hasText: 'Time' }).click();
|
||||
|
||||
// save view
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
|
||||
|
||||
// navigate away and back
|
||||
await page.goto(sineWaveGenerator.url);
|
||||
await page.goto(table.url);
|
||||
|
||||
// verify scroll position
|
||||
expect(await getScrollPosition(page, false)).toBeLessThan(1);
|
||||
});
|
||||
|
||||
test('unpauses and filters data when paused by button and user changes bounds', async ({
|
||||
page
|
||||
}) => {
|
||||
@ -72,13 +112,15 @@ test.describe('Telemetry Table', () => {
|
||||
|
||||
// Subtract 5 minutes from the current end bound datetime and set it
|
||||
// Bring up the time conductor popup
|
||||
let endDate = await page.locator('[aria-label="End bounds"]').textContent();
|
||||
endDate = new Date(endDate);
|
||||
let endTimeStamp = await page.getByLabel('End bounds').textContent();
|
||||
endTimeStamp = new Date(endTimeStamp);
|
||||
|
||||
endDate.setUTCMinutes(endDate.getUTCMinutes() - 5);
|
||||
endDate = endDate.toISOString().replace(/T/, ' ');
|
||||
endTimeStamp.setUTCMinutes(endTimeStamp.getUTCMinutes() - 5);
|
||||
const endDate = endTimeStamp.toISOString().split('T')[0];
|
||||
const milliseconds = endTimeStamp.getMilliseconds();
|
||||
const endTime = endTimeStamp.toISOString().split('T')[1].replace(`.${milliseconds}Z`, '');
|
||||
|
||||
await setTimeConductorBounds(page, undefined, endDate);
|
||||
await setTimeConductorBounds(page, { endDate, endTime });
|
||||
|
||||
await expect(tableWrapper).not.toHaveClass(/is-paused/);
|
||||
|
||||
@ -91,7 +133,7 @@ test.describe('Telemetry Table', () => {
|
||||
|
||||
// Verify that it is <= our new end bound
|
||||
const latestMilliseconds = Date.parse(latestTelemetryDate);
|
||||
const endBoundMilliseconds = Date.parse(endDate);
|
||||
const endBoundMilliseconds = Date.parse(endTimeStamp);
|
||||
expect(latestMilliseconds).toBeLessThanOrEqual(endBoundMilliseconds);
|
||||
});
|
||||
|
||||
@ -114,8 +156,7 @@ test.describe('Telemetry Table', () => {
|
||||
expect(cells.length).toBeGreaterThan(1);
|
||||
// ensure the text content of each cell contains the search term
|
||||
for (const cell of cells) {
|
||||
const text = await cell.textContent();
|
||||
expect(text).toContain('Roger');
|
||||
await expect(cell).toHaveText(/Roger/);
|
||||
}
|
||||
|
||||
await page.getByRole('searchbox', { name: 'message filter input' }).click();
|
||||
@ -126,15 +167,14 @@ test.describe('Telemetry Table', () => {
|
||||
.getByText(/Dodger/)
|
||||
.all();
|
||||
// ensure we've got more than one cell
|
||||
expect(cells.length).toBe(0);
|
||||
expect(cells).toHaveLength(0);
|
||||
// ensure the text content of each cell contains the search term
|
||||
for (const cell of cells) {
|
||||
const text = await cell.textContent();
|
||||
expect(text).not.toContain('Dodger');
|
||||
await expect(cell).not.toHaveText(/Dodger/);
|
||||
}
|
||||
|
||||
// Click pause button
|
||||
await page.click('button[title="Pause"]');
|
||||
await page.getByLabel('Pause').click();
|
||||
});
|
||||
|
||||
test('Supports filtering using Regex', async ({ page }) => {
|
||||
@ -160,8 +200,7 @@ test.describe('Telemetry Table', () => {
|
||||
expect(cells.length).toBeGreaterThan(1);
|
||||
// ensure the text content of each cell contains the search term
|
||||
for (const cell of cells) {
|
||||
const text = await cell.textContent();
|
||||
expect(text).toContain('Roger');
|
||||
await expect(cell).toHaveText(/Roger/);
|
||||
}
|
||||
|
||||
await page.getByRole('searchbox', { name: 'message filter input' }).click();
|
||||
@ -172,14 +211,51 @@ test.describe('Telemetry Table', () => {
|
||||
.getByText(/Dodger/)
|
||||
.all();
|
||||
// ensure we've got more than one cell
|
||||
expect(cells.length).toBe(0);
|
||||
expect(cells).toHaveLength(0);
|
||||
// ensure the text content of each cell contains the search term
|
||||
for (const cell of cells) {
|
||||
const text = await cell.textContent();
|
||||
expect(text).not.toContain('Dodger');
|
||||
await expect(cell).not.toHaveText(/Dodger/);
|
||||
}
|
||||
|
||||
// Click pause button
|
||||
await page.click('button[title="Pause"]');
|
||||
await page.getByLabel('Pause').click();
|
||||
});
|
||||
});
|
||||
|
||||
async function getScrollPosition(page, top = true) {
|
||||
const tableBody = page.locator('.c-table__body-w');
|
||||
|
||||
// Wait for the scrollbar to appear
|
||||
await tableBody.evaluate((node) => {
|
||||
return new Promise((resolve) => {
|
||||
function checkScroll() {
|
||||
if (node.scrollHeight > node.clientHeight) {
|
||||
resolve();
|
||||
} else {
|
||||
setTimeout(checkScroll, 100);
|
||||
}
|
||||
}
|
||||
checkScroll();
|
||||
});
|
||||
});
|
||||
|
||||
// make sure there are rows
|
||||
const rows = page.getByLabel('table content').getByLabel('Table Row');
|
||||
await rows.first().waitFor();
|
||||
|
||||
// Using this to allow for rows to come and go, so we can truly test the scroll position
|
||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
const { scrollTop, clientHeight, scrollHeight } = await tableBody.evaluate((node) => ({
|
||||
scrollTop: node.scrollTop,
|
||||
clientHeight: node.clientHeight,
|
||||
scrollHeight: node.scrollHeight
|
||||
}));
|
||||
|
||||
if (top) {
|
||||
return scrollTop;
|
||||
} else {
|
||||
return Math.abs(scrollHeight - (scrollTop + clientHeight));
|
||||
}
|
||||
}
|
||||
|
@ -22,16 +22,18 @@
|
||||
|
||||
import {
|
||||
createDomainObjectWithDefaults,
|
||||
setIndependentTimeConductorBounds
|
||||
navigateToObjectWithFixedTimeBounds,
|
||||
setFixedIndependentTimeConductorBounds
|
||||
} from '../../../../appActions.js';
|
||||
import { expect, test } from '../../../../pluginFixtures.js';
|
||||
|
||||
const FIXED_TIME =
|
||||
'./#/browse/mine?tc.mode=fixed&tc.startBound=1693592063607&tc.endBound=1693593893607&tc.timeSystem=utc&view=grid&hideInspector=true&hideTree=true';
|
||||
const FIXED_TIME_URL = './#/browse/mine';
|
||||
|
||||
test.describe('Datepicker operations', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(FIXED_TIME);
|
||||
await navigateToObjectWithFixedTimeBounds(page, FIXED_TIME_URL, 1693592063607, 1693593893607);
|
||||
});
|
||||
|
||||
test('Verify that user can use the datepicker in the TC', async ({ page }) => {
|
||||
await page.getByLabel('Time Conductor Mode').click();
|
||||
// Click on the date picker that is left-most on the screen
|
||||
@ -42,12 +44,13 @@ test.describe('Datepicker operations', () => {
|
||||
// Expect datepicker to close and time conductor date setting to be changed
|
||||
await expect(page.getByRole('dialog')).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('Verify that user can use the datepicker in the ITC', async ({ page }) => {
|
||||
const createdTimeList = await createDomainObjectWithDefaults(page, { type: 'Time List' });
|
||||
|
||||
await page.goto(createdTimeList.url, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
await setIndependentTimeConductorBounds(page, {
|
||||
await setFixedIndependentTimeConductorBounds(page, {
|
||||
start: '2024-11-12 19:11:11.000Z',
|
||||
end: '2024-11-12 20:11:11.000Z'
|
||||
});
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user