Compare commits
64 Commits
omm-r5.1.0
...
release/2.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a8cbc778c | ||
|
|
44e603cc00 | ||
|
|
496ab4d5a3 | ||
|
|
aad9e51262 | ||
|
|
ba4353aacb | ||
|
|
9f079255f1 | ||
|
|
f5eacc504b | ||
|
|
26fa1653e3 | ||
|
|
b7c68f715b | ||
|
|
549a579bf3 | ||
|
|
fe677fa359 | ||
|
|
1bbc3789ec | ||
|
|
636849885b | ||
|
|
6f2b20eee9 | ||
|
|
e38821cc1f | ||
|
|
4345d216f7 | ||
|
|
84a12c7833 | ||
|
|
ad8445114f | ||
|
|
bcd50dfa35 | ||
|
|
a798ddf05e | ||
|
|
7af7e68779 | ||
|
|
c200999659 | ||
|
|
ddeeff4822 | ||
|
|
5610846147 | ||
|
|
88fde47932 | ||
|
|
2a0faba35f | ||
|
|
a47abf5f96 | ||
|
|
968eee6698 | ||
|
|
43d56a68bb | ||
|
|
f055a8a0c7 | ||
|
|
2820237d60 | ||
|
|
dbdc9bb4e2 | ||
|
|
a9a98380f2 | ||
|
|
e3ab085dd5 | ||
|
|
519135527b | ||
|
|
fc37f6e05b | ||
|
|
ab1df89396 | ||
|
|
9ee5ab96f3 | ||
|
|
8b2c6e3fb3 | ||
|
|
b8b0a08eeb | ||
|
|
633b6be2fd | ||
|
|
4963aff8a0 | ||
|
|
6786be54fa | ||
|
|
b081389e68 | ||
|
|
7a3ec3a241 | ||
|
|
c0c383bf18 | ||
|
|
fe1c99de12 | ||
|
|
2e60da0401 | ||
|
|
bc3a5408b4 | ||
|
|
344bf8eed3 | ||
|
|
cbb3368937 | ||
|
|
b7a671d392 | ||
|
|
4f10a93ef5 | ||
|
|
f8186e4b4e | ||
|
|
4e0c364d89 | ||
|
|
f3bed9c651 | ||
|
|
4d93907d58 | ||
|
|
6f656a6783 | ||
|
|
767fb6c5fd | ||
|
|
b0a0b4bb58 | ||
|
|
340f4a9e79 | ||
|
|
3007b28b0f | ||
|
|
20789601b4 | ||
|
|
a56cfed732 |
@@ -2,11 +2,15 @@ version: 2.1
|
||||
executors:
|
||||
pw-focal-development:
|
||||
docker:
|
||||
- image: mcr.microsoft.com/playwright:v1.29.0-focal
|
||||
- image: mcr.microsoft.com/playwright:v1.32.3-focal
|
||||
environment:
|
||||
NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed
|
||||
PERCY_POSTINSTALL_BROWSER: 'true' # Needed to store the percy browser in cache deps
|
||||
PERCY_LOGLEVEL: 'debug' # Enable DEBUG level logging for Percy (Issue: https://github.com/nasa/openmct/issues/5742)
|
||||
ubuntu:
|
||||
machine:
|
||||
image: ubuntu-2204:current
|
||||
docker_layer_caching: true
|
||||
parameters:
|
||||
BUST_CACHE:
|
||||
description: "Set this with the CircleCI UI Trigger Workflow button (boolean = true) to bust the cache!"
|
||||
@@ -23,9 +27,8 @@ commands:
|
||||
- restore_cache_cmd:
|
||||
node-version: << parameters.node-version >>
|
||||
- node/install:
|
||||
install-npm: true
|
||||
node-version: << parameters.node-version >>
|
||||
- run: npm install --prefer-offline --no-audit --progress=false
|
||||
- run: npm install --no-audit --progress=false
|
||||
restore_cache_cmd:
|
||||
description: "Custom command for restoring cache with the ability to bust cache. When BUST_CACHE is set to true, jobs will not restore cache"
|
||||
parameters:
|
||||
@@ -37,7 +40,7 @@ commands:
|
||||
equal: [false, << pipeline.parameters.BUST_CACHE >> ]
|
||||
steps:
|
||||
- restore_cache:
|
||||
key: deps-{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}-{{ checksum ".circleci/config.yml" }}
|
||||
key: deps--{{ arch }}--{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}-{{ checksum ".circleci/config.yml" }}
|
||||
save_cache_cmd:
|
||||
description: "Custom command for saving cache."
|
||||
parameters:
|
||||
@@ -45,7 +48,7 @@ commands:
|
||||
type: string
|
||||
steps:
|
||||
- save_cache:
|
||||
key: deps-{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}-{{ checksum ".circleci/config.yml" }}
|
||||
key: deps--{{ arch }}--{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}-{{ checksum ".circleci/config.yml" }}
|
||||
paths:
|
||||
- ~/.npm
|
||||
- node_modules
|
||||
@@ -53,8 +56,8 @@ commands:
|
||||
description: "Track important packages and files"
|
||||
steps:
|
||||
- run: |
|
||||
mkdir /tmp/artifacts
|
||||
printenv NODE_ENV >> /tmp/artifacts/NODE_ENV.txt
|
||||
[[ $EUID -ne 0 ]] && (sudo mkdir -p /tmp/artifacts && sudo chmod 777 /tmp/artifacts) || (mkdir -p /tmp/artifacts && chmod 777 /tmp/artifacts)
|
||||
printenv NODE_ENV >> /tmp/artifacts/NODE_ENV.txt || true
|
||||
npm -v >> /tmp/artifacts/npm-version.txt
|
||||
node -v >> /tmp/artifacts/node-version.txt
|
||||
ls -latR >> /tmp/artifacts/dir.txt
|
||||
@@ -69,7 +72,7 @@ commands:
|
||||
- run: npm run cov:e2e:report || true
|
||||
- run: npm run cov:e2e:<<parameters.suite>>:publish
|
||||
orbs:
|
||||
node: circleci/node@4.9.0
|
||||
node: circleci/node@5.1.0
|
||||
browser-tools: circleci/browser-tools@1.3.0
|
||||
jobs:
|
||||
npm-audit:
|
||||
@@ -110,7 +113,11 @@ jobs:
|
||||
path: dist/reports/tests/
|
||||
- store_artifacts:
|
||||
path: coverage
|
||||
- generate_and_store_version_and_filesystem_artifacts
|
||||
- when:
|
||||
condition:
|
||||
equal: [ 42, 42 ] # Always generate version artifacts regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2
|
||||
steps:
|
||||
- generate_and_store_version_and_filesystem_artifacts
|
||||
e2e-test:
|
||||
parameters:
|
||||
node-version:
|
||||
@@ -128,8 +135,12 @@ jobs:
|
||||
steps:
|
||||
- run: npx playwright install chrome-beta
|
||||
- run: SHARD="$((${CIRCLE_NODE_INDEX}+1))"; npm run test:e2e:<<parameters.suite>> -- --shard=${SHARD}/${CIRCLE_NODE_TOTAL}
|
||||
- generate_e2e_code_cov_report:
|
||||
suite: <<parameters.suite>>
|
||||
- when:
|
||||
condition:
|
||||
equal: [ 42, 42 ] # Always run codecov reports regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2
|
||||
steps:
|
||||
- generate_e2e_code_cov_report:
|
||||
suite: <<parameters.suite>>
|
||||
- store_test_results:
|
||||
path: test-results/results.xml
|
||||
- store_artifacts:
|
||||
@@ -138,7 +149,46 @@ jobs:
|
||||
path: coverage
|
||||
- store_artifacts:
|
||||
path: html-test-results
|
||||
- generate_and_store_version_and_filesystem_artifacts
|
||||
- when:
|
||||
condition:
|
||||
equal: [ 42, 42 ] # Always generate version artifacts regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2
|
||||
steps:
|
||||
- generate_and_store_version_and_filesystem_artifacts
|
||||
e2e-couchdb:
|
||||
parameters:
|
||||
node-version:
|
||||
type: string
|
||||
executor: ubuntu
|
||||
steps:
|
||||
- build_and_install:
|
||||
node-version: <<parameters.node-version>>
|
||||
- run: npx playwright@1.32.3 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
|
||||
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
|
||||
- run: npm run test:e2e:couchdb
|
||||
- when:
|
||||
condition:
|
||||
equal: [ 42, 42 ] # Always run codecov reports regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2
|
||||
steps:
|
||||
- generate_e2e_code_cov_report:
|
||||
suite: full #add to full suite
|
||||
- store_test_results:
|
||||
path: test-results/results.xml
|
||||
- store_artifacts:
|
||||
path: test-results
|
||||
- store_artifacts:
|
||||
path: coverage
|
||||
- store_artifacts:
|
||||
path: html-test-results
|
||||
- when:
|
||||
condition:
|
||||
equal: [ 42, 42 ] # Always generate version artifacts regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2
|
||||
steps:
|
||||
- generate_and_store_version_and_filesystem_artifacts
|
||||
perf-test:
|
||||
parameters:
|
||||
node-version:
|
||||
@@ -154,7 +204,11 @@ jobs:
|
||||
path: test-results
|
||||
- store_artifacts:
|
||||
path: html-test-results
|
||||
- generate_and_store_version_and_filesystem_artifacts
|
||||
- when:
|
||||
condition:
|
||||
equal: [ 42, 42 ] # Always run codecov reports regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2
|
||||
steps:
|
||||
- generate_and_store_version_and_filesystem_artifacts
|
||||
visual-test:
|
||||
parameters:
|
||||
node-version:
|
||||
@@ -170,46 +224,49 @@ jobs:
|
||||
path: test-results
|
||||
- store_artifacts:
|
||||
path: html-test-results
|
||||
- generate_and_store_version_and_filesystem_artifacts
|
||||
- when:
|
||||
condition:
|
||||
equal: [ 42, 42 ] # Always generate version artifacts regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2
|
||||
steps:
|
||||
- generate_and_store_version_and_filesystem_artifacts
|
||||
workflows:
|
||||
overall-circleci-commit-status: #These jobs run on every commit
|
||||
jobs:
|
||||
- lint:
|
||||
name: node14-lint
|
||||
node-version: lts/fermium
|
||||
name: node16-lint
|
||||
node-version: lts/gallium
|
||||
- unit-test:
|
||||
name: node18-chrome
|
||||
node-version: "18"
|
||||
node-version: lts/hydrogen
|
||||
- e2e-test:
|
||||
name: e2e-stable
|
||||
node-version: lts/gallium
|
||||
node-version: lts/hydrogen
|
||||
suite: stable
|
||||
- perf-test:
|
||||
node-version: lts/gallium
|
||||
node-version: lts/hydrogen
|
||||
- visual-test:
|
||||
node-version: lts/gallium
|
||||
node-version: lts/hydrogen
|
||||
|
||||
the-nightly: #These jobs do not run on PRs, but against master at night
|
||||
jobs:
|
||||
- unit-test:
|
||||
name: node14-chrome-nightly
|
||||
node-version: lts/fermium
|
||||
- unit-test:
|
||||
name: node16-chrome-nightly
|
||||
node-version: lts/gallium
|
||||
- unit-test:
|
||||
name: node18-chrome
|
||||
node-version: "18"
|
||||
node-version: lts/hydrogen
|
||||
- npm-audit:
|
||||
node-version: lts/gallium
|
||||
node-version: lts/hydrogen
|
||||
- e2e-test:
|
||||
name: e2e-full-nightly
|
||||
node-version: lts/gallium
|
||||
node-version: lts/hydrogen
|
||||
suite: full
|
||||
- perf-test:
|
||||
node-version: lts/gallium
|
||||
node-version: lts/hydrogen
|
||||
- visual-test:
|
||||
node-version: lts/gallium
|
||||
node-version: lts/hydrogen
|
||||
- e2e-couchdb:
|
||||
node-version: lts/hydrogen
|
||||
triggers:
|
||||
- schedule:
|
||||
cron: "0 0 * * *"
|
||||
|
||||
10
.github/dependabot.yml
vendored
10
.github/dependabot.yml
vendored
@@ -4,13 +4,13 @@ updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 10
|
||||
labels:
|
||||
- "pr:daveit"
|
||||
- "pr:e2e"
|
||||
- "type:maintenance"
|
||||
- "dependencies"
|
||||
- "pr:daveit"
|
||||
- "pr:platform"
|
||||
ignore:
|
||||
#We have to source the playwright container which is not detected by Dependabot
|
||||
@@ -25,11 +25,15 @@ updates:
|
||||
update-types: ["version-update:semver-patch"]
|
||||
- dependency-name: "sinon"
|
||||
update-types: ["version-update:semver-patch"]
|
||||
- dependency-name: "moment-timezone"
|
||||
update-types: ["version-update:semver-patch"]
|
||||
- dependency-name: "@types/lodash"
|
||||
update-types: ["version-update:semver-patch"]
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
labels:
|
||||
- "pr:daveit"
|
||||
- "type:maintenance"
|
||||
- "dependencies"
|
||||
- "pr:daveit"
|
||||
|
||||
52
.github/workflows/e2e-couchdb.yml
vendored
52
.github/workflows/e2e-couchdb.yml
vendored
@@ -5,34 +5,56 @@ on:
|
||||
types:
|
||||
- labeled
|
||||
- opened
|
||||
env:
|
||||
OPENMCT_DATABASE_NAME: openmct
|
||||
COUCH_ADMIN_USER: admin
|
||||
COUCH_ADMIN_PASSWORD: password
|
||||
COUCH_BASE_LOCAL: http://localhost:5984
|
||||
COUCH_NODE_NAME: nonode@nohost
|
||||
jobs:
|
||||
e2e-couchdb:
|
||||
if: ${{ github.event.label.name == 'pr:e2e:couchdb' }}
|
||||
if: ${{ github.event.label.name == 'pr:e2e:couchdb' }} || ${{ github.event.action == 'opened' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- run : docker-compose -f src/plugins/persistence/couch/couchdb-compose.yaml up --detach
|
||||
- run : sleep 3 # wait until CouchDB has started (TODO: there must be a better way)
|
||||
- run : bash src/plugins/persistence/couch/setup-couchdb.sh
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
- run: npx playwright@1.29.0 install
|
||||
node-version: 'lts/gallium'
|
||||
- run: npx playwright@1.32.3 install
|
||||
- run: npm install
|
||||
- run: sh src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh
|
||||
- run: npm run test:e2e:couchdb
|
||||
- run: ls -latr
|
||||
- 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
|
||||
sleep 3
|
||||
bash src/plugins/persistence/couch/setup-couchdb.sh
|
||||
bash src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh
|
||||
- name: Run CouchDB Tests and publish to deploysentinel
|
||||
env:
|
||||
DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}
|
||||
run: npm run test:e2e:couchdb
|
||||
- name: Publish Results to Codecov.io
|
||||
env:
|
||||
SUPER_SECRET: ${{ secrets.CODECOV_TOKEN }}
|
||||
run: npm run cov:e2e:full:publish
|
||||
- name: Archive test results
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: test-results
|
||||
- name: Archive html test results
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: html-test-results
|
||||
- name: Remove pr:e2e:couchdb label (if present)
|
||||
if: ${{ contains(github.event.pull_request.labels.*.name, 'pr:e2e:couchdb') }}
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const { owner, repo, number } = context.issue;
|
||||
const labelToRemove = 'pr:e2e:couchdb';
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: number,
|
||||
name: labelToRemove
|
||||
});
|
||||
} catch (error) {
|
||||
core.warning(`Failed to remove 'pr:e2e:couchdb' label: ${error.message}`);
|
||||
}
|
||||
|
||||
29
.github/workflows/e2e-pr.yml
vendored
29
.github/workflows/e2e-pr.yml
vendored
@@ -5,7 +5,6 @@ on:
|
||||
types:
|
||||
- labeled
|
||||
- opened
|
||||
|
||||
jobs:
|
||||
e2e-full:
|
||||
if: ${{ github.event.label.name == 'pr:e2e' }}
|
||||
@@ -30,11 +29,18 @@ jobs:
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
- run: npx playwright@1.29.0 install
|
||||
- run: npx playwright@1.32.3 install
|
||||
- run: npx playwright install chrome-beta
|
||||
- run: npm install
|
||||
- run: npm run test:e2e:full
|
||||
- run: npm run test:e2e:full -- --max-failures=40
|
||||
- run: npm run cov:e2e:report || true
|
||||
- shell: bash
|
||||
env:
|
||||
SUPER_SECRET: ${{ secrets.CODECOV_TOKEN }}
|
||||
run: |
|
||||
npm run cov:e2e:full:publish
|
||||
- name: Archive test results
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: test-results
|
||||
@@ -60,3 +66,20 @@ jobs:
|
||||
repo: "openmct",
|
||||
body: 'Failure ❌ ! Build artifacts are here: https://github.com/nasa/openmct/actions/runs/' + context.runId
|
||||
})
|
||||
- name: Remove pr:e2e label (if present)
|
||||
if: ${{ contains(github.event.pull_request.labels.*.name, 'pr:e2e') }}
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const { owner, repo, number } = context.issue;
|
||||
const labelToRemove = 'pr:e2e';
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: number,
|
||||
name: labelToRemove
|
||||
});
|
||||
} catch (error) {
|
||||
core.warning(`Failed to remove 'pr:e2e' label: ${error.message}`);
|
||||
}
|
||||
|
||||
21
.github/workflows/e2e.yml
vendored
21
.github/workflows/e2e.yml
vendored
@@ -1,21 +0,0 @@
|
||||
name: "e2e"
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Which branch do you want to test?' # Limited to branch for now
|
||||
required: false
|
||||
default: 'master'
|
||||
jobs:
|
||||
e2e:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.inputs.version }}
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
- run: npm install
|
||||
- name: Run the e2e tests
|
||||
run: npm run test:e2e:ci
|
||||
1
.github/workflows/pr-platform.yml
vendored
1
.github/workflows/pr-platform.yml
vendored
@@ -16,7 +16,6 @@ jobs:
|
||||
- macos-latest
|
||||
- windows-latest
|
||||
node_version:
|
||||
- 14
|
||||
- 16
|
||||
- 18
|
||||
architecture:
|
||||
|
||||
@@ -53,7 +53,11 @@ module.exports = merge(common, {
|
||||
},
|
||||
client: {
|
||||
progress: true,
|
||||
overlay: true
|
||||
overlay: {
|
||||
// Disable overlay for runtime errors.
|
||||
// See: https://github.com/webpack/webpack-dev-server/issues/4771
|
||||
runtimeErrors: false
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -191,7 +191,7 @@ The following guidelines are provided for anyone contributing source code to the
|
||||
if (responseCode === 401)
|
||||
```
|
||||
1. Use the ternary operator only for simple cases such as variable assignment. Nested ternaries should be avoided in all cases.
|
||||
1. Test specs should reside alongside the source code they test, not in a separate directory.
|
||||
1. Unit Test specs should reside alongside the source code they test, not in a separate directory.
|
||||
1. Organize code by feature, not by type.
|
||||
eg.
|
||||
```
|
||||
@@ -222,44 +222,6 @@ The following guidelines are provided for anyone contributing source code to the
|
||||
Deviations from Open MCT code style guidelines require two-party agreement,
|
||||
typically from the author of the change and its reviewer.
|
||||
|
||||
### Test Standards
|
||||
|
||||
Automated testing shall occur whenever changes are merged into the main
|
||||
development branch and must be confirmed alongside any pull request.
|
||||
|
||||
Automated tests are tests which exercise plugins, API, and utility classes.
|
||||
Tests are subject to code review along with the actual implementation, to
|
||||
ensure that tests are applicable and useful.
|
||||
|
||||
Examples of useful tests:
|
||||
* Tests which replicate bugs (or their root causes) to verify their
|
||||
resolution.
|
||||
* Tests which reflect details from software specifications.
|
||||
* Tests which exercise edge or corner cases among inputs.
|
||||
* Tests which verify expected interactions with other components in the
|
||||
system.
|
||||
|
||||
#### Guidelines
|
||||
* 100% statement coverage is achievable and desirable.
|
||||
* Do blackbox testing. Test external behaviors, not internal details. Write tests that describe what your plugin is supposed to do. How it does this doesn't matter, so don't test it.
|
||||
* Unit test specs for plugins should be defined at the plugin level. Start with one test spec per plugin named pluginSpec.js, and as this test spec grows too big, break it up into multiple test specs that logically group related tests.
|
||||
* Unit tests for API or for utility functions and classes may be defined at a per-source file level.
|
||||
* Wherever possible only use and mock public API, builtin functions, and UI in your test specs. Do not directly invoke any private functions. ie. only call or mock functions and objects exposed by openmct.* (eg. openmct.telemetry, openmct.objectView, etc.), and builtin browser functions (fetch, requestAnimationFrame, setTimeout, etc.).
|
||||
* Where builtin functions have been mocked, be sure to clear them between tests.
|
||||
* Test at an appropriate level of isolation. Eg.
|
||||
* If you’re testing a view, you do not need to test the whole application UI, you can just fetch the view provider using the public API and render the view into an element that you have created.
|
||||
* You do not need to test that the view switcher works, there should be separate tests for that.
|
||||
* You do not need to test that telemetry providers work, you can mock openmct.telemetry.request() to feed test data to the view.
|
||||
* Use your best judgement when deciding on appropriate scope.
|
||||
* Automated tests for plugins should start by actually installing the plugin being tested, and then test that installing the plugin adds the desired features and behavior to Open MCT, observing the above rules.
|
||||
* All variables used in a test spec, including any instances of the Open MCT API should be declared inside of an appropriate block scope (not at the root level of the source file), and should be initialized in the relevant beforeEach block. `beforeEach` is preferable to `beforeAll` to avoid leaking of state between tests.
|
||||
* A `afterEach` or `afterAll` should be used to do any clean up necessary to prevent leakage of state between test specs. This can happen when functions on `window` are wrapped, or when the URL is changed. [A convenience function](https://github.com/nasa/openmct/blob/master/src/utils/testing.js#L59) is provided for resetting the URL and clearing builtin spies between tests.
|
||||
* If writing unit tests for legacy Angular code be sure to follow [best practices in order to avoid memory leaks](https://www.thecodecampus.de/blog/avoid-memory-leaks-angularjs-unit-tests/).
|
||||
|
||||
#### Examples
|
||||
* [Example of an automated test spec for an object view plugin](https://github.com/nasa/openmct/blob/master/src/plugins/telemetryTable/pluginSpec.js)
|
||||
* [Example of an automated test spec for API](https://github.com/nasa/openmct/blob/master/src/api/time/TimeAPISpec.js)
|
||||
|
||||
### Commit Message Standards
|
||||
|
||||
Commit messages should:
|
||||
@@ -301,7 +263,7 @@ Issue severity is categorized as follows (in ascending order):
|
||||
|
||||
* _Trivial_: Minimal impact on the usefulness and functionality of the software; a "nice-to-have." Visual impact without functional impact,
|
||||
* _Medium_: Some impairment of use, but simple workarounds exist
|
||||
* _Critical_: Significant loss of functionality or impairment of use. Display of telemetry data is not affected though.
|
||||
* _Critical_: Significant loss of functionality or impairment of use. Display of telemetry data is not affected though. Complex workarounds exist.
|
||||
* _Blocker_: Major functionality is impaired or lost, threatening mission success. Display of telemetry data is impaired or blocked by the bug, which could lead to loss of situational awareness.
|
||||
|
||||
## Check Lists
|
||||
@@ -310,22 +272,4 @@ The following check lists should be completed and attached to pull requests
|
||||
when they are filed (author checklist) and when they are merged (reviewer
|
||||
checklist).
|
||||
|
||||
### Author Checklist
|
||||
|
||||
[Within PR Template](.github/PULL_REQUEST_TEMPLATE.md)
|
||||
|
||||
### Reviewer Checklist
|
||||
|
||||
* [ ] Changes appear to address issue?
|
||||
* [ ] Changes appear not to be breaking changes?
|
||||
* [ ] Appropriate unit tests included?
|
||||
* [ ] Code style and in-line documentation are appropriate?
|
||||
* [ ] Commit messages meet standards?
|
||||
* [ ] Has associated issue been labelled `unverified`? (only applicable if this PR closes the issue)
|
||||
* [ ] Has associated issue been labelled `bug`? (only applicable if this PR is for a bug fix)
|
||||
* [ ] List of Acceptance Tests Performed.
|
||||
|
||||
Write out a small list of tests performed with just enough detail for another developer on the team
|
||||
to execute.
|
||||
|
||||
i.e. ```When Clicking on Add button, new `object` appears in dropdown.```
|
||||
|
||||
50
TESTING.md
Normal file
50
TESTING.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Testing
|
||||
Open MCT Testing is iterating and improving at a rapid pace. This document serves to capture and index existing testing documentation and house documentation which no other obvious location as our testing evolves.
|
||||
|
||||
## General Testing Process
|
||||
Documentation located [here](./docs/src/process/testing/plan.md)
|
||||
|
||||
## Unit Testing
|
||||
Unit testing is essential part of our test strategy and complements our e2e testing strategy.
|
||||
|
||||
#### Unit Test Guidelines
|
||||
* Unit Test specs should reside alongside the source code they test, not in a separate directory.
|
||||
* Unit test specs for plugins should be defined at the plugin level. Start with one test spec per plugin named pluginSpec.js, and as this test spec grows too big, break it up into multiple test specs that logically group related tests.
|
||||
* Unit tests for API or for utility functions and classes may be defined at a per-source file level.
|
||||
* Wherever possible only use and mock public API, builtin functions, and UI in your test specs. Do not directly invoke any private functions. ie. only call or mock functions and objects exposed by openmct.* (eg. openmct.telemetry, openmct.objectView, etc.), and builtin browser functions (fetch, requestAnimationFrame, setTimeout, etc.).
|
||||
* Where builtin functions have been mocked, be sure to clear them between tests.
|
||||
* Test at an appropriate level of isolation. Eg.
|
||||
* If you’re testing a view, you do not need to test the whole application UI, you can just fetch the view provider using the public API and render the view into an element that you have created.
|
||||
* You do not need to test that the view switcher works, there should be separate tests for that.
|
||||
* You do not need to test that telemetry providers work, you can mock openmct.telemetry.request() to feed test data to the view.
|
||||
* Use your best judgement when deciding on appropriate scope.
|
||||
* Automated tests for plugins should start by actually installing the plugin being tested, and then test that installing the plugin adds the desired features and behavior to Open MCT, observing the above rules.
|
||||
* All variables used in a test spec, including any instances of the Open MCT API should be declared inside of an appropriate block scope (not at the root level of the source file), and should be initialized in the relevant beforeEach block. `beforeEach` is preferable to `beforeAll` to avoid leaking of state between tests.
|
||||
* A `afterEach` or `afterAll` should be used to do any clean up necessary to prevent leakage of state between test specs. This can happen when functions on `window` are wrapped, or when the URL is changed. [A convenience function](https://github.com/nasa/openmct/blob/master/src/utils/testing.js#L59) is provided for resetting the URL and clearing builtin spies between tests.
|
||||
|
||||
#### Unit Test Examples
|
||||
* [Example of an automated test spec for an object view plugin](https://github.com/nasa/openmct/blob/master/src/plugins/telemetryTable/pluginSpec.js)
|
||||
* [Example of an automated test spec for API](https://github.com/nasa/openmct/blob/master/src/api/time/TimeAPISpec.js)
|
||||
|
||||
#### Unit Testing Execution
|
||||
|
||||
The unit tests can be executed in one of two ways:
|
||||
`npm run test` which runs the entire suite against headless chrome
|
||||
`npm run test:debug` for debugging the tests in realtime in an active chrome session.
|
||||
|
||||
## e2e, performance, and visual testing
|
||||
Documentation located [here](./e2e/README.md)
|
||||
|
||||
## Code Coverage
|
||||
|
||||
* 100% statement coverage is achievable and desirable.
|
||||
|
||||
Codecov.io will combine each of the above commands with [Codecov.io Flags](https://docs.codecov.com/docs/flags). Effectively, this allows us to combine multiple reports which are run at various stages of our CI Pipeline or run as part of a parallel process.
|
||||
|
||||
This e2e coverage is combined with our unit test report to give a comprehensive (if flawed) view of line coverage.
|
||||
|
||||
### Limitations in our code coverage reporting
|
||||
|
||||
Our code coverage implementation has two known limitations:
|
||||
- [Variability and accuracy](https://github.com/nasa/openmct/issues/5811)
|
||||
- [Vue instrumentation](https://github.com/nasa/openmct/issues/4973)
|
||||
10
codecov.yml
10
codecov.yml
@@ -15,14 +15,14 @@ coverage:
|
||||
|
||||
flags:
|
||||
unit:
|
||||
carryforward: true
|
||||
e2e-ci:
|
||||
carryforward: true
|
||||
carryforward: false
|
||||
e2e-stable:
|
||||
carryforward: false
|
||||
e2e-full:
|
||||
carryforward: true
|
||||
|
||||
comment:
|
||||
layout: "reach,diff,flags,files,footer"
|
||||
layout: "diff,flags,files,footer"
|
||||
behavior: default
|
||||
require_changes: false
|
||||
show_carryforward_flags: true
|
||||
show_carryforward_flags: true
|
||||
|
||||
@@ -139,16 +139,18 @@ These tests are expected to become blocking and gating with assertions as we ext
|
||||
|
||||
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.
|
||||
|
||||
- `./helper` - contains helper functions or scripts which are leveraged directly within the testsuites. i.e. non-default plugin scripts injected into DOM
|
||||
- `./test-data` - contains test data which is leveraged or generated in the functional, performance, or visual test suites. i.e. localStorage data
|
||||
- `./tests/functional` - the bulk of the tests are contained within this folder to verify the functionality of open mct
|
||||
- `./tests/functional/example/` - tests which specifically verify the example plugins
|
||||
- `./tests/functional/plugins/` - tests which loosely test each plugin. This folder is the most likely to change. Note: some @snapshot tests are still contained within this structure
|
||||
- `./tests/framework/` - tests which verify that our testframework functionality and assumptions will continue to work based on further refactoring or playwright version changes
|
||||
- `./tests/performance/` - performance tests
|
||||
- `./tests/visual/` - Visual tests
|
||||
- `./appActions.js` - Contains common fixtures which can be leveraged by testcase authors to quickly move through the application when writing new tests.
|
||||
- `./baseFixture.js` - Contains base fixtures which only extend default `@playwright/test` functionality. The goal is to remove these fixtures as native Playwright APIs improve.
|
||||
|File Path|Description|
|
||||
|:-:|-|
|
||||
|`./helper` | Contains helper functions or scripts which are leveraged directly within the test suites (e.g.: non-default plugin scripts injected into the DOM)|
|
||||
|`./test-data` | Contains test data which is leveraged or generated in the functional, performance, or visual test suites (e.g.: localStorage data).|
|
||||
|`./tests/functional` | The bulk of the tests are contained within this folder to verify the functionality of Open MCT.|
|
||||
|`./tests/functional/example/` | Tests which specifically verify the example plugins (e.g.: Sine Wave Generator).|
|
||||
|`./tests/functional/plugins/` | Tests which loosely test each plugin. This folder is the most likely to change. Note: some `@snapshot` tests are still contained within this structure.|
|
||||
|`./tests/framework/` | Tests which verify that our testing framework's functionality and assumptions will continue to work based on further refactoring or Playwright version changes (e.g.: verifying custom fixtures and appActions).|
|
||||
|`./tests/performance/` | Performance tests.|
|
||||
|`./tests/visual/` | Visual tests.|
|
||||
|`./appActions.js` | Contains common methods which can be leveraged by test case authors to quickly move through the application when writing new tests.|
|
||||
|`./baseFixture.js` | Contains base fixtures which only extend default `@playwright/test` functionality. The expectation is that these fixtures will be removed as the native Playwright API improves|
|
||||
|
||||
Our functional tests end in `*.e2e.spec.js`, visual tests in `*.visual.spec.js` and performance tests in `*.perf.spec.js`.
|
||||
|
||||
@@ -158,10 +160,12 @@ Where possible, we try to run Open MCT without modification or configuration cha
|
||||
|
||||
Open MCT is leveraging the [config file](https://playwright.dev/docs/test-configuration) pattern to describe the capabilities of Open MCT e2e _where_ it's run
|
||||
|
||||
- `./playwright-ci.config.js` - Used when running in CI or to debug CI issues locally
|
||||
- `./playwright-local.config.js` - Used when running locally
|
||||
- `./playwright-performance.config.js` - Used when running performance tests in CI or locally
|
||||
- `./playwright-visual.config.js` - Used to run the visual tests in CI or locally
|
||||
|Config File|Description|
|
||||
|:-:|-|
|
||||
|`./playwright-ci.config.js` | Used when running in CI or to debug CI issues locally|
|
||||
|`./playwright-local.config.js` | Used when running locally|
|
||||
|`./playwright-performance.config.js` | Used when running performance tests in CI or locally|
|
||||
|`./playwright-visual.config.js` | Used to run the visual tests in CI or locally|
|
||||
|
||||
#### Test Tags
|
||||
|
||||
@@ -169,13 +173,15 @@ Test tags are a great way of organizing tests outside of a file structure. To le
|
||||
|
||||
Current list of test tags:
|
||||
|
||||
- `@ipad` - Test case or test suite is compatible with Playwright's iPad support and Open MCT's read-only mobile view (i.e. no Create button).
|
||||
- `@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).
|
||||
- `@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.
|
||||
|Test Tag|Description|
|
||||
|:-:|-|
|
||||
|`@ipad` | Test case or test suite is compatible with Playwright's iPad support and Open MCT's read-only mobile view (i.e. no create button).|
|
||||
|`@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).|
|
||||
|`@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.|
|
||||
|
||||
### Continuous Integration
|
||||
|
||||
@@ -200,6 +206,7 @@ CircleCI
|
||||
Github Actions / Workflow
|
||||
|
||||
- Full suite against all browsers/projects. Triggered with Github Label Event 'pr:e2e'
|
||||
- CouchDB Tests. Triggered on PR Create and again with Github Label Event 'pr:e2e:couchdb'
|
||||
- Visual Tests. Triggered with Github Label Event 'pr:visual'
|
||||
|
||||
#### 3. Scheduled / Batch Testing
|
||||
@@ -231,7 +238,8 @@ At the same time, we don't want to waste CI resources on parallel runs, so we've
|
||||
|
||||
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.
|
||||
- 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:
|
||||
|
||||
@@ -292,13 +300,24 @@ Skipping based on browser version (Rarely used): <https://github.com/microsoft/p
|
||||
- How to make tests robust to function in other contexts (VISTA, VIPER, etc.)
|
||||
- Leverage the use of `appActions.js` methods such as `createDomainObjectWithDefaults()`
|
||||
- How to make tests faster and more resilient
|
||||
- When possible, navigate directly by URL
|
||||
- Leverage `await page.goto('./', { waitUntil: 'networkidle' });`
|
||||
- When possible, navigate directly by URL:
|
||||
|
||||
```javascript
|
||||
// You can capture the CreatedObjectInfo returned from this appAction:
|
||||
const clock = await createDomainObjectWithDefaults(page, { type: 'Clock' });
|
||||
|
||||
// ...and use its `url` property to navigate directly to it later in the test:
|
||||
await page.goto(clock.url);
|
||||
```
|
||||
|
||||
- Leverage `await page.goto('./', { waitUntil: 'domcontentloaded' });`
|
||||
- Initial navigation should _almost_ always use the `{ waitUntil: 'domcontentloaded' }` option.
|
||||
- Avoid repeated setup to test to test a single assertion. Write longer tests with multiple soft assertions.
|
||||
|
||||
### How to write a great test (WIP)
|
||||
|
||||
- Use our [App Actions](./appActions.js) for performing common actions whenever applicable.
|
||||
- Use `waitForPlotsToRender()` before asserting against anything that is dependent upon plot series data being loaded and drawn.
|
||||
- If you create an object outside of using the `createDomainObjectWithDefaults` App Action, make sure to fill in the 'Notes' section of your object with `page.testNotes`:
|
||||
|
||||
```js
|
||||
@@ -345,12 +364,16 @@ We leverage the following official Playwright reporters:
|
||||
- Tracefile
|
||||
- Screenshots
|
||||
|
||||
When running the tests locally with the `npm run test:local` command, the html report will open automatically on failure. Inside this HTML report will be a complete summary of the finished tests. If the tests failed, you'll see embedded links to screenshot failure, execution logs, and the Tracefile.
|
||||
When running the tests locally with the `npm run test:e2e:local` command, the html report will open automatically on failure. Inside this HTML report will be a complete summary of the finished tests. If the tests failed, you'll see embedded links to screenshot failure, execution logs, and the Tracefile.
|
||||
|
||||
When looking at the reports run in CI, you'll leverage this same HTML Report which is hosted either in CircleCI or Github Actions as a build artifact.
|
||||
|
||||
### e2e Code Coverage
|
||||
|
||||
Our e2e code coverage is captured and combined with our unit test coverage. For more information, please see our [code coverage documentation](../TESTING.md)
|
||||
|
||||
#### Generating e2e code coverage
|
||||
|
||||
Code coverage is collected during test execution using our custom [baseFixture](./baseFixtures.js). The raw coverage files are stored in a `.nyc_report` directory to be converted into a lcov file with the following [nyc](https://github.com/istanbuljs/nyc) command:
|
||||
|
||||
```npm run cov:e2e:report```
|
||||
@@ -361,10 +384,6 @@ At this point, the nyc linecov report can be published to [codecov.io](https://a
|
||||
or
|
||||
```npm run cov:e2e:full:publish``` for the full suite running against all available platforms.
|
||||
|
||||
Codecov.io will combine each of the above commands with [Codecov.io Flags](https://docs.codecov.com/docs/flags). Effectively, this allows us to combine multiple reports which are run at various stages of our CI Pipeline or run as part of a parallel process.
|
||||
|
||||
This e2e coverage is combined with our unit test report to give a comprehensive (if flawed) view of line coverage.
|
||||
|
||||
## Other
|
||||
|
||||
### About e2e testing
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
|
||||
const Buffer = require('buffer').Buffer;
|
||||
const genUuid = require('uuid').v4;
|
||||
const { expect } = require('@playwright/test');
|
||||
|
||||
/**
|
||||
* This common function creates a domain object with the default options. It is the preferred way of creating objects
|
||||
@@ -74,7 +75,6 @@ async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine
|
||||
// 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}?hideTree=true`);
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
//Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
@@ -140,6 +140,7 @@ async function createNotification(page, createNotificationOptions) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand an item in the tree by a given object name.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} name
|
||||
*/
|
||||
@@ -272,6 +273,7 @@ async function getFocusedObjectUuid(page) {
|
||||
* @returns {Promise<string>} the url of the object
|
||||
*/
|
||||
async function getHashUrlToDomainObject(page, uuid) {
|
||||
await page.waitForLoadState('load'); //Add some determinism
|
||||
const hashUrl = await page.evaluate(async (objectUuid) => {
|
||||
const path = await window.openmct.objects.getOriginalPath(objectUuid);
|
||||
let url = './#/browse/' + [...path].reverse()
|
||||
@@ -404,19 +406,92 @@ async function selectInspectorTab(page, name) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits and asserts that all plot series data on the page
|
||||
* is loaded and drawn.
|
||||
*
|
||||
* In lieu of a better way to detect when a plot is done rendering,
|
||||
* we [attach a class to the '.gl-plot' element](https://github.com/nasa/openmct/blob/5924d7ea95a0c2d4141c602a3c7d0665cb91095f/src/plugins/plot/MctPlot.vue#L27)
|
||||
* once all pending series data has been loaded. The following appAction retrieves
|
||||
* 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
|
||||
*/
|
||||
async function waitForPlotsToRender(page) {
|
||||
const plotLocator = page.locator('.gl-plot');
|
||||
for (const plot of await plotLocator.all()) {
|
||||
await expect(plot).toHaveClass(/js-series-data-loaded/);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} PlotPixel
|
||||
* @property {number} r The value of the red channel (0-255)
|
||||
* @property {number} g The value of the green channel (0-255)
|
||||
* @property {number} b The value of the blue channel (0-255)
|
||||
* @property {number} a The value of the alpha channel (0-255)
|
||||
* @property {string} strValue The rgba string value of the pixel
|
||||
*/
|
||||
|
||||
/**
|
||||
* Wait for all plots to render and then retrieve and return an array
|
||||
* of canvas plot pixel data (RGBA values).
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} canvasSelector The selector for the canvas element
|
||||
* @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);
|
||||
const canvasContextHandle = await page.evaluateHandle(canvas => canvas.getContext('2d'), canvasHandle);
|
||||
|
||||
await waitForPlotsToRender(page);
|
||||
await 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)
|
||||
/** @type {ImageData} */
|
||||
const data = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
|
||||
/** @type {number[]} */
|
||||
const imageDataValues = Object.values(data);
|
||||
/** @type {PlotPixel[]} */
|
||||
const plotPixels = [];
|
||||
// Each pixel consists of four values within the ImageData.data array. The for loop iterates by multiples of four.
|
||||
// The values associated with each pixel are R (red), G (green), B (blue), and A (alpha), in that order.
|
||||
for (let i = 0; i < imageDataValues.length;) {
|
||||
if (imageDataValues[i] > 0) {
|
||||
plotPixels.push({
|
||||
r: imageDataValues[i],
|
||||
g: imageDataValues[i + 1],
|
||||
b: imageDataValues[i + 2],
|
||||
a: imageDataValues[i + 3],
|
||||
strValue: `rgb(${imageDataValues[i]}, ${imageDataValues[i + 1]}, ${imageDataValues[i + 2]}, ${imageDataValues[i + 3]})`
|
||||
});
|
||||
}
|
||||
|
||||
i = i + 4;
|
||||
}
|
||||
|
||||
window.getCanvasValue(plotPixels);
|
||||
}, [canvasHandle, canvasContextHandle]);
|
||||
|
||||
return getTelemValuePromise;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
module.exports = {
|
||||
createDomainObjectWithDefaults,
|
||||
createNotification,
|
||||
expandTreePaneItemByName,
|
||||
expandEntireTree,
|
||||
createPlanFromJSON,
|
||||
openObjectTreeContextMenu,
|
||||
expandEntireTree,
|
||||
expandTreePaneItemByName,
|
||||
getCanvasPixels,
|
||||
getHashUrlToDomainObject,
|
||||
getFocusedObjectUuid,
|
||||
openObjectTreeContextMenu,
|
||||
setFixedTimeMode,
|
||||
setRealTimeMode,
|
||||
setStartOffset,
|
||||
setEndOffset,
|
||||
selectInspectorTab
|
||||
selectInspectorTab,
|
||||
waitForPlotsToRender
|
||||
};
|
||||
|
||||
@@ -170,5 +170,6 @@ exports.test = base.test.extend({
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
exports.expect = expect;
|
||||
exports.waitForAnimations = waitForAnimations;
|
||||
|
||||
@@ -58,8 +58,14 @@ async function navigateToFaultManagementWithoutExample(page) {
|
||||
async function navigateToFaultItemInTree(page) {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
// Click text=Fault Management
|
||||
await page.click('text=Fault Management'); // this verifies the plugin has been added
|
||||
const faultManagementTreeItem = page.getByRole('tree', {
|
||||
name: "Main Tree"
|
||||
}).getByRole('treeitem', {
|
||||
name: "Fault Management"
|
||||
});
|
||||
|
||||
// Navigate to "Fault Management" from the tree
|
||||
await faultManagementTreeItem.click();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -141,8 +147,7 @@ async function clearSearch(page) {
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function selectFaultItem(page, rowNumber) {
|
||||
// eslint-disable-next-line playwright/no-force-option
|
||||
await page.check(`.c-fault-mgmt-item > input >> nth=${rowNumber - 1}`, { force: true }); // this will not work without force true, saw this may be a pw bug
|
||||
await page.locator(`.c-fault-mgmt-item > input >> nth=${rowNumber - 1}`).check();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -28,7 +28,7 @@ const NOTEBOOK_DROP_AREA = '.c-notebook__drag-area';
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function enterTextEntry(page, text) {
|
||||
// Click .c-notebook__drag-area
|
||||
// Click the 'Add Notebook Entry' area
|
||||
await page.locator(NOTEBOOK_DROP_AREA).click();
|
||||
|
||||
// enter text
|
||||
@@ -58,6 +58,7 @@ async function dragAndDropEmbed(page, notebookObject) {
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function commitEntry(page) {
|
||||
//Click the Commit Entry button
|
||||
await page.locator('.c-ne__save-button > button').click();
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ const NUM_WORKERS = 2;
|
||||
|
||||
/** @type {import('@playwright/test').PlaywrightTestConfig} */
|
||||
const config = {
|
||||
retries: 2, //Retries 2 times for a total of 3 runs. When running sharded and with maxFailures = 5, this should ensure that flake is managed without failing the full suite
|
||||
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',
|
||||
testIgnore: '**/*.perf.spec.js', //Ignore performance tests and define in playwright-perfromance.config.js
|
||||
timeout: 60 * 1000,
|
||||
@@ -74,7 +74,8 @@ const config = {
|
||||
outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840
|
||||
}],
|
||||
['junit', { outputFile: '../test-results/results.xml' }],
|
||||
['github']
|
||||
['github'],
|
||||
['@deploysentinel/playwright']
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
@@ -150,3 +150,17 @@ exports.test = test.extend({
|
||||
}
|
||||
});
|
||||
exports.expect = expect;
|
||||
|
||||
/**
|
||||
* Takes a readable stream and returns a string.
|
||||
* @param {ReadableStream} readable - the readable stream
|
||||
* @return {Promise<String>} the stringified stream
|
||||
*/
|
||||
exports.streamToString = async function (readable) {
|
||||
let result = '';
|
||||
for await (const chunk of readable) {
|
||||
result += chunk;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -25,7 +25,7 @@ const { createDomainObjectWithDefaults, createNotification, expandEntireTree } =
|
||||
|
||||
test.describe('AppActions', () => {
|
||||
test('createDomainObjectsWithDefaults', async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
const e2eFolder = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder',
|
||||
@@ -86,7 +86,7 @@ test.describe('AppActions', () => {
|
||||
});
|
||||
});
|
||||
test("createNotification", async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
await createNotification(page, {
|
||||
message: 'Test info notification',
|
||||
severity: 'info'
|
||||
@@ -110,7 +110,7 @@ test.describe('AppActions', () => {
|
||||
await page.locator('[aria-label="Dismiss"]').click();
|
||||
});
|
||||
test('expandEntireTree', async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
const rootFolder = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Folder'
|
||||
|
||||
@@ -32,7 +32,7 @@ test.describe('baseFixtures tests', () => {
|
||||
test('Verify that tests fail if console.error is thrown', async ({ page }) => {
|
||||
test.fail();
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
//Verify that ../fixtures.js detects console log errors
|
||||
await Promise.all([
|
||||
@@ -43,7 +43,7 @@ test.describe('baseFixtures tests', () => {
|
||||
});
|
||||
test('Verify that tests pass if console.warn is thrown', async ({ page }) => {
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
//Verify that ../fixtures.js detects console log errors
|
||||
await Promise.all([
|
||||
|
||||
@@ -63,7 +63,7 @@ test.describe('Renaming Timer Object', () => {
|
||||
let timer;
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Open a browser, navigate to the main page, and wait until all network events to resolve
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// We provide some helper functions in appActions like `createDomainObjectWithDefaults()`.
|
||||
// This example will create a Timer object with default properties, under the root folder:
|
||||
|
||||
@@ -36,7 +36,7 @@ const { test, expect } = require('../../pluginFixtures.js');
|
||||
|
||||
test('Generate Visual Test Data @localStorage', async ({ page, context }) => {
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
const overlayPlot = await createDomainObjectWithDefaults(page, { type: 'Overlay Plot' });
|
||||
|
||||
// click create button
|
||||
|
||||
@@ -30,7 +30,7 @@ test.describe('recycled_local_storage @localStorage', () => {
|
||||
//We may want to do some additional level of verification of this file. For now, we just verify that it exists and can be used in a test suite.
|
||||
test.use({ storageState: './e2e/test-data/recycled_local_storage.json' });
|
||||
test('Can use recycled_local_storage file', async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ const { test, expect } = require('../../baseFixtures.js');
|
||||
test.describe('Branding tests', () => {
|
||||
test('About Modal launches with basic branding properties', async ({ page }) => {
|
||||
// Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Click About button
|
||||
await page.click('.l-shell__app-logo');
|
||||
@@ -47,7 +47,7 @@ test.describe('Branding tests', () => {
|
||||
});
|
||||
test('Verify Links in About Modal @2p', async ({ page }) => {
|
||||
// Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Click About button
|
||||
await page.click('.l-shell__app-logo');
|
||||
|
||||
@@ -100,7 +100,7 @@ test.describe("CouchDB initialization with mocked responses @couchdb", () => {
|
||||
&& req.method() === 'GET');
|
||||
|
||||
// Go to baseURL.
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Wait for both requests to resolve.
|
||||
await Promise.all([
|
||||
|
||||
@@ -30,7 +30,7 @@ const { createDomainObjectWithDefaults } = require('../../../appActions');
|
||||
test.describe('Example Event Generator CRUD Operations', () => {
|
||||
test('Can create a Test Event Generator and it results in the table View', async ({ page }) => {
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
//Create a name for the object
|
||||
const newObjectName = 'Test Event Generator';
|
||||
|
||||
@@ -32,7 +32,7 @@ test.describe('Sine Wave Generator', () => {
|
||||
test.skip(browserName === 'firefox', 'This test needs to be updated to work with firefox');
|
||||
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
//Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
@@ -36,7 +36,7 @@ const imageFilePath = 'e2e/test-data/rick.jpg';
|
||||
test.describe('Form Validation Behavior', () => {
|
||||
test('Required Field indicators appear if title is empty and can be corrected', async ({ page }) => {
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
await page.click('button:has-text("Create")');
|
||||
await page.click(':nth-match(:text("Folder"), 2)');
|
||||
@@ -77,7 +77,7 @@ test.describe('Form File Input Behavior', () => {
|
||||
});
|
||||
|
||||
test('Can select a JSON file type', async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
await page.getByRole('button', { name: ' Create ' }).click();
|
||||
await page.getByRole('menuitem', { name: 'JSON File Input Object' }).click();
|
||||
@@ -91,7 +91,7 @@ test.describe('Form File Input Behavior', () => {
|
||||
});
|
||||
|
||||
test('Can select an image file type', async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
await page.getByRole('button', { name: ' Create ' }).click();
|
||||
await page.getByRole('menuitem', { name: 'Image File Input Object' }).click();
|
||||
@@ -117,7 +117,7 @@ test.describe('Persistence operations @addInit', () => {
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/4323'
|
||||
});
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
@@ -138,7 +138,7 @@ test.describe('Persistence operations @couchdb', () => {
|
||||
description: 'https://github.com/nasa/openmct/issues/5616'
|
||||
});
|
||||
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Create a new 'Clock' object with default settings
|
||||
const clock = await createDomainObjectWithDefaults(page, {
|
||||
@@ -166,12 +166,13 @@ test.describe('Persistence operations @couchdb', () => {
|
||||
timeout: 1000
|
||||
}).toEqual(1);
|
||||
});
|
||||
test('Can create an object after a conflict error @couchdb @2p', async ({ page }) => {
|
||||
test('Can create an object after a conflict error @couchdb @2p', async ({ page, openmctConfig }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/5982'
|
||||
});
|
||||
|
||||
const { myItemsFolderName } = openmctConfig;
|
||||
// Instantiate a second page/tab
|
||||
const page2 = await page.context().newPage();
|
||||
|
||||
// Both pages: Go to baseURL
|
||||
@@ -180,6 +181,10 @@ test.describe('Persistence operations @couchdb', () => {
|
||||
page2.goto('./', { waitUntil: 'networkidle' })
|
||||
]);
|
||||
|
||||
//Slow down the test a bit
|
||||
await expect(page.getByRole('treeitem', { name: ` ${myItemsFolderName}` })).toBeVisible();
|
||||
await expect(page2.getByRole('treeitem', { name: ` ${myItemsFolderName}` })).toBeVisible();
|
||||
|
||||
// Both pages: Click the Create button
|
||||
await Promise.all([
|
||||
page.click('button:has-text("Create")'),
|
||||
|
||||
@@ -36,7 +36,7 @@ test.describe('Persistence operations @addInit', () => {
|
||||
});
|
||||
|
||||
test('Non-persistable objects should not show persistence related actions', async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
await page.locator('text=Persistence Testing').first().click({
|
||||
button: 'right'
|
||||
|
||||
@@ -35,7 +35,7 @@ test.describe('Notifications List', () => {
|
||||
});
|
||||
|
||||
// Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Create an error notification with the message "Error message"
|
||||
await createNotification(page, {
|
||||
@@ -80,7 +80,7 @@ test.describe('Notification Overlay', () => {
|
||||
});
|
||||
|
||||
// Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Create a new Display Layout object
|
||||
await createDomainObjectWithDefaults(page, { type: 'Display Layout' });
|
||||
|
||||
@@ -28,12 +28,13 @@ const { getPreciseDuration } = require('../../../../src/utils/duration');
|
||||
|
||||
test.describe("Gantt Chart", () => {
|
||||
let ganttChart;
|
||||
let plan;
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
ganttChart = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Gantt Chart'
|
||||
});
|
||||
await createPlanFromJSON(page, {
|
||||
plan = await createPlanFromJSON(page, {
|
||||
json: testPlan1,
|
||||
parent: ganttChart.uuid
|
||||
});
|
||||
@@ -82,4 +83,21 @@ test.describe("Gantt Chart", () => {
|
||||
expect(expectedEndDate).toEqual(actualEndDate);
|
||||
expect(expectedDuration).toEqual(actualDuration);
|
||||
});
|
||||
test("Displays a Plan's draft status", async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/6641'
|
||||
});
|
||||
|
||||
// Mark the Plan's status as draft in the OpenMCT API
|
||||
await page.evaluate(async (planObject) => {
|
||||
await window.openmct.status.set(planObject.uuid, 'draft');
|
||||
}, plan);
|
||||
|
||||
// Navigate to the Gantt Chart
|
||||
await page.goto(ganttChart.url);
|
||||
|
||||
// Assert that the Plan's status is displayed as draft
|
||||
expect(await page.locator('.u-contents.c-swimlane.is-status--draft').count()).toBe(Object.keys(testPlan1).length);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,7 +27,7 @@ const { assertPlanActivities } = require('../../../helper/planningUtils');
|
||||
test.describe("Plan", () => {
|
||||
let plan;
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
plan = await createPlanFromJSON(page, {
|
||||
json: testPlan1
|
||||
});
|
||||
|
||||
@@ -80,7 +80,7 @@ test.describe("Time Strip", () => {
|
||||
const activityBounds = page.locator('.activity-bounds');
|
||||
|
||||
// Goto baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
const timestrip = await test.step("Create a Time Strip", async () => {
|
||||
const createdTimeStrip = await createDomainObjectWithDefaults(page, { type: 'Time Strip' });
|
||||
|
||||
@@ -34,7 +34,7 @@ test.describe('Clock Generator CRUD Operations', () => {
|
||||
description: 'https://github.com/nasa/openmct/issues/4878'
|
||||
});
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
//Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
@@ -37,7 +37,7 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
|
||||
//TODO: This needs to be refactored
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
await page.locator('li[role="menuitem"]:has-text("Condition Set")').click();
|
||||
@@ -148,7 +148,7 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
|
||||
});
|
||||
test('condition set object can be deleted by Search Tree Actions menu on @localStorage', async ({ page }) => {
|
||||
//Navigate to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
//Assertions on loaded Condition Set in main view. This is a stateful transition step after page.goto()
|
||||
await expect(page.locator('a:has-text("Unnamed Condition Set Condition Set") >> nth=0')).toBeVisible();
|
||||
@@ -182,7 +182,7 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
|
||||
test.describe('Basic Condition Set Use', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Open a browser, navigate to the main page, and wait until all network events to resolve
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
});
|
||||
test('Can add a condition', async ({ page }) => {
|
||||
// Create a new condition set
|
||||
@@ -247,7 +247,7 @@ test.describe('Basic Condition Set Use', () => {
|
||||
});
|
||||
test('ConditionSet should output blank instead of the default value', async ({ page }) => {
|
||||
//Navigate to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
//Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
@@ -27,7 +27,7 @@ test.describe('Display Layout', () => {
|
||||
/** @type {import('../../../../appActions').CreatedObjectInfo} */
|
||||
let sineWaveObject;
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
await setRealTimeMode(page);
|
||||
|
||||
// Create Sine Wave Generator
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const utils = require('../../../../helper/faultUtils');
|
||||
const { selectInspectorTab } = require('../../../../appActions');
|
||||
|
||||
test.describe('The Fault Management Plugin using example faults', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
@@ -38,6 +39,7 @@ test.describe('The Fault Management Plugin using example faults', () => {
|
||||
test('When selecting a fault, it has an "is-selected" class and it\'s information shows in the inspector @unstable', async ({ page }) => {
|
||||
await utils.selectFaultItem(page, 1);
|
||||
|
||||
await selectInspectorTab(page, 'Fault Management Configuration');
|
||||
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();
|
||||
|
||||
@@ -52,6 +54,7 @@ test.describe('The Fault Management Plugin using example faults', () => {
|
||||
const selectedRows = page.locator('.c-fault-mgmt__list.is-selected .c-fault-mgmt__list-faultname');
|
||||
expect.soft(await selectedRows.count()).toEqual(2);
|
||||
|
||||
await selectInspectorTab(page, 'Fault Management Configuration');
|
||||
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();
|
||||
|
||||
@@ -27,7 +27,7 @@ test.describe('Flexible Layout', () => {
|
||||
let sineWaveObject;
|
||||
let clockObject;
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Create Sine Wave Generator
|
||||
sineWaveObject = await createDomainObjectWithDefaults(page, {
|
||||
|
||||
@@ -31,7 +31,7 @@ const uuid = require('uuid').v4;
|
||||
test.describe('Gauge', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Open a browser, navigate to the main page, and wait until all networkevents to resolve
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
});
|
||||
|
||||
test('Can add and remove telemetry sources @unstable', async ({ page }) => {
|
||||
|
||||
@@ -37,7 +37,7 @@ const thumbnailUrlParamsRegexp = /\?w=100&h=100/;
|
||||
test.describe('Example Imagery Object', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Create a default 'Example Imagery' object
|
||||
const exampleImagery = await createDomainObjectWithDefaults(page, { type: 'Example Imagery' });
|
||||
@@ -178,7 +178,7 @@ test.describe('Example Imagery in Display Layout', () => {
|
||||
let displayLayout;
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
displayLayout = await createDomainObjectWithDefaults(page, { type: 'Display Layout' });
|
||||
await page.goto(displayLayout.url);
|
||||
@@ -317,7 +317,7 @@ test.describe('Example Imagery in Display Layout', () => {
|
||||
test.describe('Example Imagery in Flexible layout', () => {
|
||||
let flexibleLayout;
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
flexibleLayout = await createDomainObjectWithDefaults(page, { type: 'Flexible Layout' });
|
||||
await page.goto(flexibleLayout.url);
|
||||
@@ -359,7 +359,7 @@ test.describe('Example Imagery in Flexible layout', () => {
|
||||
test.describe('Example Imagery in Tabs View', () => {
|
||||
let tabsView;
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
tabsView = await createDomainObjectWithDefaults(page, { type: 'Tabs View' });
|
||||
await page.goto(tabsView.url);
|
||||
@@ -395,7 +395,7 @@ test.describe('Example Imagery in Tabs View', () => {
|
||||
test.describe('Example Imagery in Time Strip', () => {
|
||||
let timeStripObject;
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
timeStripObject = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Time Strip'
|
||||
});
|
||||
|
||||
@@ -25,7 +25,7 @@ const { createDomainObjectWithDefaults, setStartOffset, setFixedTimeMode, setRea
|
||||
|
||||
test.describe('Testing LAD table configuration', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Create LAD table
|
||||
const ladTable = await createDomainObjectWithDefaults(page, {
|
||||
@@ -139,7 +139,7 @@ test.describe('Testing LAD table configuration', () => {
|
||||
test.describe('Testing LAD table @unstable', () => {
|
||||
let sineWaveObject;
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
await setRealTimeMode(page);
|
||||
|
||||
// Create Sine Wave Generator
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
This test suite is dedicated to tests which verify the basic operations surrounding Notebooks.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { test, expect, streamToString } = require('../../../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||
const nbUtils = require('../../../../helper/notebookUtils');
|
||||
const path = require('path');
|
||||
@@ -72,7 +72,7 @@ test.describe('Notebook section tests', () => {
|
||||
//The following test cases are associated with Notebook Sections
|
||||
test.beforeEach(async ({ page }) => {
|
||||
//Navigate to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Create Notebook
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
@@ -133,7 +133,7 @@ test.describe('Notebook page tests', () => {
|
||||
//The following test cases are associated with Notebook Pages
|
||||
test.beforeEach(async ({ page }) => {
|
||||
//Navigate to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Create Notebook
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
@@ -198,6 +198,36 @@ test.describe('Notebook page tests', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Notebook export tests', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
//Navigate to baseURL
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Create Notebook
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: NOTEBOOK_NAME
|
||||
});
|
||||
});
|
||||
test('can export notebook as text', async ({ page }) => {
|
||||
await nbUtils.enterTextEntry(page, `Foo bar entry`);
|
||||
// Click on 3 Dot Menu
|
||||
await page.locator('button[title="More options"]').click();
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
|
||||
await page.getByRole('menuitem', { name: /Export Notebook as Text/ }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
const download = await downloadPromise;
|
||||
const readStream = await download.createReadStream();
|
||||
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 all notebook entry metdata', async ({ page }) => {});
|
||||
test.fixme('can export all notebook tags', async ({ page }) => {});
|
||||
test.fixme('can export all notebook snapshots', async ({ page }) => {});
|
||||
});
|
||||
|
||||
test.describe('Notebook search tests', () => {
|
||||
test.fixme('Can search for a single result', async ({ page }) => {});
|
||||
test.fixme('Can search for many results', async ({ page }) => {});
|
||||
@@ -213,16 +243,24 @@ test.describe('Notebook entry tests', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// eslint-disable-next-line no-undef
|
||||
await page.addInitScript({ path: path.join(__dirname, '../../../../helper/', 'addInitNotebookWithUrls.js') });
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
notebookObject = await createDomainObjectWithDefaults(page, {
|
||||
type: NOTEBOOK_NAME
|
||||
});
|
||||
});
|
||||
test.fixme('When a new entry is created, it should be focused', async ({ page }) => {});
|
||||
test('When a new entry is created, it should be focused and selected', async ({ page }) => {
|
||||
// Navigate to the notebook object
|
||||
await page.goto(notebookObject.url);
|
||||
|
||||
// Click .c-notebook__drag-area
|
||||
await page.locator('.c-notebook__drag-area').click();
|
||||
await expect(page.locator('[aria-label="Notebook Entry Input"]')).toBeVisible();
|
||||
await expect(page.locator('[aria-label="Notebook Entry"]')).toHaveClass(/is-selected/);
|
||||
});
|
||||
test('When an object is dropped into a notebook, a new entry is created and it should be focused @unstable', async ({ page }) => {
|
||||
// Create Overlay Plot
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
const overlayPlot = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Overlay Plot'
|
||||
});
|
||||
|
||||
@@ -232,17 +270,17 @@ test.describe('Notebook entry tests', () => {
|
||||
// Reveal the notebook in the tree
|
||||
await page.getByTitle('Show selected item in tree').click();
|
||||
|
||||
await page.dragAndDrop('role=treeitem[name=/Dropped Overlay Plot/]', '.c-notebook__drag-area');
|
||||
await page.dragAndDrop(`role=treeitem[name=/${overlayPlot.name}/]`, '.c-notebook__drag-area');
|
||||
|
||||
const embed = page.locator('.c-ne__embed__link');
|
||||
const embedName = await embed.textContent();
|
||||
|
||||
await expect(embed).toHaveClass(/icon-plot-overlay/);
|
||||
expect(embedName).toBe('Dropped Overlay Plot');
|
||||
expect(embedName).toBe(overlayPlot.name);
|
||||
});
|
||||
test('When an object is dropped into a notebooks existing entry, it should be focused @unstable', async ({ page }) => {
|
||||
// Create Overlay Plot
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
const overlayPlot = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Overlay Plot'
|
||||
});
|
||||
|
||||
@@ -253,17 +291,35 @@ test.describe('Notebook entry tests', () => {
|
||||
await page.getByTitle('Show selected item in tree').click();
|
||||
|
||||
await nbUtils.enterTextEntry(page, 'Entry to drop into');
|
||||
await page.dragAndDrop('role=treeitem[name=/Dropped Overlay Plot/]', 'text=Entry to drop into');
|
||||
await page.dragAndDrop(`role=treeitem[name=/${overlayPlot.name}/]`, 'text=Entry to drop into');
|
||||
|
||||
const existingEntry = page.locator('.c-ne__content', { has: page.locator('text="Entry to drop into"') });
|
||||
const embed = existingEntry.locator('.c-ne__embed__link');
|
||||
const embedName = await embed.textContent();
|
||||
|
||||
await expect(embed).toHaveClass(/icon-plot-overlay/);
|
||||
expect(embedName).toBe('Dropped Overlay Plot');
|
||||
expect(embedName).toBe(overlayPlot.name);
|
||||
});
|
||||
test.fixme('new entries persist through navigation events without save', async ({ page }) => {});
|
||||
test.fixme('previous and new entries can be deleted', async ({ page }) => {});
|
||||
test('previous and new entries can be deleted', async ({ page }) => {
|
||||
// Navigate to the notebook object
|
||||
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 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();
|
||||
});
|
||||
test('when a valid link is entered into a notebook entry, it becomes clickable when viewing', async ({ page }) => {
|
||||
const TEST_LINK = 'http://www.google.com';
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ test.describe('Snapshot Menu tests', () => {
|
||||
test.describe('Snapshot Container tests', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
//Navigate to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Create Notebook
|
||||
// const notebook = await createDomainObjectWithDefaults(page, {
|
||||
|
||||
@@ -26,46 +26,49 @@ This test suite is dedicated to tests which verify the basic operations surround
|
||||
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||
const nbUtils = require('../../../../helper/notebookUtils');
|
||||
|
||||
test.describe('Notebook Tests with CouchDB @couchdb', () => {
|
||||
let testNotebook;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
//Navigate to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Create Notebook
|
||||
testNotebook = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Notebook',
|
||||
name: "TestNotebook"
|
||||
});
|
||||
testNotebook = await createDomainObjectWithDefaults(page, {type: 'Notebook' });
|
||||
await page.goto(testNotebook.url, { waitUntil: 'networkidle'});
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
// Collect all request events to count and assert after notebook action
|
||||
let addingNotebookElementsRequests = [];
|
||||
page.on('request', (request) => addingNotebookElementsRequests.push(request));
|
||||
let notebookElementsRequests = [];
|
||||
page.on('request', (request) => notebookElementsRequests.push(request));
|
||||
|
||||
//Clicking Add Page generates
|
||||
let [notebookUrlRequest, allDocsRequest] = await Promise.all([
|
||||
// Waits for the next request with the specified url
|
||||
page.waitForRequest(`**/openmct/${testNotebook.uuid}`),
|
||||
page.waitForRequest('**/openmct/_all_docs?include_docs=true'),
|
||||
// Triggers the request
|
||||
page.click('[aria-label="Add Page"]'),
|
||||
// Ensures that there are no other network requests
|
||||
page.waitForLoadState('networkidle')
|
||||
page.click('[aria-label="Add Page"]')
|
||||
]);
|
||||
// Ensures that there are no other network requests
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Assert that only two requests are made
|
||||
// Network Requests are:
|
||||
// 1) The actual POST to create the page
|
||||
// 2) The shared worker event from 👆 request
|
||||
expect(addingNotebookElementsRequests.length).toBe(2);
|
||||
expect(notebookElementsRequests.length).toBe(2);
|
||||
|
||||
// Assert on request object
|
||||
expect(notebookUrlRequest.postDataJSON().metadata.name).toBe('TestNotebook');
|
||||
expect(notebookUrlRequest.postDataJSON().metadata.name).toBe(testNotebook.name);
|
||||
expect(notebookUrlRequest.postDataJSON().model.persisted).toBeGreaterThanOrEqual(notebookUrlRequest.postDataJSON().model.modified);
|
||||
expect(allDocsRequest.postDataJSON().keys).toContain(testNotebook.uuid);
|
||||
|
||||
@@ -73,13 +76,10 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => {
|
||||
// Network Requests are:
|
||||
// 1) The actual POST to create the entry
|
||||
// 2) The shared worker event from 👆 POST request
|
||||
addingNotebookElementsRequests = [];
|
||||
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"]').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"]').fill(`First Entry`);
|
||||
await page.locator('[aria-label="Notebook Entry Input"]').press('Enter');
|
||||
notebookElementsRequests = [];
|
||||
await nbUtils.enterTextEntry(page, 'First Entry');
|
||||
await page.waitForLoadState('networkidle');
|
||||
expect(addingNotebookElementsRequests.length).toBeLessThanOrEqual(2);
|
||||
expect(notebookElementsRequests.length).toBeLessThanOrEqual(2);
|
||||
|
||||
// Add some tags
|
||||
// Network Requests are for each tag creation are:
|
||||
@@ -95,32 +95,17 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => {
|
||||
// 10) Entry is timestamped
|
||||
// 11) The shared worker event from 👆 POST request
|
||||
|
||||
addingNotebookElementsRequests = [];
|
||||
await page.hover(`button:has-text("Add Tag")`);
|
||||
await page.locator(`button:has-text("Add Tag")`).click();
|
||||
await page.locator('[placeholder="Type to select tag"]').click();
|
||||
await page.locator('[aria-label="Autocomplete Options"] >> text=Driving').click();
|
||||
await page.waitForSelector('[aria-label="Tag"]:has-text("Driving")');
|
||||
page.waitForLoadState('networkidle');
|
||||
expect(filterNonFetchRequests(addingNotebookElementsRequests).length).toBeLessThanOrEqual(11);
|
||||
notebookElementsRequests = [];
|
||||
await addTagAndAwaitNetwork(page, 'Driving');
|
||||
expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(11);
|
||||
|
||||
addingNotebookElementsRequests = [];
|
||||
await page.hover(`button:has-text("Add Tag")`);
|
||||
await page.locator(`button:has-text("Add Tag")`).click();
|
||||
await page.locator('[placeholder="Type to select tag"]').click();
|
||||
await page.locator('[aria-label="Autocomplete Options"] >> text=Drilling').click();
|
||||
await page.waitForSelector('[aria-label="Tag"]:has-text("Drilling")');
|
||||
page.waitForLoadState('networkidle');
|
||||
expect(filterNonFetchRequests(addingNotebookElementsRequests).length).toBeLessThanOrEqual(11);
|
||||
notebookElementsRequests = [];
|
||||
await addTagAndAwaitNetwork(page, 'Drilling');
|
||||
expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(11);
|
||||
|
||||
addingNotebookElementsRequests = [];
|
||||
await page.hover(`button:has-text("Add Tag")`);
|
||||
await page.locator(`button:has-text("Add Tag")`).click();
|
||||
await page.locator('[placeholder="Type to select tag"]').click();
|
||||
await page.locator('[aria-label="Autocomplete Options"] >> text=Science').click();
|
||||
await page.waitForSelector('[aria-label="Tag"]:has-text("Science")');
|
||||
page.waitForLoadState('networkidle');
|
||||
expect(filterNonFetchRequests(addingNotebookElementsRequests).length).toBeLessThanOrEqual(11);
|
||||
notebookElementsRequests = [];
|
||||
await addTagAndAwaitNetwork(page, 'Science');
|
||||
expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(11);
|
||||
|
||||
// Delete all the tags
|
||||
// Network requests are:
|
||||
@@ -129,58 +114,25 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => {
|
||||
// 3) Timestamp update on entry
|
||||
// 4) The shared worker event from 👆 POST request
|
||||
// This happens for 3 tags so 12 requests
|
||||
addingNotebookElementsRequests = [];
|
||||
await page.hover('[aria-label="Tag"]:has-text("Driving")');
|
||||
await page.locator('[aria-label="Remove tag Driving"]').click();
|
||||
await page.waitForSelector('[aria-label="Tag"]:has-text("Driving")', {state: 'hidden'});
|
||||
await page.hover('[aria-label="Tag"]:has-text("Drilling")');
|
||||
await page.locator('[aria-label="Remove tag Drilling"]').click();
|
||||
await page.waitForSelector('[aria-label="Tag"]:has-text("Drilling")', {state: 'hidden'});
|
||||
page.hover('[aria-label="Tag"]:has-text("Science")');
|
||||
await page.locator('[aria-label="Remove tag Science"]').click();
|
||||
await page.waitForSelector('[aria-label="Tag"]:has-text("Science")', {state: 'hidden'});
|
||||
page.waitForLoadState('networkidle');
|
||||
expect(filterNonFetchRequests(addingNotebookElementsRequests).length).toBeLessThanOrEqual(12);
|
||||
notebookElementsRequests = [];
|
||||
await removeTagAndAwaitNetwork(page, 'Driving');
|
||||
await removeTagAndAwaitNetwork(page, 'Drilling');
|
||||
await removeTagAndAwaitNetwork(page, 'Science');
|
||||
expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(12);
|
||||
|
||||
// Add two more pages
|
||||
await page.click('[aria-label="Add Page"]');
|
||||
await page.click('[aria-label="Add Page"]');
|
||||
|
||||
// Add three entries
|
||||
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"]').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"]').fill(`First Entry`);
|
||||
await page.locator('[aria-label="Notebook Entry Input"]').press('Enter');
|
||||
|
||||
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"] >> nth=1').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"] >> nth=1').fill(`Second Entry`);
|
||||
await page.locator('[aria-label="Notebook Entry Input"] >> nth=1').press('Enter');
|
||||
|
||||
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"] >> nth=2').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"] >> nth=2').fill(`Third Entry`);
|
||||
await page.locator('[aria-label="Notebook Entry Input"] >> nth=2').press('Enter');
|
||||
await nbUtils.enterTextEntry(page, 'First Entry');
|
||||
await nbUtils.enterTextEntry(page, 'Second Entry');
|
||||
await nbUtils.enterTextEntry(page, 'Third Entry');
|
||||
|
||||
// Add three tags
|
||||
await page.hover(`button:has-text("Add Tag")`);
|
||||
await page.locator(`button:has-text("Add Tag")`).click();
|
||||
await page.locator('[placeholder="Type to select tag"]').click();
|
||||
await page.locator('[aria-label="Autocomplete Options"] >> text=Science').click();
|
||||
await page.waitForSelector('[aria-label="Tag"]:has-text("Science")');
|
||||
|
||||
await page.hover(`button:has-text("Add Tag")`);
|
||||
await page.locator(`button:has-text("Add Tag")`).click();
|
||||
await page.locator('[placeholder="Type to select tag"]').click();
|
||||
await page.locator('[aria-label="Autocomplete Options"] >> text=Drilling').click();
|
||||
await page.waitForSelector('[aria-label="Tag"]:has-text("Drilling")');
|
||||
|
||||
await page.hover(`button:has-text("Add Tag")`);
|
||||
await page.locator(`button:has-text("Add Tag")`).click();
|
||||
await page.locator('[placeholder="Type to select tag"]').click();
|
||||
await page.locator('[aria-label="Autocomplete Options"] >> text=Driving').click();
|
||||
await page.waitForSelector('[aria-label="Tag"]:has-text("Driving")');
|
||||
page.waitForLoadState('networkidle');
|
||||
await addTagAndAwaitNetwork(page, 'Science');
|
||||
await addTagAndAwaitNetwork(page, 'Drilling');
|
||||
await addTagAndAwaitNetwork(page, 'Driving');
|
||||
|
||||
// Add a fourth entry
|
||||
// Network requests are:
|
||||
@@ -188,14 +140,11 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => {
|
||||
// 2) The shared worker event from 👆 POST request
|
||||
// 3) Timestamp update on entry
|
||||
// 4) The shared worker event from 👆 POST request
|
||||
addingNotebookElementsRequests = [];
|
||||
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"] >> nth=3').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"] >> nth=3').fill(`Fourth Entry`);
|
||||
await page.locator('[aria-label="Notebook Entry Input"] >> nth=3').press('Enter');
|
||||
notebookElementsRequests = [];
|
||||
await nbUtils.enterTextEntry(page, 'Fourth Entry');
|
||||
page.waitForLoadState('networkidle');
|
||||
|
||||
expect(filterNonFetchRequests(addingNotebookElementsRequests).length).toBeLessThanOrEqual(4);
|
||||
expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(4);
|
||||
|
||||
// Add a fifth entry
|
||||
// Network requests are:
|
||||
@@ -203,28 +152,22 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => {
|
||||
// 2) The shared worker event from 👆 POST request
|
||||
// 3) Timestamp update on entry
|
||||
// 4) The shared worker event from 👆 POST request
|
||||
addingNotebookElementsRequests = [];
|
||||
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"] >> nth=4').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"] >> nth=4').fill(`Fifth Entry`);
|
||||
await page.locator('[aria-label="Notebook Entry Input"] >> nth=4').press('Enter');
|
||||
notebookElementsRequests = [];
|
||||
await nbUtils.enterTextEntry(page, 'Fifth Entry');
|
||||
page.waitForLoadState('networkidle');
|
||||
|
||||
expect(filterNonFetchRequests(addingNotebookElementsRequests).length).toBeLessThanOrEqual(4);
|
||||
expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(4);
|
||||
|
||||
// Add a sixth entry
|
||||
// 1) Send POST to add new entry
|
||||
// 2) The shared worker event from 👆 POST request
|
||||
// 3) Timestamp update on entry
|
||||
// 4) The shared worker event from 👆 POST request
|
||||
addingNotebookElementsRequests = [];
|
||||
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"] >> nth=5').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"] >> nth=5').fill(`Sixth Entry`);
|
||||
await page.locator('[aria-label="Notebook Entry Input"] >> nth=5').press('Enter');
|
||||
notebookElementsRequests = [];
|
||||
await nbUtils.enterTextEntry(page, 'Sixth Entry');
|
||||
page.waitForLoadState('networkidle');
|
||||
|
||||
expect(filterNonFetchRequests(addingNotebookElementsRequests).length).toBeLessThanOrEqual(4);
|
||||
expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(4);
|
||||
});
|
||||
|
||||
test('Search tests', async ({ page }) => {
|
||||
@@ -233,35 +176,21 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => {
|
||||
description: 'https://github.com/akhenry/openmct-yamcs/issues/69'
|
||||
});
|
||||
await page.getByText('Annotations').click();
|
||||
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"]').click();
|
||||
await page.locator('[aria-label="Notebook Entry Input"]').fill(`First Entry`);
|
||||
await page.locator('[aria-label="Notebook Entry Input"]').press('Enter');
|
||||
await nbUtils.enterTextEntry(page, 'First Entry');
|
||||
|
||||
// Add three tags
|
||||
await page.hover(`button:has-text("Add Tag")`);
|
||||
await page.locator(`button:has-text("Add Tag")`).click();
|
||||
await page.locator('[placeholder="Type to select tag"]').click();
|
||||
await page.locator('[aria-label="Autocomplete Options"] >> text=Science').click();
|
||||
await page.waitForSelector('[aria-label="Tag"]:has-text("Science")');
|
||||
|
||||
await page.hover(`button:has-text("Add Tag")`);
|
||||
await page.locator(`button:has-text("Add Tag")`).click();
|
||||
await page.locator('[placeholder="Type to select tag"]').click();
|
||||
await page.locator('[aria-label="Autocomplete Options"] >> text=Drilling').click();
|
||||
await page.waitForSelector('[aria-label="Tag"]:has-text("Drilling")');
|
||||
|
||||
await page.hover(`button:has-text("Add Tag")`);
|
||||
await page.locator(`button:has-text("Add Tag")`).click();
|
||||
await page.locator('[placeholder="Type to select tag"]').click();
|
||||
await page.locator('[aria-label="Autocomplete Options"] >> text=Driving').click();
|
||||
await page.waitForSelector('[aria-label="Tag"]:has-text("Driving")');
|
||||
await addTagAndAwaitNetwork(page, 'Science');
|
||||
await addTagAndAwaitNetwork(page, 'Drilling');
|
||||
await addTagAndAwaitNetwork(page, 'Driving');
|
||||
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
|
||||
//Partial match for "Science" should only return Science
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Sc');
|
||||
await expect(page.locator('[aria-label="Search Result"]').first()).toContainText("Science");
|
||||
await expect(page.locator('[aria-label="Search Result"]').first()).not.toContainText("Driving");
|
||||
await expect(page.locator('[aria-label="Search Result"]').first()).not.toContainText("Drilling");
|
||||
|
||||
//Searching for a tag which does not exist should return an empty result
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Xq');
|
||||
await expect(page.locator('text=No results found')).toBeVisible();
|
||||
@@ -275,3 +204,40 @@ function filterNonFetchRequests(requests) {
|
||||
return (request.resourceType() === 'fetch');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a tag to a notebook entry by providing a tagName.
|
||||
* Reduces indeterminism by waiting until all necessary requests are completed.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} tagName
|
||||
*/
|
||||
async function addTagAndAwaitNetwork(page, tagName) {
|
||||
await page.hover(`button:has-text("Add Tag")`);
|
||||
await page.locator(`button:has-text("Add Tag")`).click();
|
||||
await page.locator('[placeholder="Type to select tag"]').click();
|
||||
await Promise.all([
|
||||
// Waits for the next request with the specified url
|
||||
page.waitForRequest('**/openmct/_all_docs?include_docs=true'),
|
||||
// Triggers the request
|
||||
page.locator(`[aria-label="Autocomplete Options"] >> text=${tagName}`).click(),
|
||||
expect(page.locator(`[aria-label="Tag"]:has-text("${tagName}")`)).toBeVisible()
|
||||
]);
|
||||
await page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a tag to a notebook entry by providing a tagName.
|
||||
* Reduces indeterminism by waiting until all necessary requests are completed.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {string} tagName
|
||||
*/
|
||||
async function removeTagAndAwaitNetwork(page, tagName) {
|
||||
await page.hover(`[aria-label="Tag"]:has-text("${tagName}")`);
|
||||
await Promise.all([
|
||||
page.locator(`[aria-label="Remove tag ${tagName}"]`).click(),
|
||||
//With this pattern, we're awaiting the response but asserting on the request payload.
|
||||
page.waitForResponse(resp => resp.request().postData().includes(`"_deleted":true`) && resp.status() === 201)
|
||||
]);
|
||||
await expect(page.locator(`[aria-label="Tag"]:has-text("${tagName}")`)).toBeHidden();
|
||||
await page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { test, expect, streamToString } = require('../../../../pluginFixtures');
|
||||
const { openObjectTreeContextMenu, createDomainObjectWithDefaults } = require('../../../../appActions');
|
||||
const path = require('path');
|
||||
const nbUtils = require('../../../../helper/notebookUtils');
|
||||
@@ -169,13 +169,40 @@ test.describe('Restricted Notebook with a page locked and with an embed @addInit
|
||||
|
||||
});
|
||||
|
||||
test.describe('can export restricted notebook as text', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await startAndAddRestrictedNotebookObject(page);
|
||||
});
|
||||
|
||||
test('basic functionality ', async ({ page }) => {
|
||||
await nbUtils.enterTextEntry(page, `Foo bar entry`);
|
||||
// Click on 3 Dot Menu
|
||||
await page.locator('button[title="More options"]').click();
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
|
||||
await page.getByRole('menuitem', { name: /Export Notebook as Text/ }).click();
|
||||
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
const download = await downloadPromise;
|
||||
const readStream = await download.createReadStream();
|
||||
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 all notebook entry metdata', async ({ page }) => {});
|
||||
test.fixme('can export all notebook tags', async ({ page }) => {});
|
||||
test.fixme('can export all notebook snapshots', async ({ page }) => {});
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function startAndAddRestrictedNotebookObject(page) {
|
||||
// eslint-disable-next-line no-undef
|
||||
await page.addInitScript({ path: path.join(__dirname, '../../../../helper/', 'addInitRestrictedNotebook.js') });
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
return createDomainObjectWithDefaults(page, { type: CUSTOM_NAME });
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
This test suite is dedicated to tests which verify form functionality.
|
||||
This test suite is dedicated to tests which verify notebook tag functionality.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
@@ -34,9 +34,6 @@ const nbUtils = require('../../../../helper/notebookUtils');
|
||||
* @param {number} [iterations = 1] - the number of entries to create
|
||||
*/
|
||||
async function createNotebookAndEntry(page, iterations = 1) {
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
const notebook = createDomainObjectWithDefaults(page, { type: 'Notebook' });
|
||||
|
||||
for (let iteration = 0; iteration < iterations; iteration++) {
|
||||
@@ -81,12 +78,13 @@ async function createNotebookEntryAndTags(page, iterations = 1) {
|
||||
}
|
||||
|
||||
test.describe('Tagging in Notebooks @addInit', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
});
|
||||
test('Can load tags', async ({ page }) => {
|
||||
await createNotebookAndEntry(page);
|
||||
|
||||
// TODO can be removed with fix for https://github.com/nasa/openmct/issues/6411
|
||||
await page.locator('[aria-label="Notebook Entry"].is-selected div.c-ne__text').click();
|
||||
|
||||
await selectInspectorTab(page, 'Annotations');
|
||||
|
||||
await page.locator('button:has-text("Add Tag")').click();
|
||||
@@ -110,12 +108,24 @@ test.describe('Tagging in Notebooks @addInit', () => {
|
||||
await expect(page.locator('[aria-label="Autocomplete Options"]')).not.toContainText("Driving");
|
||||
await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Drilling");
|
||||
});
|
||||
test('Can add tags with blank entry', async ({ page }) => {
|
||||
await createDomainObjectWithDefaults(page, { type: 'Notebook' });
|
||||
await selectInspectorTab(page, 'Annotations');
|
||||
|
||||
await nbUtils.enterTextEntry(page, '');
|
||||
await page.hover(`button:has-text("Add Tag")`);
|
||||
await page.locator(`button:has-text("Add Tag")`).click();
|
||||
|
||||
// Click inside the tag search input
|
||||
await page.locator('[placeholder="Type to select tag"]').click();
|
||||
// Select the "Driving" tag
|
||||
await page.locator('[aria-label="Autocomplete Options"] >> text=Driving').click();
|
||||
|
||||
await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Driving");
|
||||
});
|
||||
test('Can cancel adding tags', async ({ page }) => {
|
||||
await createNotebookAndEntry(page);
|
||||
|
||||
// TODO can be removed with fix for https://github.com/nasa/openmct/issues/6411
|
||||
await page.locator('[aria-label="Notebook Entry"].is-selected div.c-ne__text').click();
|
||||
|
||||
await selectInspectorTab(page, 'Annotations');
|
||||
|
||||
// Test canceling adding a tag after we click "Type to select tag"
|
||||
@@ -204,7 +214,7 @@ test.describe('Tagging in Notebooks @addInit', () => {
|
||||
await page.locator('button[title="More options"]').click();
|
||||
await page.locator('li[title="Remove this object from its containing object."]').click();
|
||||
await page.locator('button:has-text("OK")').click();
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Unnamed');
|
||||
await expect(page.locator('text=No results found')).toBeVisible();
|
||||
@@ -215,37 +225,13 @@ test.describe('Tagging in Notebooks @addInit', () => {
|
||||
});
|
||||
test('Tags persist across reload', async ({ page }) => {
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
|
||||
const clock = await createDomainObjectWithDefaults(page, { type: 'Clock' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
const ITERATIONS = 4;
|
||||
const notebook = await createNotebookEntryAndTags(page, ITERATIONS);
|
||||
await page.goto(notebook.url);
|
||||
|
||||
for (let iteration = 0; iteration < ITERATIONS; iteration++) {
|
||||
const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`;
|
||||
await expect(page.locator(entryLocator)).toContainText("Science");
|
||||
await expect(page.locator(entryLocator)).toContainText("Driving");
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.goto('./#/browse/mine?hideTree=false'),
|
||||
page.click('.c-disclosure-triangle')
|
||||
]);
|
||||
|
||||
const treePane = page.getByRole('tree', {
|
||||
name: 'Main Tree'
|
||||
});
|
||||
// Click Clock
|
||||
await treePane.getByRole('treeitem', {
|
||||
name: clock.name
|
||||
}).click();
|
||||
// Click Notebook
|
||||
await page.getByRole('treeitem', {
|
||||
name: notebook.name
|
||||
}).click();
|
||||
|
||||
// Verify tags are present
|
||||
for (let iteration = 0; iteration < ITERATIONS; iteration++) {
|
||||
const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`;
|
||||
await expect(page.locator(entryLocator)).toContainText("Science");
|
||||
@@ -253,14 +239,9 @@ test.describe('Tagging in Notebooks @addInit', () => {
|
||||
}
|
||||
|
||||
//Reload Page
|
||||
await Promise.all([
|
||||
page.reload(),
|
||||
page.waitForLoadState('networkidle')
|
||||
]);
|
||||
|
||||
// Click Notebook
|
||||
await page.click(`text="${notebook.name}"`);
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Verify tags persist across reload
|
||||
for (let iteration = 0; iteration < ITERATIONS; iteration++) {
|
||||
const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`;
|
||||
await expect(page.locator(entryLocator)).toContainText("Science");
|
||||
@@ -270,9 +251,6 @@ test.describe('Tagging in Notebooks @addInit', () => {
|
||||
test('Can cancel adding a tag', async ({ page }) => {
|
||||
await createNotebookAndEntry(page);
|
||||
|
||||
// TODO can be removed with fix for https://github.com/nasa/openmct/issues/6411
|
||||
await page.locator('[aria-label="Notebook Entry"].is-selected div.c-ne__text').click();
|
||||
|
||||
await selectInspectorTab(page, 'Annotations');
|
||||
|
||||
// Click on the "Add Tag" button
|
||||
|
||||
@@ -44,7 +44,7 @@ test.describe('Operator Status', () => {
|
||||
await page.addInitScript({ path: path.join(__dirname, '../../../../helper/', 'addInitExampleUser.js')});
|
||||
// eslint-disable-next-line no-undef
|
||||
await page.addInitScript({ path: path.join(__dirname, '../../../../helper/', 'addInitOperatorStatus.js')});
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
});
|
||||
|
||||
// verify that operator status is visible
|
||||
|
||||
@@ -40,7 +40,7 @@ test.describe('Autoscale', () => {
|
||||
//This is necessary due to the size of the test suite.
|
||||
test.slow();
|
||||
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
await setTimeRange(page);
|
||||
|
||||
@@ -73,6 +73,7 @@ test.describe('Autoscale', () => {
|
||||
const canvas = page.locator('canvas').nth(1);
|
||||
|
||||
await canvas.hover({trial: true});
|
||||
await expect(page.locator('.js-series-data-loaded')).toBeVisible();
|
||||
|
||||
expect.soft(await canvas.screenshot()).toMatchSnapshot('autoscale-canvas-prepan.png', { animations: 'disabled' });
|
||||
|
||||
@@ -172,7 +173,7 @@ async function createSinewaveOverlayPlot(page, myItemsFolderName) {
|
||||
*/
|
||||
async function turnOffAutoscale(page) {
|
||||
// uncheck autoscale
|
||||
await page.getByRole('listitem').filter({ hasText: 'Auto scale' }).getByRole('checkbox').uncheck();
|
||||
await page.getByRole('checkbox', { name: 'Auto scale' }).uncheck();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -182,14 +183,9 @@ async function turnOffAutoscale(page) {
|
||||
*/
|
||||
async function setUserDefinedMinAndMax(page, min, max) {
|
||||
// set minimum value
|
||||
const minRangeInput = page.getByRole('listitem').filter({ hasText: 'Minimum Value' }).locator('input[type="number"]');
|
||||
await minRangeInput.click();
|
||||
await minRangeInput.fill(min);
|
||||
|
||||
await page.getByRole('spinbutton').first().fill(min);
|
||||
// set maximum value
|
||||
const maxRangeInput = page.getByRole('listitem').filter({ hasText: 'Maximum Value' }).locator('input[type="number"]');
|
||||
await maxRangeInput.click();
|
||||
await maxRangeInput.fill(max);
|
||||
await page.getByRole('spinbutton').nth(1).fill(max);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -76,7 +76,7 @@ test.describe('Log plot tests', () => {
|
||||
*/
|
||||
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: 'networkidle' });
|
||||
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.
|
||||
@@ -147,7 +147,7 @@ async function makeOverlayPlot(page, myItemsFolderName) {
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function testRegularTicks(page) {
|
||||
const yTicks = await page.locator('.gl-plot-y-tick-label');
|
||||
const yTicks = page.locator('.gl-plot-y-tick-label');
|
||||
expect(await yTicks.count()).toBe(7);
|
||||
await expect(yTicks.nth(0)).toHaveText('-2');
|
||||
await expect(yTicks.nth(1)).toHaveText('0');
|
||||
@@ -162,7 +162,7 @@ async function testRegularTicks(page) {
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function testLogTicks(page) {
|
||||
const yTicks = await page.locator('.gl-plot-y-tick-label');
|
||||
const yTicks = page.locator('.gl-plot-y-tick-label');
|
||||
expect(await yTicks.count()).toBe(9);
|
||||
await expect(yTicks.nth(0)).toHaveText('-2.98');
|
||||
await expect(yTicks.nth(1)).toHaveText('-1.51');
|
||||
@@ -180,27 +180,24 @@ async function testLogTicks(page) {
|
||||
*/
|
||||
async function enableEditMode(page) {
|
||||
// turn on edit mode
|
||||
await page.locator('text=Unnamed Overlay Plot Snapshot >> button').nth(3).click();
|
||||
await expect(await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1)).toBeVisible();
|
||||
await page.getByRole('button', { name: 'Edit' }).click();
|
||||
await expect(page.getByRole('button', { name: 'Save' })).toBeVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function enableLogMode(page) {
|
||||
// turn on log mode
|
||||
await expect(page.getByRole('listitem').filter({ hasText: 'Log mode' }).getByRole('checkbox')).not.toBeChecked();
|
||||
await page.getByRole('listitem').filter({ hasText: 'Log mode' }).getByRole('checkbox').check();
|
||||
// await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"]').first().check();
|
||||
await expect(page.getByRole('checkbox', { name: 'Log mode' })).not.toBeChecked();
|
||||
await page.getByRole('checkbox', { name: 'Log mode' }).check();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function disableLogMode(page) {
|
||||
// turn off log mode
|
||||
await expect(page.getByRole('listitem').filter({ hasText: 'Log mode' }).getByRole('checkbox')).toBeChecked();
|
||||
await page.getByRole('listitem').filter({ hasText: 'Log mode' }).getByRole('checkbox').uncheck();
|
||||
await expect(page.getByRole('checkbox', { name: 'Log mode' })).toBeChecked();
|
||||
await page.getByRole('checkbox', { name: 'Log mode' }).uncheck();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -84,7 +84,7 @@ test.describe('Handle missing object for plots', () => {
|
||||
*/
|
||||
async function makeStackedPlot(page, myItemsFolderName) {
|
||||
// fresh page with time range from 2022-03-29 22:00:00.000Z to 2022-03-29 22:00:30.000Z
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// create stacked plot
|
||||
await page.locator('button.c-create-button').click();
|
||||
|
||||
@@ -26,11 +26,11 @@ necessarily be used for reference when writing new tests in this area.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults, selectInspectorTab } = require('../../../../appActions');
|
||||
const { createDomainObjectWithDefaults, getCanvasPixels, selectInspectorTab, waitForPlotsToRender } = require('../../../../appActions');
|
||||
|
||||
test.describe('Overlay Plot', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
});
|
||||
|
||||
test('Plot legend color is in sync with plot series color', async ({ page }) => {
|
||||
@@ -52,14 +52,9 @@ test.describe('Overlay Plot', () => {
|
||||
await page.locator('li.c-tree__item.menus-to-left .c-disclosure-triangle').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
|
||||
const element = await page.waitForSelector('.plot-series-color-swatch');
|
||||
const color = await element.evaluate((el) => {
|
||||
return window.getComputedStyle(el).getPropertyValue('background-color');
|
||||
});
|
||||
|
||||
expect(color).toBe('rgb(255, 166, 61)');
|
||||
const seriesColorSwatch = page.locator('.gl-plot-label > .plot-series-color-swatch');
|
||||
await expect(seriesColorSwatch).toHaveCSS('background-color', 'rgb(255, 166, 61)');
|
||||
});
|
||||
|
||||
test('Limit lines persist when series is moved to another Y Axis and on refresh', async ({ page }) => {
|
||||
@@ -214,62 +209,27 @@ test.describe('Overlay Plot', () => {
|
||||
});
|
||||
|
||||
await page.goto(overlayPlot.url);
|
||||
// Wait for plot series data to load and be drawn
|
||||
await waitForPlotsToRender(page);
|
||||
await page.click('button[title="Edit"]');
|
||||
|
||||
await selectInspectorTab(page, 'Elements');
|
||||
|
||||
await page.locator(`#inspector-elements-tree >> text=${swgA.name}`).click();
|
||||
|
||||
const plotPixelSize = await getCanvasPixelsWithData(page);
|
||||
const plotPixels = await getCanvasPixels(page, '.js-overlay canvas');
|
||||
const plotPixelSize = plotPixels.length;
|
||||
expect(plotPixelSize).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function getCanvasPixelsWithData(page) {
|
||||
const getTelemValuePromise = new Promise(resolve => page.exposeFunction('getCanvasValue', resolve));
|
||||
|
||||
await page.evaluate(() => {
|
||||
// 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)
|
||||
let data;
|
||||
let canvas;
|
||||
let ctx;
|
||||
canvas = document.querySelector('.js-overlay canvas');
|
||||
ctx = canvas.getContext('2d');
|
||||
data = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
|
||||
const imageDataValues = Object.values(data);
|
||||
let plotPixels = [];
|
||||
// Each pixel consists of four values within the ImageData.data array. The for loop iterates by multiples of four.
|
||||
// The values associated with each pixel are R (red), G (green), B (blue), and A (alpha), in that order.
|
||||
for (let i = 0; i < imageDataValues.length;) {
|
||||
if (imageDataValues[i] > 0) {
|
||||
plotPixels.push({
|
||||
startIndex: i,
|
||||
endIndex: i + 3,
|
||||
value: `rgb(${imageDataValues[i]}, ${imageDataValues[i + 1]}, ${imageDataValues[i + 2]}, ${imageDataValues[i + 3]})`
|
||||
});
|
||||
}
|
||||
|
||||
i = i + 4;
|
||||
|
||||
}
|
||||
|
||||
window.getCanvasValue(plotPixels.length);
|
||||
});
|
||||
|
||||
return getTelemValuePromise;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Asserts that limit lines exist and are visible
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function assertLimitLinesExistAndAreVisible(page) {
|
||||
// Wait for plot series data to load
|
||||
await expect(page.locator('.js-series-data-loaded')).toBeVisible();
|
||||
await waitForPlotsToRender(page);
|
||||
// Wait for limit lines to be created
|
||||
await page.waitForSelector('.js-limit-area', { state: 'attached' });
|
||||
const limitLineCount = await page.locator('.c-plot-limit-line').count();
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2023, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
Tests to verify log plot functionality. Note this test suite if very much under active development and should not
|
||||
necessarily be used for reference when writing new tests in this area.
|
||||
*/
|
||||
|
||||
const { selectInspectorTab } = require('../../../../appActions');
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
|
||||
test.describe('Legend color in sync with plot color', () => {
|
||||
test('Testing', async ({ page }) => {
|
||||
await makeOverlayPlot(page);
|
||||
|
||||
// navigate to plot series color palette
|
||||
await page.click('.l-browse-bar__actions__edit');
|
||||
await selectInspectorTab(page, 'Config');
|
||||
|
||||
await page.locator('li.c-tree__item.menus-to-left .c-disclosure-triangle').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
|
||||
const element = await page.waitForSelector('.plot-series-color-swatch');
|
||||
const color = await element.evaluate((el) => {
|
||||
return window.getComputedStyle(el).getPropertyValue('background-color');
|
||||
});
|
||||
|
||||
expect(color).toBe('rgb(255, 166, 61)');
|
||||
});
|
||||
});
|
||||
|
||||
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.locator('text=Save and Finish Editing').click(),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
//Wait until Save Banner is gone
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached' });
|
||||
}
|
||||
|
||||
async function makeOverlayPlot(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: 'networkidle' });
|
||||
|
||||
// create overlay plot
|
||||
|
||||
await page.locator('button.c-create-button').click();
|
||||
await page.locator('li[role="menuitem"]:has-text("Overlay Plot")').click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation({ waitUntil: 'networkidle'}),
|
||||
page.locator('button:has-text("OK")').click(),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
//Wait until Save Banner is gone
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
||||
|
||||
// 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();
|
||||
|
||||
// Click OK to make generator
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation({ waitUntil: 'networkidle'}),
|
||||
page.locator('button:has-text("OK")').click(),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
//Wait until Save Banner is gone
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
||||
|
||||
// click on overlay plot
|
||||
|
||||
await page.locator('text=Open MCT My Items >> span').nth(3).click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=Unnamed Overlay Plot').first().click()
|
||||
]);
|
||||
}
|
||||
@@ -26,26 +26,25 @@
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults} = require('../../../../appActions');
|
||||
const { createDomainObjectWithDefaults, getCanvasPixels } = require('../../../../appActions');
|
||||
|
||||
test.describe('Plot Integrity Testing @unstable', () => {
|
||||
test.describe('Plot Rendering', () => {
|
||||
let sineWaveGeneratorObject;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
//Open a browser, navigate to the main page, and wait until all networkevents to resolve
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
// Open a browser, navigate to the main page, and wait until all networkevents to resolve
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
sineWaveGeneratorObject = await createDomainObjectWithDefaults(page, { type: 'Sine Wave Generator' });
|
||||
});
|
||||
|
||||
test('Plots do not re-request data when a plot is clicked', async ({ page }) => {
|
||||
//Navigate to Sine Wave Generator
|
||||
// Navigate to Sine Wave Generator
|
||||
await page.goto(sineWaveGeneratorObject.url);
|
||||
//Click on the plot canvas
|
||||
// Click on the plot canvas
|
||||
await page.locator('canvas').nth(1).click();
|
||||
//No request was made to get historical data
|
||||
// No request was made to get historical data
|
||||
const createMineFolderRequests = [];
|
||||
page.on('request', req => {
|
||||
// eslint-disable-next-line playwright/no-conditional-in-test
|
||||
createMineFolderRequests.push(req);
|
||||
});
|
||||
expect(createMineFolderRequests.length).toEqual(0);
|
||||
@@ -56,7 +55,8 @@ test.describe('Plot Integrity Testing @unstable', () => {
|
||||
await editSineWaveToUseInfinityOption(page, sineWaveGeneratorObject);
|
||||
|
||||
//Get pixel data from Canvas
|
||||
const plotPixelSize = await getCanvasPixelsWithData(page);
|
||||
const plotPixels = await getCanvasPixels(page, 'canvas');
|
||||
const plotPixelSize = plotPixels.length;
|
||||
expect(plotPixelSize).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
@@ -70,70 +70,19 @@ test.describe('Plot Integrity Testing @unstable', () => {
|
||||
*/
|
||||
async function editSineWaveToUseInfinityOption(page, sineWaveGeneratorObject) {
|
||||
await page.goto(sineWaveGeneratorObject.url);
|
||||
// Edit LAD table
|
||||
// Edit SWG properties to include infinity values
|
||||
await page.locator('[title="More options"]').click();
|
||||
await page.locator('[title="Edit properties of this object."]').click();
|
||||
// Modify the infinity option to true
|
||||
const infinityInput = page.locator('[aria-label="Include Infinity Values"]');
|
||||
await infinityInput.click();
|
||||
await page.getByRole('switch', {
|
||||
name: "Include Infinity Values"
|
||||
}).check();
|
||||
|
||||
// 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.getByRole('button', {
|
||||
name: 'Save'
|
||||
}).click();
|
||||
|
||||
// FIXME: Changes to SWG properties should be reflected on save, but they're not?
|
||||
// Thus, navigate away and back to the object.
|
||||
await page.goto('./#/browse/mine');
|
||||
await page.goto(sineWaveGeneratorObject.url);
|
||||
|
||||
await page.locator('c-progress-bar c-telemetry-table__progress-bar').waitFor({
|
||||
state: 'hidden'
|
||||
});
|
||||
|
||||
// FIXME: The progress bar disappears on series data load, not on plot render,
|
||||
// so wait for a half a second before evaluating the canvas.
|
||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function getCanvasPixelsWithData(page) {
|
||||
const getTelemValuePromise = new Promise(resolve => page.exposeFunction('getCanvasValue', resolve));
|
||||
|
||||
await page.evaluate(() => {
|
||||
// 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)
|
||||
let data;
|
||||
let canvas;
|
||||
let ctx;
|
||||
canvas = document.querySelector('canvas');
|
||||
ctx = canvas.getContext('2d');
|
||||
data = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
|
||||
const imageDataValues = Object.values(data);
|
||||
let plotPixels = [];
|
||||
// Each pixel consists of four values within the ImageData.data array. The for loop iterates by multiples of four.
|
||||
// The values associated with each pixel are R (red), G (green), B (blue), and A (alpha), in that order.
|
||||
for (let i = 0; i < imageDataValues.length;) {
|
||||
if (imageDataValues[i] > 0) {
|
||||
plotPixels.push({
|
||||
startIndex: i,
|
||||
endIndex: i + 3,
|
||||
value: `rgb(${imageDataValues[i]}, ${imageDataValues[i + 1]}, ${imageDataValues[i + 2]}, ${imageDataValues[i + 3]})`
|
||||
});
|
||||
}
|
||||
|
||||
i = i + 4;
|
||||
|
||||
}
|
||||
|
||||
window.getCanvasValue(plotPixels.length);
|
||||
});
|
||||
|
||||
return getTelemValuePromise;
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ test.describe('Scatter Plot', () => {
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Open a browser, navigate to the main page, and wait until all networkevents to resolve
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Create the Scatter Plot
|
||||
scatterPlot = await createDomainObjectWithDefaults(page, { type: 'Scatter Plot' });
|
||||
|
||||
@@ -36,7 +36,7 @@ test.describe('Stacked Plot', () => {
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Open a browser, navigate to the main page, and wait until all networkevents to resolve
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
stackedPlot = await createDomainObjectWithDefaults(page, {
|
||||
type: "Stacked Plot"
|
||||
@@ -138,7 +138,7 @@ test.describe('Stacked Plot', () => {
|
||||
|
||||
// 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('list', { name: "Y Axis Properties" }).locator("h2")).toContainText("Y Axis");
|
||||
await expect(page.getByRole('heading', { name: "Y Axis" })).toBeVisible();
|
||||
await expect(page.locator('[aria-label="Plot Series Properties"] .c-object-label')).toContainText(swgA.name);
|
||||
|
||||
// Click on the 2nd plot
|
||||
@@ -146,7 +146,7 @@ test.describe('Stacked Plot', () => {
|
||||
|
||||
// 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('list', { name: "Y Axis Properties" }).locator("h2")).toContainText("Y Axis");
|
||||
await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible();
|
||||
await expect(page.locator('[aria-label="Plot Series Properties"] .c-object-label')).toContainText(swgB.name);
|
||||
|
||||
// Click on the 3rd plot
|
||||
@@ -154,7 +154,7 @@ test.describe('Stacked Plot', () => {
|
||||
|
||||
// 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('list', { name: "Y Axis Properties" }).locator("h2")).toContainText("Y Axis");
|
||||
await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible();
|
||||
await expect(page.locator('[aria-label="Plot Series Properties"] .c-object-label')).toContainText(swgC.name);
|
||||
|
||||
// Go into edit mode
|
||||
@@ -167,7 +167,7 @@ test.describe('Stacked Plot', () => {
|
||||
|
||||
// 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('list', { name: "Y Axis Properties" }).locator("h2")).toContainText("Y Axis");
|
||||
await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible();
|
||||
await expect(page.locator('[aria-label="Plot Series Properties"] .c-object-label')).toContainText(swgA.name);
|
||||
|
||||
//Click on canvas for the 2nd plot
|
||||
@@ -175,7 +175,7 @@ test.describe('Stacked Plot', () => {
|
||||
|
||||
// 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('list', { name: "Y Axis Properties" }).locator("h2")).toContainText("Y Axis");
|
||||
await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible();
|
||||
await expect(page.locator('[aria-label="Plot Series Properties"] .c-object-label')).toContainText(swgB.name);
|
||||
|
||||
//Click on canvas for the 3rd plot
|
||||
@@ -183,7 +183,7 @@ test.describe('Stacked Plot', () => {
|
||||
|
||||
// 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('list', { name: "Y Axis Properties" }).locator("h2")).toContainText("Y Axis");
|
||||
await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible();
|
||||
await expect(page.locator('[aria-label="Plot Series Properties"] .c-object-label')).toContainText(swgC.name);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,7 +25,7 @@ Tests to verify plot tagging functionality.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('../../../../pluginFixtures');
|
||||
const { createDomainObjectWithDefaults, setRealTimeMode, setFixedTimeMode } = require('../../../../appActions');
|
||||
const { createDomainObjectWithDefaults, setRealTimeMode, setFixedTimeMode, waitForPlotsToRender } = require('../../../../appActions');
|
||||
|
||||
test.describe('Plot Tagging', () => {
|
||||
/**
|
||||
@@ -133,12 +133,9 @@ test.describe('Plot Tagging', () => {
|
||||
await expect(page.getByText('No results found')).toBeVisible();
|
||||
|
||||
//Reload Page
|
||||
await Promise.all([
|
||||
page.reload(),
|
||||
page.waitForLoadState('networkidle')
|
||||
]);
|
||||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||||
// wait for plots to load
|
||||
await expect(page.locator('.js-series-data-loaded')).toBeVisible();
|
||||
await waitForPlotsToRender(page);
|
||||
|
||||
await page.getByText('Annotations').click();
|
||||
await expect(page.getByText('No tags to display for this item')).toBeVisible();
|
||||
@@ -157,7 +154,7 @@ test.describe('Plot Tagging', () => {
|
||||
}
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
});
|
||||
|
||||
test('Tags work with Overlay Plots', async ({ page }) => {
|
||||
|
||||
@@ -30,7 +30,7 @@ test.describe('Telemetry Table', () => {
|
||||
description: 'https://github.com/nasa/openmct/issues/5113'
|
||||
});
|
||||
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
const table = await createDomainObjectWithDefaults(page, { type: 'Telemetry Table' });
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
|
||||
@@ -26,7 +26,7 @@ const { setFixedTimeMode, setRealTimeMode, setStartOffset, setEndOffset } = requ
|
||||
test.describe('Time conductor operations', () => {
|
||||
test('validate start time does not exceeds end time', async ({ page }) => {
|
||||
// Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
const year = new Date().getFullYear();
|
||||
|
||||
let startDate = 'xxxx-01-01 01:00:00.000Z';
|
||||
@@ -82,7 +82,7 @@ test.describe('Time conductor input fields real-time mode', () => {
|
||||
};
|
||||
|
||||
// Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Switch to real-time mode
|
||||
await setRealTimeMode(page);
|
||||
@@ -119,7 +119,7 @@ test.describe('Time conductor input fields real-time mode', () => {
|
||||
const endDelta = (1 * 1000);
|
||||
|
||||
// Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Switch to real-time mode
|
||||
await setRealTimeMode(page);
|
||||
|
||||
@@ -26,7 +26,7 @@ const { openObjectTreeContextMenu, createDomainObjectWithDefaults } = require('.
|
||||
test.describe('Timer', () => {
|
||||
let timer;
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
timer = await createDomainObjectWithDefaults(page, { type: 'timer' });
|
||||
});
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ test.describe('Recent Objects', () => {
|
||||
/** @type {import('@playwright/test').Locator} */
|
||||
let folderA;
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
// Set Recent Objects List locator for subsequent tests
|
||||
recentObjectsList = page.getByRole('list', {
|
||||
@@ -58,7 +58,7 @@ test.describe('Recent Objects', () => {
|
||||
});
|
||||
test('Navigated objects show up in recents, object renames and deletions are reflected', async ({ page }) => {
|
||||
// Verify that both created objects appear in the list and are in the correct order
|
||||
assertInitialRecentObjectsListState();
|
||||
await assertInitialRecentObjectsListState();
|
||||
|
||||
// Navigate to the folder by clicking on the main object name in the recent objects list item
|
||||
await page.getByRole('listitem', { name: folderA.name }).getByText(folderA.name).click();
|
||||
@@ -149,9 +149,9 @@ test.describe('Recent Objects', () => {
|
||||
await expect(clockTreeItem.locator('.c-tree__item')).not.toHaveClass(/is-targeted-item/);
|
||||
});
|
||||
test("Persists on refresh", async ({ page }) => {
|
||||
assertInitialRecentObjectsListState();
|
||||
await assertInitialRecentObjectsListState();
|
||||
await page.reload();
|
||||
assertInitialRecentObjectsListState();
|
||||
await assertInitialRecentObjectsListState();
|
||||
});
|
||||
test("Displays objects and aliases uniquely", async ({ page }) => {
|
||||
const mainTree = page.getByRole('tree', { name: 'Main Tree'});
|
||||
@@ -252,13 +252,57 @@ test.describe('Recent Objects', () => {
|
||||
// Assert that the list is empty
|
||||
expect(await recentObjectsList.locator('.c-recentobjects-listitem').count()).toBe(0);
|
||||
});
|
||||
test("Ensure clear recent objects button is active or inactive", async ({ page }) => {
|
||||
// Assert that the list initially contains 3 objects (clock, folder, my items)
|
||||
expect(await recentObjectsList.locator('.c-recentobjects-listitem').count()).toBe(3);
|
||||
|
||||
// Assert that the button is enabled
|
||||
expect(
|
||||
await page
|
||||
.getByRole("button", { name: "Clear Recently Viewed" })
|
||||
.isEnabled()
|
||||
).toBe(true);
|
||||
|
||||
// Click the aria-label="Clear Recently Viewed" button
|
||||
await page.getByRole("button", { name: "Clear Recently Viewed" }).click();
|
||||
|
||||
// Click on the "OK" button in the confirmation dialog
|
||||
await page.getByRole("button", { name: "OK" }).click();
|
||||
|
||||
// Assert that the list is empty
|
||||
expect(
|
||||
await recentObjectsList.locator(".c-recentobjects-listitem").count()
|
||||
).toBe(0);
|
||||
|
||||
// Assert that the button is disabled
|
||||
expect(
|
||||
await page
|
||||
.getByRole("button", { name: "Clear Recently Viewed" })
|
||||
.isEnabled()
|
||||
).toBe(false);
|
||||
|
||||
// Navigate to folder object
|
||||
await page.goto(folderA.url);
|
||||
|
||||
// Assert that the list contains 1 object
|
||||
expect(await recentObjectsList.locator('.c-recentobjects-listitem').count()).toBe(1);
|
||||
|
||||
// Assert that the button is enabled
|
||||
expect(
|
||||
await page
|
||||
.getByRole("button", { name: "Clear Recently Viewed" })
|
||||
.isEnabled()
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
function assertInitialRecentObjectsListState() {
|
||||
expect(recentObjectsList.getByRole('listitem', { name: clock.name })).toBeTruthy();
|
||||
expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeTruthy();
|
||||
expect(recentObjectsList.getByRole('listitem', { name: clock.name }).locator('a').getByText(folderA.name)).toBeTruthy();
|
||||
expect(recentObjectsList.getByRole('listitem').nth(0).getByText(clock.name)).toBeTruthy();
|
||||
expect(recentObjectsList.getByRole('listitem', { name: clock.name }).locator('a').getByText(folderA.name)).toBeTruthy();
|
||||
expect(recentObjectsList.getByRole('listitem').nth(1).getByText(folderA.name)).toBeTruthy();
|
||||
return Promise.all([
|
||||
expect(recentObjectsList.getByRole('listitem', { name: clock.name })).toBeVisible(),
|
||||
expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeVisible(),
|
||||
expect(recentObjectsList.getByRole('listitem', { name: clock.name }).locator('a').getByText(folderA.name)).toBeVisible(),
|
||||
expect(recentObjectsList.getByRole('listitem').nth(0).getByText(clock.name)).toBeVisible(),
|
||||
expect(recentObjectsList.getByRole('listitem', { name: clock.name }).locator('a').getByText(folderA.name)).toBeVisible(),
|
||||
expect(recentObjectsList.getByRole('listitem').nth(3).getByText(folderA.name)).toBeVisible()
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -38,7 +38,7 @@ const { test, expect } = require('../../pluginFixtures');
|
||||
test('Verify that the create button appears and that the Folder Domain Object is available for selection', async ({ page }) => {
|
||||
|
||||
//Go to baseURL
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
|
||||
//Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
@@ -28,7 +28,7 @@ const {
|
||||
|
||||
test.describe('Main Tree', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
});
|
||||
|
||||
test('Creating a child object within a folder and immediately opening it shows the created object in the tree @couchdb', async ({ page }) => {
|
||||
|
||||
74
e2e/tests/visual/ladTable.visual.spec.js
Normal file
74
e2e/tests/visual/ladTable.visual.spec.js
Normal file
@@ -0,0 +1,74 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2023, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
const { expect, test } = require('../../pluginFixtures');
|
||||
const percySnapshot = require('@percy/playwright');
|
||||
const { createDomainObjectWithDefaults } = require('../../appActions');
|
||||
|
||||
test.describe('Visual - LAD Table', () => {
|
||||
/** @type {import('@playwright/test').Locator} */
|
||||
let ladTable;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
// Create LAD Table
|
||||
ladTable = await createDomainObjectWithDefaults(page, {
|
||||
type: 'LAD Table',
|
||||
name: 'LAD Table Test'
|
||||
});
|
||||
// Create SWG inside of LAD Table
|
||||
await createDomainObjectWithDefaults(page, {
|
||||
type: 'Sine Wave Generator',
|
||||
name: 'SWG4LAD Table Test',
|
||||
parent: ladTable.uuid
|
||||
});
|
||||
|
||||
//Modify SWG to create a really stable SWG
|
||||
await page.locator('button[title="More options"]').click();
|
||||
|
||||
await page.getByRole('menuitem', { name: ' Edit Properties...' }).click();
|
||||
|
||||
//Forgive me, padre
|
||||
await page.getByRole('spinbutton', { name: 'Data Rate (hz)' }).fill('0');
|
||||
await page.getByRole('spinbutton', { name: 'Period' }).fill('0');
|
||||
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
});
|
||||
test('Toggled column widths behave accordingly', async ({ page, theme }) => {
|
||||
|
||||
await page.goto(ladTable.url);
|
||||
//Close panes for visual consistency
|
||||
await page.getByTitle('Collapse Inspect Pane').click();
|
||||
await page.getByTitle('Collapse Browse Pane').click();
|
||||
|
||||
await expect(page.locator('button[title="Expand Columns"]')).toBeVisible();
|
||||
|
||||
await percySnapshot(page, `LAD Table w/ Sine Wave Generator columns autosized (theme: ${theme})`);
|
||||
|
||||
await page.locator('button[title="Expand Columns"]').click();
|
||||
|
||||
await expect(page.locator('button[title="Autosize Columns"]')).toBeVisible();
|
||||
|
||||
await percySnapshot(page, `LAD Table w/ Sine Wave Generator columns expanded (theme: ${theme})`);
|
||||
|
||||
});
|
||||
});
|
||||
@@ -34,7 +34,7 @@ test.describe('Visual - Check Notification Info Banner of \'Save successful\'',
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
});
|
||||
|
||||
test('Create a clock, click on \'Save successful\' banner and dismiss it', async ({ page }) => {
|
||||
test('Create a clock, click on \'Save successful\' banner and dismiss it', async ({ page, theme }) => {
|
||||
// Create a clock domain object
|
||||
await createDomainObjectWithDefaults(page, { type: 'Clock' });
|
||||
// Verify there is a button with aria-label="Review 1 Notification"
|
||||
@@ -47,7 +47,7 @@ test.describe('Visual - Check Notification Info Banner of \'Save successful\'',
|
||||
expect(await page.locator('div[role="dialog"]').isVisible()).toBe(true);
|
||||
// Verify the div with role="dialog" contains text "Save successful"
|
||||
expect(await page.locator('div[role="dialog"]').innerText()).toContain('Save successful');
|
||||
await percySnapshot(page, 'Notification banner');
|
||||
await percySnapshot(page, `Notification banner - ${theme}`);
|
||||
// Verify there is a button with text "Dismiss"
|
||||
expect(await page.locator('button:has-text("Dismiss")').isVisible()).toBe(true);
|
||||
// Click on button with text "Dismiss"
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2023, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
const { test } = require('../../pluginFixtures');
|
||||
const { setBoundsToSpanAllActivities } = require('../../helper/planningUtils');
|
||||
const { createDomainObjectWithDefaults, createPlanFromJSON } = require('../../appActions');
|
||||
const percySnapshot = require('@percy/playwright');
|
||||
const examplePlanLarge = require('../../test-data/examplePlans/ExamplePlan_Large.json');
|
||||
|
||||
test.describe('Visual - Planning', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'networkidle' });
|
||||
});
|
||||
test('Plan View', async ({ page, theme }) => {
|
||||
const plan = await createPlanFromJSON(page, {
|
||||
json: examplePlanLarge
|
||||
});
|
||||
|
||||
await setBoundsToSpanAllActivities(page, examplePlanLarge, plan.url);
|
||||
await percySnapshot(page, `Plan View (theme: ${theme})`);
|
||||
});
|
||||
test('Gantt Chart View', async ({ page, theme }) => {
|
||||
const ganttChart = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Gantt Chart'
|
||||
});
|
||||
await createPlanFromJSON(page, {
|
||||
json: examplePlanLarge,
|
||||
parent: ganttChart.uuid
|
||||
});
|
||||
await setBoundsToSpanAllActivities(page, examplePlanLarge, ganttChart.url);
|
||||
await percySnapshot(page, `Gantt Chart View (theme: ${theme})`);
|
||||
});
|
||||
});
|
||||
108
e2e/tests/visual/planning.visual.spec.js
Normal file
108
e2e/tests/visual/planning.visual.spec.js
Normal file
@@ -0,0 +1,108 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2023, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
const { test } = require('../../pluginFixtures');
|
||||
const { setBoundsToSpanAllActivities } = require('../../helper/planningUtils');
|
||||
const { createDomainObjectWithDefaults, createPlanFromJSON } = require('../../appActions');
|
||||
const percySnapshot = require('@percy/playwright');
|
||||
const examplePlanSmall = require('../../test-data/examplePlans/ExamplePlan_Small2.json');
|
||||
|
||||
const snapshotScope = '.l-shell__pane-main .l-pane__contents';
|
||||
|
||||
test.describe('Visual - Planning', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('./', { waitUntil: 'domcontentloaded' });
|
||||
});
|
||||
|
||||
test('Plan View', async ({ page, theme }) => {
|
||||
const plan = await createPlanFromJSON(page, {
|
||||
name: 'Plan Visual Test',
|
||||
json: examplePlanSmall
|
||||
});
|
||||
|
||||
await setBoundsToSpanAllActivities(page, examplePlanSmall, plan.url);
|
||||
await percySnapshot(page, `Plan View (theme: ${theme})`, {
|
||||
scope: snapshotScope
|
||||
});
|
||||
});
|
||||
|
||||
test('Plan View w/ draft status', async ({ page, theme }) => {
|
||||
const plan = await createPlanFromJSON(page, {
|
||||
name: 'Plan Visual Test (Draft)',
|
||||
json: examplePlanSmall
|
||||
});
|
||||
await page.goto('./#/browse/mine');
|
||||
|
||||
await setDraftStatusForPlan(page, plan);
|
||||
|
||||
await setBoundsToSpanAllActivities(page, examplePlanSmall, plan.url);
|
||||
await percySnapshot(page, `Plan View w/ draft status (theme: ${theme})`, {
|
||||
scope: snapshotScope
|
||||
});
|
||||
});
|
||||
|
||||
test('Gantt Chart View', async ({ page, theme }) => {
|
||||
const ganttChart = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Gantt Chart',
|
||||
name: 'Gantt Chart Visual Test'
|
||||
});
|
||||
await createPlanFromJSON(page, {
|
||||
json: examplePlanSmall,
|
||||
parent: ganttChart.uuid
|
||||
});
|
||||
await setBoundsToSpanAllActivities(page, examplePlanSmall, ganttChart.url);
|
||||
await percySnapshot(page, `Gantt Chart View (theme: ${theme})`, {
|
||||
scope: snapshotScope
|
||||
});
|
||||
});
|
||||
|
||||
test('Gantt Chart View w/ draft status', async ({ page, theme }) => {
|
||||
const ganttChart = await createDomainObjectWithDefaults(page, {
|
||||
type: 'Gantt Chart',
|
||||
name: 'Gantt Chart Visual Test (Draft)'
|
||||
});
|
||||
const plan = await createPlanFromJSON(page, {
|
||||
json: examplePlanSmall,
|
||||
parent: ganttChart.uuid
|
||||
});
|
||||
|
||||
await setDraftStatusForPlan(page, plan);
|
||||
|
||||
await page.goto('./#/browse/mine');
|
||||
|
||||
await setBoundsToSpanAllActivities(page, examplePlanSmall, ganttChart.url);
|
||||
await percySnapshot(page, `Gantt Chart View w/ draft status (theme: ${theme})`, {
|
||||
scope: snapshotScope
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Uses the Open MCT API to set the status of a plan to 'draft'.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {import('../../appActions').CreatedObjectInfo} plan
|
||||
*/
|
||||
async function setDraftStatusForPlan(page, plan) {
|
||||
await page.evaluate(async (planObject) => {
|
||||
await window.openmct.status.set(planObject.uuid, 'draft');
|
||||
}, plan);
|
||||
}
|
||||
@@ -33,6 +33,8 @@ export default function (staticFaults = false) {
|
||||
return Promise.resolve(faultsData);
|
||||
},
|
||||
subscribe(domainObject, callback) {
|
||||
callback({ type: 'global-alarm-status' });
|
||||
|
||||
return () => {};
|
||||
},
|
||||
supportsRequest(domainObject) {
|
||||
|
||||
@@ -200,6 +200,8 @@
|
||||
openmct.install(openmct.plugins.Timelist());
|
||||
openmct.install(openmct.plugins.BarChart());
|
||||
openmct.install(openmct.plugins.ScatterPlot());
|
||||
openmct.start();
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
openmct.start();
|
||||
});
|
||||
</script>
|
||||
</html>
|
||||
|
||||
50
package.json
50
package.json
@@ -1,13 +1,14 @@
|
||||
{
|
||||
"name": "openmct",
|
||||
"version": "2.2.1-SNAPSHOT",
|
||||
"version": "2.2.3",
|
||||
"description": "The Open MCT core platform",
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "7.18.9",
|
||||
"@babel/eslint-parser": "7.19.1",
|
||||
"@braintree/sanitize-url": "6.0.2",
|
||||
"@percy/cli": "1.17.0",
|
||||
"@deploysentinel/playwright": "0.3.4",
|
||||
"@percy/cli": "1.24.0",
|
||||
"@percy/playwright": "1.0.4",
|
||||
"@playwright/test": "1.29.0",
|
||||
"@playwright/test": "1.32.3",
|
||||
"@types/eventemitter3": "1.2.0",
|
||||
"@types/jasmine": "4.3.1",
|
||||
"@types/lodash": "4.14.192",
|
||||
@@ -20,10 +21,10 @@
|
||||
"d3-axis": "3.0.0",
|
||||
"d3-scale": "3.3.0",
|
||||
"d3-selection": "3.0.0",
|
||||
"eslint": "8.36.0",
|
||||
"eslint-plugin-compat": "4.1.1",
|
||||
"eslint": "8.40.0",
|
||||
"eslint-plugin-compat": "4.1.4",
|
||||
"eslint-plugin-playwright": "0.12.0",
|
||||
"eslint-plugin-vue": "9.10.0",
|
||||
"eslint-plugin-vue": "9.11.0",
|
||||
"eslint-plugin-you-dont-need-lodash-underscore": "6.12.0",
|
||||
"eventemitter3": "1.2.0",
|
||||
"file-saver": "2.0.5",
|
||||
@@ -31,17 +32,17 @@
|
||||
"html2canvas": "1.4.1",
|
||||
"imports-loader": "4.0.1",
|
||||
"jasmine-core": "4.5.0",
|
||||
"karma": "6.3.20",
|
||||
"karma-chrome-launcher": "3.1.1",
|
||||
"karma": "6.4.2",
|
||||
"karma-chrome-launcher": "3.2.0",
|
||||
"karma-cli": "2.0.0",
|
||||
"karma-coverage": "2.2.0",
|
||||
"karma-coverage-istanbul-reporter": "3.0.3",
|
||||
"karma-jasmine": "5.1.0",
|
||||
"karma-junit-reporter": "2.0.1",
|
||||
"karma-sourcemap-loader": "0.3.8",
|
||||
"karma-sourcemap-loader": "0.4.0",
|
||||
"karma-spec-reporter": "0.0.36",
|
||||
"karma-webpack": "5.0.0",
|
||||
"kdbush": "^3.0.0",
|
||||
"kdbush": "3.0.0",
|
||||
"location-bar": "3.0.1",
|
||||
"lodash": "4.17.21",
|
||||
"mini-css-extract-plugin": "2.7.5",
|
||||
@@ -50,29 +51,29 @@
|
||||
"moment-timezone": "0.5.41",
|
||||
"nyc": "15.1.0",
|
||||
"painterro": "1.2.78",
|
||||
"playwright-core": "1.29.0",
|
||||
"playwright-core": "1.32.3",
|
||||
"plotly.js-basic-dist": "2.20.0",
|
||||
"plotly.js-gl2d-dist": "2.20.0",
|
||||
"printj": "1.3.1",
|
||||
"resolve-url-loader": "5.0.0",
|
||||
"sanitize-html": "2.10.0",
|
||||
"sass": "1.59.3",
|
||||
"sass-loader": "13.2.1",
|
||||
"sass": "1.62.1",
|
||||
"sass-loader": "13.2.2",
|
||||
"sinon": "15.0.1",
|
||||
"style-loader": "^3.3.1",
|
||||
"typescript": "4.9.5",
|
||||
"style-loader": "3.3.2",
|
||||
"typescript": "5.0.4",
|
||||
"uuid": "9.0.0",
|
||||
"vue": "2.6.14",
|
||||
"vue-eslint-parser": "9.1.0",
|
||||
"vue-eslint-parser": "9.2.1",
|
||||
"vue-loader": "15.9.8",
|
||||
"vue-template-compiler": "2.6.14",
|
||||
"webpack": "5.76.3",
|
||||
"webpack-cli": "5.0.0",
|
||||
"webpack-dev-server": "4.11.1",
|
||||
"webpack": "5.81.0",
|
||||
"webpack-cli": "5.0.2",
|
||||
"webpack-dev-server": "4.13.3",
|
||||
"webpack-merge": "5.8.0"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rm -rf ./dist ./node_modules ./package-lock.json",
|
||||
"clean": "rm -rf ./dist ./node_modules ./package-lock.json ./coverage ./html-test-results ./test-results ./.nyc_output ",
|
||||
"clean-test-lint": "npm run clean; npm install; npm run test; npm run lint",
|
||||
"start": "npx webpack serve --config ./.webpack/webpack.dev.js",
|
||||
"start:coverage": "npx webpack serve --config ./.webpack/webpack.coverage.js",
|
||||
@@ -86,7 +87,7 @@
|
||||
"test": "karma start",
|
||||
"test:debug": "KARMA_DEBUG=true karma start",
|
||||
"test:e2e": "npx playwright test",
|
||||
"test:e2e:couchdb": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @couchdb",
|
||||
"test:e2e:couchdb": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @couchdb --workers=1",
|
||||
"test:e2e:stable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep-invert \"@unstable|@couchdb\"",
|
||||
"test:e2e:unstable": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @unstable",
|
||||
"test:e2e:local": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome",
|
||||
@@ -107,14 +108,15 @@
|
||||
"url": "https://github.com/nasa/openmct.git"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.19.1"
|
||||
"node": ">=16.19.1"
|
||||
},
|
||||
"browserslist": [
|
||||
"Firefox ESR",
|
||||
"not IE 11",
|
||||
"last 2 Chrome versions",
|
||||
"unreleased Chrome versions",
|
||||
"ios_saf > 15"
|
||||
"ios_saf >= 16",
|
||||
"Safari >= 16"
|
||||
],
|
||||
"author": "",
|
||||
"license": "Apache-2.0"
|
||||
|
||||
@@ -31,7 +31,7 @@ class ActionsAPI extends EventEmitter {
|
||||
this._actionCollections = new WeakMap();
|
||||
this._openmct = openmct;
|
||||
|
||||
this._groupOrder = ['windowing', 'undefined', 'view', 'action', 'json'];
|
||||
this._groupOrder = ['windowing', 'undefined', 'view', 'action', 'export', 'import'];
|
||||
|
||||
this.register = this.register.bind(this);
|
||||
this.getActionsCollection = this.getActionsCollection.bind(this);
|
||||
|
||||
@@ -21,18 +21,31 @@
|
||||
*****************************************************************************/
|
||||
|
||||
export default class FaultManagementAPI {
|
||||
/**
|
||||
* @param {import("openmct").OpenMCT} openmct
|
||||
*/
|
||||
constructor(openmct) {
|
||||
this.openmct = openmct;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {*} provider
|
||||
*/
|
||||
addProvider(provider) {
|
||||
this.provider = provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean}
|
||||
*/
|
||||
supportsActions() {
|
||||
return this.provider?.acknowledgeFault !== undefined && this.provider?.shelveFault !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("../objects/ObjectAPI").DomainObject} domainObject
|
||||
* @returns {Promise.<FaultAPIResponse[]>}
|
||||
*/
|
||||
request(domainObject) {
|
||||
if (!this.provider?.supportsRequest(domainObject)) {
|
||||
return Promise.reject();
|
||||
@@ -41,6 +54,11 @@ export default class FaultManagementAPI {
|
||||
return this.provider.request(domainObject);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("../objects/ObjectAPI").DomainObject} domainObject
|
||||
* @param {Function} callback
|
||||
* @returns {Function} unsubscribe
|
||||
*/
|
||||
subscribe(domainObject, callback) {
|
||||
if (!this.provider?.supportsSubscribe(domainObject)) {
|
||||
return Promise.reject();
|
||||
@@ -49,58 +67,55 @@ export default class FaultManagementAPI {
|
||||
return this.provider.subscribe(domainObject, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Fault} fault
|
||||
* @param {*} ackData
|
||||
*/
|
||||
acknowledgeFault(fault, ackData) {
|
||||
return this.provider.acknowledgeFault(fault, ackData);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Fault} fault
|
||||
* @param {*} shelveData
|
||||
* @returns {Promise.<T>}
|
||||
*/
|
||||
shelveFault(fault, shelveData) {
|
||||
return this.provider.shelveFault(fault, shelveData);
|
||||
}
|
||||
}
|
||||
|
||||
/** @typedef {object} Fault
|
||||
* @property {string} type
|
||||
* @property {object} fault
|
||||
* @property {boolean} fault.acknowledged
|
||||
* @property {object} fault.currentValueInfo
|
||||
* @property {number} fault.currentValueInfo.value
|
||||
* @property {string} fault.currentValueInfo.rangeCondition
|
||||
* @property {string} fault.currentValueInfo.monitoringResult
|
||||
* @property {string} fault.id
|
||||
* @property {string} fault.name
|
||||
* @property {string} fault.namespace
|
||||
* @property {number} fault.seqNum
|
||||
* @property {string} fault.severity
|
||||
* @property {boolean} fault.shelved
|
||||
* @property {string} fault.shortDescription
|
||||
* @property {string} fault.triggerTime
|
||||
* @property {object} fault.triggerValueInfo
|
||||
* @property {number} fault.triggerValueInfo.value
|
||||
* @property {string} fault.triggerValueInfo.rangeCondition
|
||||
* @property {string} fault.triggerValueInfo.monitoringResult
|
||||
* @example
|
||||
* {
|
||||
* "type": "",
|
||||
* "fault": {
|
||||
* "acknowledged": true,
|
||||
* "currentValueInfo": {
|
||||
* "value": 0,
|
||||
* "rangeCondition": "",
|
||||
* "monitoringResult": ""
|
||||
* },
|
||||
* "id": "",
|
||||
* "name": "",
|
||||
* "namespace": "",
|
||||
* "seqNum": 0,
|
||||
* "severity": "",
|
||||
* "shelved": true,
|
||||
* "shortDescription": "",
|
||||
* "triggerTime": "",
|
||||
* "triggerValueInfo": {
|
||||
* "value": 0,
|
||||
* "rangeCondition": "",
|
||||
* "monitoringResult": ""
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
/**
|
||||
* @typedef {object} TriggerValueInfo
|
||||
* @property {number} value
|
||||
* @property {string} rangeCondition
|
||||
* @property {string} monitoringResult
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} CurrentValueInfo
|
||||
* @property {number} value
|
||||
* @property {string} rangeCondition
|
||||
* @property {string} monitoringResult
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} Fault
|
||||
* @property {boolean} acknowledged
|
||||
* @property {CurrentValueInfo} currentValueInfo
|
||||
* @property {string} id
|
||||
* @property {string} name
|
||||
* @property {string} namespace
|
||||
* @property {number} seqNum
|
||||
* @property {string} severity
|
||||
* @property {boolean} shelved
|
||||
* @property {string} shortDescription
|
||||
* @property {string} triggerTime
|
||||
* @property {TriggerValueInfo} triggerValueInfo
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} FaultAPIResponse
|
||||
* @property {string} type
|
||||
* @property {Fault} fault
|
||||
*/
|
||||
|
||||
@@ -88,7 +88,7 @@ export default class TelemetryAPI {
|
||||
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
|
||||
*/
|
||||
canProvideTelemetry(domainObject) {
|
||||
return Boolean(this.#findSubscriptionProvider(domainObject))
|
||||
return Boolean(this.findSubscriptionProvider(domainObject))
|
||||
|| Boolean(this.findRequestProvider(domainObject));
|
||||
}
|
||||
|
||||
@@ -123,9 +123,10 @@ export default class TelemetryAPI {
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Returns a telemetry subscription provider that supports
|
||||
* a given domain object and options.
|
||||
*/
|
||||
#findSubscriptionProvider() {
|
||||
findSubscriptionProvider() {
|
||||
const args = Array.prototype.slice.apply(arguments);
|
||||
function supportsDomainObject(provider) {
|
||||
return provider.supportsSubscribe.apply(provider, args);
|
||||
@@ -348,7 +349,7 @@ export default class TelemetryAPI {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
const provider = this.#findSubscriptionProvider(domainObject);
|
||||
const provider = this.findSubscriptionProvider(domainObject);
|
||||
|
||||
if (!this.subscribeCache) {
|
||||
this.subscribeCache = {};
|
||||
|
||||
@@ -226,7 +226,10 @@ export default {
|
||||
};
|
||||
},
|
||||
toggleFixedLayout() {
|
||||
this.configuration.isFixedLayout = !this.configuration.isFixedLayout;
|
||||
const config = structuredClone(this.configuration);
|
||||
|
||||
config.isFixedLayout = !this.configuration.isFixedLayout;
|
||||
this.ladTableConfiguration.updateConfiguration(config);
|
||||
},
|
||||
initializeViewActions() {
|
||||
if (this.configuration.isFixedLayout) {
|
||||
|
||||
@@ -21,16 +21,18 @@
|
||||
*****************************************************************************/
|
||||
|
||||
<template>
|
||||
<component
|
||||
:is="urlDefined ? 'a' : 'span'"
|
||||
<div
|
||||
ref="conditionWidgetElement"
|
||||
class="c-condition-widget u-style-receiver js-style-receiver"
|
||||
:href="url"
|
||||
:target="url ? '_BLANK' : ''"
|
||||
>
|
||||
<div class="c-condition-widget__label">
|
||||
{{ label }}
|
||||
</div>
|
||||
</component>
|
||||
<component
|
||||
:is="urlDefined ? 'a' : 'div'"
|
||||
class="c-condition-widget__label-wrapper"
|
||||
:href="url"
|
||||
>
|
||||
<div class="c-condition-widget__label">{{ label }}</div>
|
||||
</component>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -40,19 +42,26 @@ export default {
|
||||
inject: ['openmct', 'domainObject'],
|
||||
data: function () {
|
||||
return {
|
||||
conditionalLabel: '',
|
||||
conditionSetIdentifier: null,
|
||||
domainObjectLabel: '',
|
||||
url: null,
|
||||
urlDefined: false,
|
||||
useConditionSetOutputAsLabel: false
|
||||
conditionalLabel: ''
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
urlDefined() {
|
||||
return this.domainObject.url?.length > 0;
|
||||
},
|
||||
url() {
|
||||
return this.urlDefined ? sanitizeUrl(this.domainObject.url) : null;
|
||||
},
|
||||
useConditionSetOutputAsLabel() {
|
||||
return this.conditionSetIdentifier && this.domainObject.configuration.useConditionSetOutputAsLabel;
|
||||
},
|
||||
conditionSetIdentifier() {
|
||||
return this.domainObject.configuration?.objectStyles?.conditionSetIdentifier;
|
||||
},
|
||||
label() {
|
||||
return this.useConditionSetOutputAsLabel
|
||||
? this.conditionalLabel
|
||||
: this.domainObjectLabel
|
||||
: this.domainObject.label
|
||||
;
|
||||
}
|
||||
},
|
||||
@@ -69,20 +78,11 @@ export default {
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.unlisten = this.openmct.objects.observe(this.domainObject, '*', this.updateDomainObject);
|
||||
|
||||
if (this.domainObject) {
|
||||
this.updateDomainObject(this.domainObject);
|
||||
this.listenToConditionSetChanges();
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.conditionSetIdentifier = null;
|
||||
|
||||
if (this.unlisten) {
|
||||
this.unlisten();
|
||||
}
|
||||
|
||||
this.stopListeningToConditionSetChanges();
|
||||
},
|
||||
methods: {
|
||||
@@ -121,31 +121,6 @@ export default {
|
||||
}
|
||||
|
||||
this.conditionalLabel = latestDatum.output || '';
|
||||
},
|
||||
updateDomainObject(domainObject) {
|
||||
if (this.domainObjectLabel !== domainObject.label) {
|
||||
this.domainObjectLabel = domainObject.label;
|
||||
}
|
||||
|
||||
const urlDefined = domainObject.url && domainObject.url.length > 0;
|
||||
if (this.urlDefined !== urlDefined) {
|
||||
this.urlDefined = urlDefined;
|
||||
}
|
||||
|
||||
const url = this.urlDefined ? sanitizeUrl(domainObject.url) : null;
|
||||
if (this.url !== url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
const conditionSetIdentifier = domainObject.configuration?.objectStyles?.conditionSetIdentifier;
|
||||
if (conditionSetIdentifier && this.conditionSetIdentifier !== conditionSetIdentifier) {
|
||||
this.conditionSetIdentifier = conditionSetIdentifier;
|
||||
}
|
||||
|
||||
const useConditionSetOutputAsLabel = this.conditionSetIdentifier && domainObject.configuration.useConditionSetOutputAsLabel;
|
||||
if (this.useConditionSetOutputAsLabel !== useConditionSetOutputAsLabel) {
|
||||
this.useConditionSetOutputAsLabel = useConditionSetOutputAsLabel;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -26,31 +26,35 @@
|
||||
background-color: rgba($colorBodyFg, 0.1); // Give a little presence if the user hasn't defined a fill color
|
||||
border-radius: $basicCr;
|
||||
border: 1px solid transparent;
|
||||
display: inline-block;
|
||||
padding: $interiorMarginLg $interiorMarginLg * 2;
|
||||
display: block;
|
||||
max-width: max-content;
|
||||
|
||||
a {
|
||||
display: block;
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.c-condition-widget__label {
|
||||
padding: $interiorMargin;
|
||||
// Either a <div> or an <a> tag
|
||||
padding: $interiorMargin $interiorMargin * 1.5;
|
||||
text-align: center;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
a.c-condition-widget {
|
||||
// Widget is conditionally made into a <a> when URL property has been defined
|
||||
cursor: pointer !important;
|
||||
pointer-events: inherit;
|
||||
}
|
||||
|
||||
// Make Condition Widget expand when in a hidden frame Layout context
|
||||
// For both static and Flexible Layouts
|
||||
.c-so-view--conditionWidget.c-so-view--no-frame {
|
||||
.c-condition-widget {
|
||||
@include abs();
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
max-width: unset;
|
||||
|
||||
&__label-wrapper {
|
||||
@include abs();
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.c-so-view__frame-controls { display: none; }
|
||||
|
||||
@@ -36,6 +36,7 @@ export default function plugin() {
|
||||
domainObject.configuration = {};
|
||||
domainObject.label = 'Condition Widget';
|
||||
domainObject.conditionalLabel = '';
|
||||
domainObject.url = '';
|
||||
},
|
||||
form: [
|
||||
{
|
||||
|
||||
@@ -20,8 +20,6 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import JSONExporter from '/src/exporters/JSONExporter.js';
|
||||
|
||||
import _ from 'lodash';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
export default class ExportAsJSONAction {
|
||||
@@ -32,13 +30,12 @@ export default class ExportAsJSONAction {
|
||||
this.key = 'export.JSON';
|
||||
this.description = '';
|
||||
this.cssClass = "icon-export";
|
||||
this.group = "json";
|
||||
this.group = "export";
|
||||
this.priority = 1;
|
||||
|
||||
this.externalIdentifiers = [];
|
||||
this.tree = {};
|
||||
this.calls = 0;
|
||||
this.idMap = {};
|
||||
this.tree = null;
|
||||
this.calls = null;
|
||||
this.idMap = null;
|
||||
|
||||
this.JSONExportService = new JSONExporter();
|
||||
}
|
||||
@@ -60,21 +57,164 @@ export default class ExportAsJSONAction {
|
||||
*/
|
||||
invoke(objectpath) {
|
||||
this.tree = {};
|
||||
this.calls = 0;
|
||||
this.idMap = {};
|
||||
|
||||
const root = objectpath[0];
|
||||
this.root = JSON.parse(JSON.stringify(root));
|
||||
const rootId = this._getId(this.root);
|
||||
this.root = this._copy(root);
|
||||
|
||||
const rootId = this._getKeystring(this.root);
|
||||
this.tree[rootId] = this.root;
|
||||
|
||||
this._write(this.root);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {object} parent
|
||||
*/
|
||||
async _write(parent) {
|
||||
this.calls++;
|
||||
|
||||
//conditional object styles are not saved on the composition, so we need to check for them
|
||||
const conditionSetIdentifier = this._getConditionSetIdentifier(parent);
|
||||
const hasItemConditionSetIdentifiers = this._hasItemConditionSetIdentifiers(parent);
|
||||
const composition = this.openmct.composition.get(parent);
|
||||
|
||||
if (composition) {
|
||||
const children = await composition.load();
|
||||
|
||||
children.forEach((child) => {
|
||||
this._exportObject(child, parent);
|
||||
});
|
||||
}
|
||||
|
||||
if (!conditionSetIdentifier && !hasItemConditionSetIdentifiers) {
|
||||
this._decrementCallsAndSave();
|
||||
} else {
|
||||
const conditionSetObjects = [];
|
||||
|
||||
// conditionSetIdentifiers directly in objectStyles object
|
||||
if (conditionSetIdentifier) {
|
||||
conditionSetObjects.push(await this.openmct.objects.get(conditionSetIdentifier));
|
||||
}
|
||||
|
||||
// conditionSetIdentifiers stored on item ids in the objectStyles object
|
||||
if (hasItemConditionSetIdentifiers) {
|
||||
const itemConditionSetIdentifiers = this._getItemConditionSetIdentifiers(parent);
|
||||
|
||||
for (const itemConditionSetIdentifier of itemConditionSetIdentifiers) {
|
||||
conditionSetObjects.push(await this.openmct.objects.get(itemConditionSetIdentifier));
|
||||
}
|
||||
}
|
||||
|
||||
for (const conditionSetObject of conditionSetObjects) {
|
||||
this._exportObject(conditionSetObject, parent);
|
||||
}
|
||||
|
||||
this._decrementCallsAndSave();
|
||||
}
|
||||
}
|
||||
|
||||
_exportObject(child, parent) {
|
||||
const originalKeyString = this._getKeystring(child);
|
||||
const createable = this._isCreatableAndPersistable(child);
|
||||
const isNotInfinite = !Object.prototype.hasOwnProperty.call(this.tree, originalKeyString);
|
||||
|
||||
if (createable && isNotInfinite) {
|
||||
// for external or linked objects we generate new keys, if they don't exist already
|
||||
if (this._isLinkedObject(child, parent)) {
|
||||
child = this._rewriteLink(child, parent);
|
||||
} else {
|
||||
this.tree[originalKeyString] = child;
|
||||
}
|
||||
|
||||
this._write(child);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {object} child
|
||||
* @param {object} parent
|
||||
* @returns {object}
|
||||
*/
|
||||
_rewriteLink(child, parent) {
|
||||
const originalKeyString = this._getKeystring(child);
|
||||
const parentKeyString = this._getKeystring(parent);
|
||||
const conditionSetIdentifier = this._getConditionSetIdentifier(parent);
|
||||
const hasItemConditionSetIdentifiers = this._hasItemConditionSetIdentifiers(parent);
|
||||
const existingMappedKeyString = this.idMap[originalKeyString];
|
||||
let copy;
|
||||
|
||||
if (!existingMappedKeyString) {
|
||||
copy = this._copy(child);
|
||||
copy.identifier.key = uuid();
|
||||
|
||||
if (!conditionSetIdentifier && !hasItemConditionSetIdentifiers) {
|
||||
copy.location = parentKeyString;
|
||||
}
|
||||
|
||||
let newKeyString = this._getKeystring(copy);
|
||||
this.idMap[originalKeyString] = newKeyString;
|
||||
this.tree[newKeyString] = copy;
|
||||
} else {
|
||||
copy = this.tree[existingMappedKeyString];
|
||||
}
|
||||
|
||||
if (conditionSetIdentifier || hasItemConditionSetIdentifiers) {
|
||||
|
||||
// update objectStyle object
|
||||
if (conditionSetIdentifier) {
|
||||
const directObjectStylesIdentifier = this.openmct.objects.areIdsEqual(
|
||||
parent.configuration.objectStyles.conditionSetIdentifier,
|
||||
child.identifier
|
||||
);
|
||||
|
||||
if (directObjectStylesIdentifier) {
|
||||
parent.configuration.objectStyles.conditionSetIdentifier = copy.identifier;
|
||||
this.tree[parentKeyString].configuration.objectStyles.conditionSetIdentifier = copy.identifier;
|
||||
}
|
||||
}
|
||||
|
||||
// update per item id on objectStyle object
|
||||
if (hasItemConditionSetIdentifiers) {
|
||||
for (const itemId in parent.configuration.objectStyles) {
|
||||
if (parent.configuration.objectStyles[itemId]) {
|
||||
const itemConditionSetIdentifier = parent.configuration.objectStyles[itemId].conditionSetIdentifier;
|
||||
|
||||
if (
|
||||
itemConditionSetIdentifier
|
||||
&& this.openmct.objects.areIdsEqual(itemConditionSetIdentifier, child.identifier)
|
||||
) {
|
||||
parent.configuration.objectStyles[itemId].conditionSetIdentifier = copy.identifier;
|
||||
this.tree[parentKeyString].configuration.objectStyles[itemId].conditionSetIdentifier = copy.identifier;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// just update parent
|
||||
const index = parent.composition.findIndex(identifier => {
|
||||
return this.openmct.objects.areIdsEqual(child.identifier, identifier);
|
||||
});
|
||||
|
||||
parent.composition[index] = copy.identifier;
|
||||
this.tree[parentKeyString].composition[index] = copy.identifier;
|
||||
}
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {object} domainObject
|
||||
* @returns {string} A string representation of the given identifier, including namespace and key
|
||||
*/
|
||||
_getId(domainObject) {
|
||||
_getKeystring(domainObject) {
|
||||
return this.openmct.objects.makeKeyString(domainObject.identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {object} domainObject
|
||||
@@ -86,6 +226,7 @@ export default class ExportAsJSONAction {
|
||||
|
||||
return type && type.definition.creatable && isPersistable;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {object} child
|
||||
@@ -93,74 +234,80 @@ export default class ExportAsJSONAction {
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_isLinkedObject(child, parent) {
|
||||
if (child.location !== this._getId(parent)
|
||||
&& !Object.keys(this.tree).includes(child.location)
|
||||
&& this._getId(child) !== this._getId(this.root)
|
||||
|| this.externalIdentifiers.includes(this._getId(child))) {
|
||||
const rootKeyString = this._getKeystring(this.root);
|
||||
const childKeyString = this._getKeystring(child);
|
||||
const parentKeyString = this._getKeystring(parent);
|
||||
|
||||
return true;
|
||||
return (child.location !== parentKeyString
|
||||
&& !Object.keys(this.tree).includes(child.location)
|
||||
&& childKeyString !== rootKeyString)
|
||||
|| this.idMap[childKeyString] !== undefined;
|
||||
}
|
||||
|
||||
_getConditionSetIdentifier(object) {
|
||||
return object.configuration?.objectStyles?.conditionSetIdentifier;
|
||||
}
|
||||
|
||||
_hasItemConditionSetIdentifiers(parent) {
|
||||
const objectStyles = parent.configuration?.objectStyles;
|
||||
|
||||
for (const itemId in objectStyles) {
|
||||
if (Object.prototype.hasOwnProperty.call(objectStyles[itemId], 'conditionSetIdentifier')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
* @param {object} child
|
||||
* @param {object} parent
|
||||
* @returns {object}
|
||||
*/
|
||||
_rewriteLink(child, parent) {
|
||||
this.externalIdentifiers.push(this._getId(child));
|
||||
const index = parent.composition.findIndex(id => {
|
||||
return _.isEqual(child.identifier, id);
|
||||
});
|
||||
const copyOfChild = JSON.parse(JSON.stringify(child));
|
||||
|
||||
copyOfChild.identifier.key = uuid();
|
||||
const newIdString = this._getId(copyOfChild);
|
||||
const parentId = this._getId(parent);
|
||||
_getItemConditionSetIdentifiers(parent) {
|
||||
const objectStyles = parent.configuration?.objectStyles;
|
||||
let identifiers = new Set();
|
||||
|
||||
this.idMap[this._getId(child)] = newIdString;
|
||||
copyOfChild.location = parentId;
|
||||
parent.composition[index] = copyOfChild.identifier;
|
||||
this.tree[newIdString] = copyOfChild;
|
||||
this.tree[parentId].composition[index] = copyOfChild.identifier;
|
||||
if (objectStyles) {
|
||||
Object.keys(objectStyles).forEach(itemId => {
|
||||
if (objectStyles[itemId].conditionSetIdentifier) {
|
||||
identifiers.add(objectStyles[itemId].conditionSetIdentifier);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return copyOfChild;
|
||||
return Array.from(identifiers);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {object} child
|
||||
* @param {object} parent
|
||||
* @returns {object}
|
||||
*/
|
||||
_rewriteLinkForReference(child, parent) {
|
||||
const childId = this._getId(child);
|
||||
this.externalIdentifiers.push(childId);
|
||||
const copyOfChild = JSON.parse(JSON.stringify(child));
|
||||
|
||||
copyOfChild.identifier.key = uuid();
|
||||
const newIdString = this._getId(copyOfChild);
|
||||
const parentId = this._getId(parent);
|
||||
|
||||
this.idMap[childId] = newIdString;
|
||||
copyOfChild.location = null;
|
||||
parent.configuration.objectStyles.conditionSetIdentifier = copyOfChild.identifier;
|
||||
this.tree[newIdString] = copyOfChild;
|
||||
this.tree[parentId].configuration.objectStyles.conditionSetIdentifier = copyOfChild.identifier;
|
||||
|
||||
return copyOfChild;
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_rewriteReferences() {
|
||||
const oldKeyStrings = Object.keys(this.idMap);
|
||||
let treeString = JSON.stringify(this.tree);
|
||||
Object.keys(this.idMap).forEach(function (oldId) {
|
||||
const newId = this.idMap[oldId];
|
||||
treeString = treeString.split(oldId).join(newId);
|
||||
}.bind(this));
|
||||
|
||||
oldKeyStrings.forEach((oldKeyString) => {
|
||||
// this will cover keyStrings, identifiers and identifiers created
|
||||
// by hand that may be structured differently from those created with 'makeKeyString'
|
||||
const newKeyString = this.idMap[oldKeyString];
|
||||
const newIdentifier = JSON.stringify(this.openmct.objects.parseKeyString(newKeyString));
|
||||
const oldIdentifier = this.openmct.objects.parseKeyString(oldKeyString);
|
||||
const oldIdentifierNamespaceFirst = JSON.stringify(oldIdentifier);
|
||||
const oldIdentifierKeyFirst = JSON.stringify({
|
||||
key: oldIdentifier.key,
|
||||
namespace: oldIdentifier.namespace
|
||||
});
|
||||
|
||||
// replace keyStrings
|
||||
treeString = treeString.split(oldKeyString).join(newKeyString);
|
||||
|
||||
// check for namespace first identifiers, replace if necessary
|
||||
if (treeString.includes(oldIdentifierNamespaceFirst)) {
|
||||
treeString = treeString.split(oldIdentifierNamespaceFirst).join(newIdentifier);
|
||||
}
|
||||
|
||||
// check for key first identifiers, replace if necessary
|
||||
if (treeString.includes(oldIdentifierKeyFirst)) {
|
||||
treeString = treeString.split(oldIdentifierKeyFirst).join(newIdentifier);
|
||||
}
|
||||
|
||||
});
|
||||
this.tree = JSON.parse(treeString);
|
||||
}
|
||||
/**
|
||||
@@ -180,70 +327,10 @@ export default class ExportAsJSONAction {
|
||||
_wrapTree() {
|
||||
return {
|
||||
"openmct": this.tree,
|
||||
"rootId": this._getId(this.root)
|
||||
"rootId": this._getKeystring(this.root)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {object} parent
|
||||
*/
|
||||
_write(parent) {
|
||||
this.calls++;
|
||||
//conditional object styles are not saved on the composition, so we need to check for them
|
||||
let childObjectReferenceId = parent.configuration?.objectStyles?.conditionSetIdentifier;
|
||||
|
||||
const composition = this.openmct.composition.get(parent);
|
||||
if (composition !== undefined) {
|
||||
composition.load()
|
||||
.then((children) => {
|
||||
children.forEach((child, index) => {
|
||||
// Only export if object is creatable
|
||||
if (this._isCreatableAndPersistable(child)) {
|
||||
// Prevents infinite export of self-contained objs
|
||||
if (!Object.prototype.hasOwnProperty.call(this.tree, this._getId(child))) {
|
||||
// If object is a link to something absent from
|
||||
// tree, generate new id and treat as new object
|
||||
if (this._isLinkedObject(child, parent)) {
|
||||
child = this._rewriteLink(child, parent);
|
||||
} else {
|
||||
this.tree[this._getId(child)] = child;
|
||||
}
|
||||
|
||||
this._write(child);
|
||||
}
|
||||
}
|
||||
});
|
||||
this._decrementCallsAndSave();
|
||||
});
|
||||
} else if (!childObjectReferenceId) {
|
||||
this._decrementCallsAndSave();
|
||||
}
|
||||
|
||||
if (childObjectReferenceId) {
|
||||
this.openmct.objects.get(childObjectReferenceId)
|
||||
.then((child) => {
|
||||
// Only export if object is creatable
|
||||
if (this._isCreatableAndPersistable(child)) {
|
||||
// Prevents infinite export of self-contained objs
|
||||
if (!Object.prototype.hasOwnProperty.call(this.tree, this._getId(child))) {
|
||||
// If object is a link to something absent from
|
||||
// tree, generate new id and treat as new object
|
||||
if (this._isLinkedObject(child, parent)) {
|
||||
child = this._rewriteLinkForReference(child, parent);
|
||||
} else {
|
||||
this.tree[this._getId(child)] = child;
|
||||
}
|
||||
|
||||
this._write(child);
|
||||
}
|
||||
}
|
||||
|
||||
this._decrementCallsAndSave();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_decrementCallsAndSave() {
|
||||
this.calls--;
|
||||
if (this.calls === 0) {
|
||||
@@ -251,4 +338,8 @@ export default class ExportAsJSONAction {
|
||||
this._saveAs(this._wrapTree());
|
||||
}
|
||||
}
|
||||
|
||||
_copy(object) {
|
||||
return JSON.parse(JSON.stringify(object));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,8 +42,6 @@ export default {
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.updateFaultList();
|
||||
|
||||
this.unsubscribe = this.openmct.faults
|
||||
.subscribe(this.domainObject, this.updateFault);
|
||||
},
|
||||
@@ -68,7 +66,11 @@ export default {
|
||||
this.openmct.faults
|
||||
.request(this.domainObject)
|
||||
.then(faultsData => {
|
||||
this.faultsList = faultsData.map(fd => fd.fault);
|
||||
if (faultsData?.length > 0) {
|
||||
this.faultsList = faultsData.map(fd => fd.fault);
|
||||
} else {
|
||||
this.faultsList = [];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,8 +29,9 @@ export default class ImportAsJSONAction {
|
||||
this.key = 'import.JSON';
|
||||
this.description = '';
|
||||
this.cssClass = "icon-import";
|
||||
this.group = "json";
|
||||
this.group = "import";
|
||||
this.priority = 2;
|
||||
this.newObjects = [];
|
||||
|
||||
this.openmct = openmct;
|
||||
}
|
||||
@@ -85,22 +86,25 @@ export default class ImportAsJSONAction {
|
||||
let objectIdentifiers = this._getObjectReferenceIds(parent);
|
||||
|
||||
if (objectIdentifiers.length) {
|
||||
let newObj;
|
||||
const parentId = this.openmct.objects.makeKeyString(parent.identifier);
|
||||
seen.push(parentId);
|
||||
|
||||
seen.push(parent.id);
|
||||
|
||||
objectIdentifiers.forEach(async (childId) => {
|
||||
for (const childId of objectIdentifiers) {
|
||||
const keystring = this.openmct.objects.makeKeyString(childId);
|
||||
if (!tree[keystring] || seen.includes(keystring)) {
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
const newModel = tree[keystring];
|
||||
delete newModel.persisted;
|
||||
|
||||
newObj = await this._instantiate(newModel);
|
||||
this._deepInstantiate(newObj, tree, seen);
|
||||
}, this);
|
||||
this.newObjects.push(newModel);
|
||||
|
||||
// make sure there weren't any errors saving
|
||||
if (newModel) {
|
||||
this._deepInstantiate(newModel, tree, seen);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
@@ -110,19 +114,32 @@ export default class ImportAsJSONAction {
|
||||
*/
|
||||
_getObjectReferenceIds(parent) {
|
||||
let objectIdentifiers = [];
|
||||
let itemObjectReferences = [];
|
||||
const objectStyles = parent?.configuration?.objectStyles;
|
||||
const parentComposition = this.openmct.composition.get(parent);
|
||||
|
||||
let parentComposition = this.openmct.composition.get(parent);
|
||||
if (parentComposition) {
|
||||
objectIdentifiers = Array.from(parentComposition.domainObject.composition);
|
||||
objectIdentifiers = Array.from(parent.composition);
|
||||
}
|
||||
|
||||
//conditional object styles are not saved on the composition, so we need to check for them
|
||||
let parentObjectReference = parent.configuration?.objectStyles?.conditionSetIdentifier;
|
||||
if (parentObjectReference) {
|
||||
objectIdentifiers.push(parentObjectReference);
|
||||
if (objectStyles) {
|
||||
const parentObjectReference = objectStyles.conditionSetIdentifier;
|
||||
|
||||
if (parentObjectReference) {
|
||||
objectIdentifiers.push(parentObjectReference);
|
||||
}
|
||||
|
||||
function hasConditionSetIdentifier(item) {
|
||||
return Boolean(item.conditionSetIdentifier);
|
||||
}
|
||||
|
||||
itemObjectReferences = Object.values(objectStyles)
|
||||
.filter(hasConditionSetIdentifier)
|
||||
.map(item => item.conditionSetIdentifier);
|
||||
}
|
||||
|
||||
return objectIdentifiers;
|
||||
return Array.from(new Set([...objectIdentifiers, ...itemObjectReferences]));
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
@@ -155,13 +172,21 @@ export default class ImportAsJSONAction {
|
||||
const tree = this._generateNewIdentifiers(objTree, namespace);
|
||||
const rootId = tree.rootId;
|
||||
|
||||
const rootModel = tree.openmct[rootId];
|
||||
delete rootModel.persisted;
|
||||
const rootObj = tree.openmct[rootId];
|
||||
delete rootObj.persisted;
|
||||
this.newObjects.push(rootObj);
|
||||
|
||||
const rootObj = await this._instantiate(rootModel);
|
||||
if (this.openmct.composition.checkPolicy(domainObject, rootObj)) {
|
||||
this._deepInstantiate(rootObj, tree.openmct, []);
|
||||
|
||||
try {
|
||||
await Promise.all(this.newObjects.map(this._instantiate, this));
|
||||
} catch (error) {
|
||||
this.openmct.notifications.error('Error saving objects');
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
const compositionCollection = this.openmct.composition.get(domainObject);
|
||||
let domainObjectKeyString = this.openmct.objects.makeKeyString(domainObject.identifier);
|
||||
this.openmct.objects.mutate(rootObj, 'location', domainObjectKeyString);
|
||||
@@ -184,16 +209,11 @@ export default class ImportAsJSONAction {
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
* @param {object} rootModel
|
||||
* @param {object} model
|
||||
* @returns {object}
|
||||
*/
|
||||
async _instantiate(rootModel) {
|
||||
const success = await this.openmct.objects.save(rootModel);
|
||||
if (success) {
|
||||
return rootModel;
|
||||
}
|
||||
|
||||
this.openmct.notifications.error('Error saving objects');
|
||||
_instantiate(model) {
|
||||
return this.openmct.objects.save(model);
|
||||
}
|
||||
/**
|
||||
* @private
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
class="c-icon-button c-button--menu icon-font"
|
||||
@click.prevent.stop="showFontMenu"
|
||||
>
|
||||
<span class="c-button__label">{{ fontTypeLable }}</span>
|
||||
<span class="c-button__label">{{ fontTypeLabel }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -43,7 +43,7 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
fontTypeLable() {
|
||||
fontTypeLabel() {
|
||||
const fontType = FONTS.find(f => f.value === this.fontStyle.font);
|
||||
if (!fontType) {
|
||||
return '??';
|
||||
|
||||
167
src/plugins/notebook/actions/ExportNotebookAsTextAction.js
Normal file
167
src/plugins/notebook/actions/ExportNotebookAsTextAction.js
Normal file
@@ -0,0 +1,167 @@
|
||||
import {saveAs} from 'saveAs';
|
||||
import Moment from 'moment';
|
||||
import {NOTEBOOK_TYPE, RESTRICTED_NOTEBOOK_TYPE} from '../notebook-constants';
|
||||
const UNKNOWN_USER = 'Unknown';
|
||||
const UNKNOWN_TIME = 'Unknown';
|
||||
const ALLOWED_TYPES = [NOTEBOOK_TYPE, RESTRICTED_NOTEBOOK_TYPE];
|
||||
|
||||
export default class ExportNotebookAsTextAction {
|
||||
|
||||
constructor(openmct) {
|
||||
this.openmct = openmct;
|
||||
|
||||
this.cssClass = 'icon-export';
|
||||
this.description = 'Exports notebook contents as a text file';
|
||||
this.group = "export";
|
||||
this.key = 'exportNotebookAsText';
|
||||
this.name = 'Export Notebook as Text';
|
||||
}
|
||||
|
||||
invoke(objectPath) {
|
||||
this.showForm(objectPath);
|
||||
}
|
||||
|
||||
getTagName(tagId, availableTags) {
|
||||
const foundTag = availableTags.find(tag => tag.id === tagId);
|
||||
if (foundTag) {
|
||||
return foundTag.label;
|
||||
} else {
|
||||
return tagId;
|
||||
}
|
||||
}
|
||||
|
||||
getTagsForEntry(entry, domainObjectKeyString, annotations) {
|
||||
const foundTags = [];
|
||||
annotations.forEach(annotation => {
|
||||
const target = annotation.targets?.[domainObjectKeyString];
|
||||
if (target?.entryId === entry.id) {
|
||||
annotation.tags.forEach(tag => {
|
||||
if (!foundTags.includes(tag)) {
|
||||
foundTags.push(tag);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return foundTags;
|
||||
}
|
||||
|
||||
formatTimeStamp(timestamp) {
|
||||
if (timestamp) {
|
||||
return `${Moment.utc(timestamp).format('YYYY-MM-DD HH:mm:ss')} UTC`;
|
||||
} else {
|
||||
return UNKNOWN_TIME;
|
||||
}
|
||||
}
|
||||
|
||||
appliesTo(objectPath) {
|
||||
const domainObject = objectPath[0];
|
||||
|
||||
return ALLOWED_TYPES.includes(domainObject.type);
|
||||
}
|
||||
|
||||
async onSave(changes, objectPath) {
|
||||
const availableTags = this.openmct.annotation.getAvailableTags();
|
||||
const identifier = objectPath[0].identifier;
|
||||
const domainObject = await this.openmct.objects.get(identifier);
|
||||
let foundAnnotations = [];
|
||||
// only load annotations if there are tags
|
||||
if (availableTags.length) {
|
||||
foundAnnotations = await this.openmct.annotation.getAnnotations(domainObject.identifier);
|
||||
}
|
||||
|
||||
let notebookAsText = `# ${domainObject.name}\n\n`;
|
||||
|
||||
if (changes.exportMetaData) {
|
||||
const createdTimestamp = domainObject.created;
|
||||
const createdBy = this.getUserName(domainObject.createdBy);
|
||||
const modifiedBy = this.getUserName(domainObject.modifiedBy);
|
||||
const modifiedTimestamp = domainObject.modified ?? domainObject.created;
|
||||
notebookAsText += `Created on ${this.formatTimeStamp(createdTimestamp)} by user ${createdBy}\n\n`;
|
||||
notebookAsText += `Updated on ${this.formatTimeStamp(modifiedTimestamp)} by user ${modifiedBy}\n\n`;
|
||||
}
|
||||
|
||||
const notebookSections = domainObject.configuration.sections;
|
||||
const notebookEntries = domainObject.configuration.entries;
|
||||
|
||||
notebookSections.forEach(section => {
|
||||
notebookAsText += `## ${section.name}\n\n`;
|
||||
|
||||
const notebookPages = section.pages;
|
||||
|
||||
notebookPages.forEach(page => {
|
||||
notebookAsText += `### ${page.name}\n\n`;
|
||||
|
||||
const notebookPageEntries = notebookEntries[section.id]?.[page.id];
|
||||
if (!notebookPageEntries) {
|
||||
// blank page
|
||||
return;
|
||||
}
|
||||
|
||||
notebookPageEntries.forEach(entry => {
|
||||
if (changes.exportMetaData) {
|
||||
const createdTimestamp = entry.createdOn;
|
||||
const createdBy = this.getUserName(entry.createdBy);
|
||||
const modifiedBy = this.getUserName(entry.modifiedBy);
|
||||
const modifiedTimestamp = entry.modified ?? entry.created;
|
||||
notebookAsText += `Created on ${this.formatTimeStamp(createdTimestamp)} by user ${createdBy}\n\n`;
|
||||
notebookAsText += `Updated on ${this.formatTimeStamp(modifiedTimestamp)} by user ${modifiedBy}\n\n`;
|
||||
}
|
||||
|
||||
if (changes.exportTags) {
|
||||
const domainObjectKeyString = this.openmct.objects.makeKeyString(domainObject.identifier);
|
||||
const tags = this.getTagsForEntry(entry, domainObjectKeyString, foundAnnotations);
|
||||
const tagNames = tags.map(tag => this.getTagName(tag, availableTags));
|
||||
if (tagNames) {
|
||||
notebookAsText += `Tags: ${tagNames.join(', ')}\n\n`;
|
||||
}
|
||||
}
|
||||
|
||||
notebookAsText += `${entry.text}\n\n`;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const blob = new Blob([notebookAsText], {type: "text/markdown"});
|
||||
const fileName = domainObject.name + '.md';
|
||||
saveAs(blob, fileName);
|
||||
}
|
||||
|
||||
getUserName(userId) {
|
||||
if (userId && userId.length) {
|
||||
return userId;
|
||||
}
|
||||
|
||||
return UNKNOWN_USER;
|
||||
}
|
||||
|
||||
async showForm(objectPath) {
|
||||
const formStructure = {
|
||||
title: "Export Notebook Text",
|
||||
sections: [
|
||||
{
|
||||
rows: [
|
||||
{
|
||||
key: "exportMetaData",
|
||||
control: "toggleSwitch",
|
||||
name: "Include Metadata (created/modified, etc.)",
|
||||
required: true,
|
||||
value: false
|
||||
},
|
||||
{
|
||||
name: "Include Tags",
|
||||
control: "toggleSwitch",
|
||||
required: true,
|
||||
key: 'exportTags',
|
||||
value: false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const changes = await this.openmct.forms.showForm(formStructure);
|
||||
|
||||
return this.onSave(changes, objectPath);
|
||||
}
|
||||
}
|
||||
@@ -125,7 +125,7 @@
|
||||
v-if="selectedPage && !selectedPage.isLocked"
|
||||
:class="{ 'disabled': activeTransaction }"
|
||||
class="c-notebook__drag-area icon-plus"
|
||||
@click="newEntry()"
|
||||
@click="newEntry(null, $event)"
|
||||
@dragover="dragOver"
|
||||
@drop.capture="dropCapture"
|
||||
@drop="dropOnEntry($event)"
|
||||
@@ -193,7 +193,7 @@ import SearchResults from './SearchResults.vue';
|
||||
import Sidebar from './Sidebar.vue';
|
||||
import ProgressBar from '../../../ui/components/ProgressBar.vue';
|
||||
import { clearDefaultNotebook, getDefaultNotebook, setDefaultNotebook, setDefaultNotebookSectionId, setDefaultNotebookPageId } from '../utils/notebook-storage';
|
||||
import { addNotebookEntry, createNewEmbed, getEntryPosById, getNotebookEntries, mutateObject } from '../utils/notebook-entries';
|
||||
import { addNotebookEntry, createNewEmbed, getEntryPosById, getNotebookEntries, mutateObject, selectEntry } from '../utils/notebook-entries';
|
||||
import { saveNotebookImageDomainObject, updateNamespaceOfDomainObject } from '../utils/notebook-image';
|
||||
import { isNotebookViewType, RESTRICTED_NOTEBOOK_TYPE } from '../notebook-constants';
|
||||
|
||||
@@ -793,15 +793,29 @@ export default {
|
||||
|
||||
return section.id;
|
||||
},
|
||||
async newEntry(embed = null) {
|
||||
async newEntry(embed, event) {
|
||||
this.startTransaction();
|
||||
this.resetSearch();
|
||||
const notebookStorage = this.createNotebookStorageObject();
|
||||
this.updateDefaultNotebook(notebookStorage);
|
||||
const id = await addNotebookEntry(this.openmct, this.domainObject, notebookStorage, embed);
|
||||
|
||||
const element = this.$refs.notebookEntries.querySelector(`#${id}`);
|
||||
const entryAnnotations = this.notebookAnnotations[id] ?? {};
|
||||
selectEntry({
|
||||
element,
|
||||
entryId: id,
|
||||
domainObject: this.domainObject,
|
||||
openmct: this.openmct,
|
||||
notebookAnnotations: entryAnnotations
|
||||
});
|
||||
if (event) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
this.filterAndSortEntries();
|
||||
this.focusEntryId = id;
|
||||
this.selectedEntryId = id;
|
||||
this.filterAndSortEntries();
|
||||
},
|
||||
orientationChange() {
|
||||
this.formatSidebar();
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
@dragover="changeCursor"
|
||||
@drop.capture="cancelEditMode"
|
||||
@drop.prevent="dropOnEntry"
|
||||
@click="selectEntry($event, entry)"
|
||||
@click="selectAndEmitEntry($event, entry)"
|
||||
>
|
||||
<div class="c-ne__time-and-content">
|
||||
<div class="c-ne__time-and-creator-and-delete">
|
||||
@@ -164,7 +164,7 @@
|
||||
<script>
|
||||
import NotebookEmbed from './NotebookEmbed.vue';
|
||||
import TextHighlight from '../../../utils/textHighlight/TextHighlight.vue';
|
||||
import { createNewEmbed } from '../utils/notebook-entries';
|
||||
import { createNewEmbed, selectEntry } from '../utils/notebook-entries';
|
||||
import { saveNotebookImageDomainObject, updateNamespaceOfDomainObject } from '../utils/notebook-image';
|
||||
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
@@ -479,37 +479,18 @@ export default {
|
||||
updateEntryValue($event) {
|
||||
this.editMode = false;
|
||||
const value = $event.target.innerText;
|
||||
if (value !== this.entry.text && value.match(/\S/)) {
|
||||
this.entry.text = sanitizeHtml(value, SANITIZATION_SCHEMA);
|
||||
this.timestampAndUpdate();
|
||||
} else {
|
||||
this.$emit('cancelEdit');
|
||||
}
|
||||
this.entry.text = sanitizeHtml(value, SANITIZATION_SCHEMA);
|
||||
this.timestampAndUpdate();
|
||||
},
|
||||
selectEntry(event, entry) {
|
||||
const targetDetails = {};
|
||||
const keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
targetDetails[keyString] = {
|
||||
entryId: entry.id
|
||||
};
|
||||
const targetDomainObjects = {};
|
||||
targetDomainObjects[keyString] = this.domainObject;
|
||||
this.openmct.selection.select(
|
||||
[
|
||||
{
|
||||
element: event.currentTarget,
|
||||
context: {
|
||||
type: 'notebook-entry-selection',
|
||||
item: this.domainObject,
|
||||
targetDetails,
|
||||
targetDomainObjects,
|
||||
annotations: this.notebookAnnotations,
|
||||
annotationType: this.openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,
|
||||
onAnnotationChange: this.timestampAndUpdate
|
||||
}
|
||||
}
|
||||
],
|
||||
false);
|
||||
selectAndEmitEntry(event, entry) {
|
||||
selectEntry({
|
||||
element: event.currentTarget,
|
||||
entryId: entry.id,
|
||||
domainObject: this.domainObject,
|
||||
openmct: this.openmct,
|
||||
onAnnotationChange: this.timestampAndUpdate,
|
||||
notebookAnnotations: this.notebookAnnotations
|
||||
});
|
||||
event.stopPropagation();
|
||||
this.$emit('entry-selection', this.entry);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
*****************************************************************************/
|
||||
|
||||
import CopyToNotebookAction from './actions/CopyToNotebookAction';
|
||||
import ExportNotebookAsTextAction from './actions/ExportNotebookAsTextAction';
|
||||
import NotebookSnapshotIndicator from './components/NotebookSnapshotIndicator.vue';
|
||||
import NotebookViewProvider from './NotebookViewProvider';
|
||||
import NotebookType from './NotebookType';
|
||||
@@ -80,6 +81,7 @@ function installBaseNotebookFunctionality(openmct) {
|
||||
};
|
||||
openmct.types.addType('notebookSnapshotImage', notebookSnapshotImageType);
|
||||
openmct.actions.register(new CopyToNotebookAction(openmct));
|
||||
openmct.actions.register(new ExportNotebookAsTextAction(openmct));
|
||||
|
||||
const notebookSnapshotIndicator = new Vue ({
|
||||
components: {
|
||||
|
||||
@@ -2,7 +2,7 @@ import objectLink from '../../../ui/mixins/object-link';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
async function getUsername(openmct) {
|
||||
let username = '';
|
||||
let username = null;
|
||||
|
||||
if (openmct.user.hasProvider()) {
|
||||
const user = await openmct.user.getCurrentUser();
|
||||
@@ -44,6 +44,35 @@ export function addEntryIntoPage(notebookStorage, entries, entry) {
|
||||
return newEntries;
|
||||
}
|
||||
|
||||
export function selectEntry({
|
||||
element, entryId, domainObject, openmct,
|
||||
onAnnotationChange, notebookAnnotations
|
||||
}) {
|
||||
const targetDetails = {};
|
||||
const keyString = openmct.objects.makeKeyString(domainObject.identifier);
|
||||
targetDetails[keyString] = {
|
||||
entryId
|
||||
};
|
||||
const targetDomainObjects = {};
|
||||
targetDomainObjects[keyString] = domainObject;
|
||||
openmct.selection.select(
|
||||
[
|
||||
{
|
||||
element,
|
||||
context: {
|
||||
type: 'notebook-entry-selection',
|
||||
item: domainObject,
|
||||
targetDetails,
|
||||
targetDomainObjects,
|
||||
annotations: notebookAnnotations,
|
||||
annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,
|
||||
onAnnotationChange
|
||||
}
|
||||
}
|
||||
],
|
||||
false);
|
||||
}
|
||||
|
||||
export function getHistoricLinkInFixedMode(openmct, bounds, historicLink) {
|
||||
if (historicLink.includes('tc.mode=fixed')) {
|
||||
return historicLink;
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
version: "3"
|
||||
services:
|
||||
couchdb:
|
||||
image: couchdb:${COUCHDB_IMAGE_TAG:-3.2.1}
|
||||
image: couchdb:${COUCHDB_IMAGE_TAG:-3.3.2}
|
||||
ports:
|
||||
- "5984:5984"
|
||||
- "5986:5986"
|
||||
volumes:
|
||||
- couchdb:/opt/couchdb/data
|
||||
environment:
|
||||
|
||||
@@ -1,57 +1,25 @@
|
||||
#!/bin/bash -e
|
||||
|
||||
# Do a couple checks for environment variables we expect to have a value.
|
||||
|
||||
if [ -z "${OPENMCT_DATABASE_NAME}" ] ; then
|
||||
echo "OPENMCT_DATABASE_NAME has no value" 1>&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${COUCH_ADMIN_USER}" ] ; then
|
||||
echo "COUCH_ADMIN_USER has no value" 1>&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${COUCH_BASE_LOCAL}" ] ; then
|
||||
echo "COUCH_BASE_LOCAL has no value" 1>&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Come up with what we'll be providing to curl's -u option. Always supply the username from the environment,
|
||||
# and optionally supply the password from the environment, if it has a value.
|
||||
CURL_USERPASS_ARG="${COUCH_ADMIN_USER}"
|
||||
if [ "${COUCH_ADMIN_PASSWORD}" ] ; then
|
||||
CURL_USERPASS_ARG+=":${COUCH_ADMIN_PASSWORD}"
|
||||
fi
|
||||
|
||||
system_tables_exist () {
|
||||
resource_exists $COUCH_BASE_LOCAL/_users
|
||||
}
|
||||
|
||||
create_users_db () {
|
||||
curl -su "${CURL_USERPASS_ARG}" -X PUT $COUCH_BASE_LOCAL/_users
|
||||
}
|
||||
|
||||
create_replicator_db () {
|
||||
curl -su "${CURL_USERPASS_ARG}" -X PUT $COUCH_BASE_LOCAL/_replicator
|
||||
}
|
||||
|
||||
setup_system_tables () {
|
||||
users_db_response=$(create_users_db)
|
||||
if [ "{\"ok\":true}" == "${users_db_response}" ]; then
|
||||
echo Successfully created users db
|
||||
replicator_db_response=$(create_replicator_db)
|
||||
if [ "{\"ok\":true}" == "${replicator_db_response}" ]; then
|
||||
echo Successfully created replicator DB
|
||||
else
|
||||
echo Unable to create replicator DB
|
||||
fi
|
||||
else
|
||||
echo Unable to create users db
|
||||
# Check if required environment variables have values, exit if not.
|
||||
check_env_var() {
|
||||
if [ -z "$1" ]; then
|
||||
echo "$2 has no value" 1>&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
resource_exists () {
|
||||
check_env_var "${OPENMCT_DATABASE_NAME}" "OPENMCT_DATABASE_NAME"
|
||||
check_env_var "${COUCH_ADMIN_USER}" "COUCH_ADMIN_USER"
|
||||
check_env_var "${COUCH_BASE_LOCAL}" "COUCH_BASE_LOCAL"
|
||||
|
||||
# Construct curl's -u option value based on COUCH_ADMIN_USER and COUCH_ADMIN_PASSWORD environment variables.
|
||||
CURL_USERPASS_ARG="${COUCH_ADMIN_USER}"
|
||||
if [ "${COUCH_ADMIN_PASSWORD}" ]; then
|
||||
CURL_USERPASS_ARG+=":${COUCH_ADMIN_PASSWORD}"
|
||||
fi
|
||||
|
||||
# Functions
|
||||
resource_exists() {
|
||||
response=$(curl -u "${CURL_USERPASS_ARG}" -s -o /dev/null -I -w "%{http_code}" $1);
|
||||
if [ "200" == "${response}" ]; then
|
||||
echo "TRUE"
|
||||
@@ -60,16 +28,16 @@ resource_exists () {
|
||||
fi
|
||||
}
|
||||
|
||||
db_exists () {
|
||||
db_exists() {
|
||||
resource_exists $COUCH_BASE_LOCAL/$OPENMCT_DATABASE_NAME
|
||||
}
|
||||
|
||||
create_db () {
|
||||
create_db() {
|
||||
response=$(curl -su "${CURL_USERPASS_ARG}" -XPUT $COUCH_BASE_LOCAL/$OPENMCT_DATABASE_NAME);
|
||||
echo $response
|
||||
}
|
||||
|
||||
admin_user_exists () {
|
||||
admin_user_exists() {
|
||||
response=$(curl -su "${CURL_USERPASS_ARG}" -o /dev/null -I -w "%{http_code}" $COUCH_BASE_LOCAL/_node/$COUCH_NODE_NAME/_config/admins/$COUCH_ADMIN_USER);
|
||||
if [ "200" == "${response}" ]; then
|
||||
echo "TRUE"
|
||||
@@ -78,7 +46,7 @@ admin_user_exists () {
|
||||
fi
|
||||
}
|
||||
|
||||
create_admin_user () {
|
||||
create_admin_user() {
|
||||
echo Creating admin user
|
||||
curl -X PUT $COUCH_BASE_LOCAL/_node/$COUCH_NODE_NAME/_config/admins/$COUCH_ADMIN_USER -d \'"$COUCH_ADMIN_PASSWORD"\'
|
||||
}
|
||||
@@ -87,7 +55,7 @@ is_cors_enabled() {
|
||||
resource_exists $COUCH_BASE_LOCAL/_node/$COUCH_NODE_NAME/_config/httpd/enable_cors
|
||||
}
|
||||
|
||||
enable_cors () {
|
||||
enable_cors() {
|
||||
curl -su "${CURL_USERPASS_ARG}" -o /dev/null -X PUT $COUCH_BASE_LOCAL/_node/$COUCH_NODE_NAME/_config/httpd/enable_cors -d '"true"'
|
||||
curl -su "${CURL_USERPASS_ARG}" -o /dev/null -X PUT $COUCH_BASE_LOCAL/_node/$COUCH_NODE_NAME/_config/cors/origins -d '"*"'
|
||||
curl -su "${CURL_USERPASS_ARG}" -o /dev/null -X PUT $COUCH_BASE_LOCAL/_node/$COUCH_NODE_NAME/_config/cors/credentials -d '"true"'
|
||||
@@ -95,6 +63,36 @@ enable_cors () {
|
||||
curl -su "${CURL_USERPASS_ARG}" -o /dev/null -X PUT $COUCH_BASE_LOCAL/_node/$COUCH_NODE_NAME/_config/cors/headers -d '"accept, authorization, content-type, origin, referer, x-csrf-token"'
|
||||
}
|
||||
|
||||
update_db_permissions() {
|
||||
local db_name=$1
|
||||
echo "Updating ${db_name} database permissions"
|
||||
response=$(curl -su "${CURL_USERPASS_ARG}" --location \
|
||||
--request PUT $COUCH_BASE_LOCAL/$db_name/_security \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw '{ "admins": {"roles": []},"members": {"roles": []}}')
|
||||
if [ "{\"ok\":true}" == "${response}" ]; then
|
||||
echo "Database permissions successfully updated"
|
||||
else
|
||||
echo "Database permissions not updated"
|
||||
fi
|
||||
}
|
||||
|
||||
create_system_tables() {
|
||||
local system_tables=("_users" "_replicator")
|
||||
for table in "${system_tables[@]}"; do
|
||||
echo "Creating $table database"
|
||||
response=$(curl -su "${CURL_USERPASS_ARG}" -X PUT $COUCH_BASE_LOCAL/$table)
|
||||
if [ "{\"ok\":true}" == "${response}" ]; then
|
||||
echo "Successfully created $table database"
|
||||
else
|
||||
echo "Unable to create $table database"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Main script execution
|
||||
|
||||
# Check if the admin user exists; if not, create it.
|
||||
if [ "$(admin_user_exists)" == "FALSE" ]; then
|
||||
echo "Admin user does not exist, creating..."
|
||||
create_admin_user
|
||||
@@ -102,40 +100,32 @@ else
|
||||
echo "Admin user exists"
|
||||
fi
|
||||
|
||||
if [ "TRUE" == $(system_tables_exist) ]; then
|
||||
echo System tables exist, skipping creation
|
||||
# Check if system tables exist; if not, create them.
|
||||
system_tables_exist=$(resource_exists $COUCH_BASE_LOCAL/_users)
|
||||
if [ "TRUE" == "${system_tables_exist}" ]; then
|
||||
echo "System tables exist, skipping creation"
|
||||
else
|
||||
echo Is fresh install, creating system tables
|
||||
setup_system_tables
|
||||
echo "Fresh install, creating system tables"
|
||||
create_system_tables
|
||||
fi
|
||||
|
||||
# Check if the database exists; if not, create it.
|
||||
if [ "FALSE" == $(db_exists) ]; then
|
||||
response=$(create_db)
|
||||
if [ "{\"ok\":true}" == "${response}" ]; then
|
||||
echo Database successfully created
|
||||
echo "Database successfully created"
|
||||
else
|
||||
echo Database creation failed
|
||||
echo "Database creation failed"
|
||||
fi
|
||||
else
|
||||
echo Database already exists, nothing to do
|
||||
echo "Database already exists, nothing to do"
|
||||
fi
|
||||
|
||||
echo "Updating _replicator database permissions"
|
||||
response=$(curl -su "${CURL_USERPASS_ARG}" --location --request PUT $COUCH_BASE_LOCAL/_replicator/_security --header 'Content-Type: application/json' --data-raw '{ "admins": {"roles": []},"members": {"roles": []}}');
|
||||
if [ "{\"ok\":true}" == "${response}" ]; then
|
||||
echo "Database permissions successfully updated"
|
||||
else
|
||||
echo "Database permissions not updated"
|
||||
fi
|
||||
|
||||
echo "Updating ${OPENMCT_DATABASE_NAME} database permissions"
|
||||
response=$(curl -su "${CURL_USERPASS_ARG}" --location --request PUT $COUCH_BASE_LOCAL/$OPENMCT_DATABASE_NAME/_security --header 'Content-Type: application/json' --data-raw '{ "admins": {"roles": []},"members": {"roles": []}}');
|
||||
if [ "{\"ok\":true}" == "${response}" ]; then
|
||||
echo "Database permissions successfully updated"
|
||||
else
|
||||
echo "Database permissions not updated"
|
||||
fi
|
||||
# Update _replicator and OPENMCT_DATABASE_NAME database permissions
|
||||
update_db_permissions "_replicator"
|
||||
update_db_permissions "${OPENMCT_DATABASE_NAME}"
|
||||
|
||||
# Check if CORS is enabled; if not, enable it.
|
||||
if [ "FALSE" == $(is_cors_enabled) ]; then
|
||||
echo "Enabling CORS"
|
||||
enable_cors
|
||||
|
||||
@@ -211,6 +211,7 @@ export default {
|
||||
this.removeFromComposition(this.planObject);
|
||||
this.planObject = domainObject;
|
||||
this.planData = getValidatedData(domainObject);
|
||||
this.setStatus(this.openmct.status.get(domainObject.identifier));
|
||||
this.setScaleAndGenerateActivities();
|
||||
dialog.dismiss();
|
||||
}
|
||||
@@ -232,6 +233,7 @@ export default {
|
||||
this.planObject = domainObject;
|
||||
this.swimlaneVisibility = this.configuration.swimlaneVisibility;
|
||||
this.planData = getValidatedData(domainObject);
|
||||
this.setStatus(this.openmct.status.get(domainObject.identifier));
|
||||
this.setScaleAndGenerateActivities();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
</li>
|
||||
<li class="grid-row">
|
||||
<div
|
||||
id="log-mode-checkbox"
|
||||
class="grid-cell label"
|
||||
title="Enable log mode."
|
||||
>
|
||||
@@ -29,6 +30,7 @@
|
||||
<input
|
||||
v-model="logMode"
|
||||
class="js-log-mode-input"
|
||||
aria-labelledby="log-mode-checkbox"
|
||||
type="checkbox"
|
||||
@change="updateForm('logMode')"
|
||||
/>
|
||||
@@ -36,12 +38,14 @@
|
||||
</li>
|
||||
<li class="grid-row">
|
||||
<div
|
||||
id="autoscale-checkbox"
|
||||
class="grid-cell label"
|
||||
title="Automatically scale the Y axis to keep all values in view."
|
||||
>Auto scale</div>
|
||||
<div class="grid-cell value"><input
|
||||
v-model="autoscale"
|
||||
type="checkbox"
|
||||
aria-labelledby="autoscale-checkbox"
|
||||
@change="updateForm('autoscale')"
|
||||
></div>
|
||||
</li>
|
||||
|
||||
@@ -46,6 +46,7 @@ export default class RemoteClock extends DefaultClock {
|
||||
|
||||
this.timeTelemetryObject = undefined;
|
||||
this.parseTime = undefined;
|
||||
this.formatTime = undefined;
|
||||
this.metadata = undefined;
|
||||
|
||||
this.lastTick = 0;
|
||||
@@ -137,6 +138,10 @@ export default class RemoteClock extends DefaultClock {
|
||||
this.parseTime = (datum) => {
|
||||
return timeFormatter.parse(datum);
|
||||
};
|
||||
|
||||
this.formatTime = (datum) => {
|
||||
return timeFormatter.format(datum);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -46,76 +46,96 @@ class StaticModelProvider {
|
||||
throw new Error(keyString + ' not found in import models.');
|
||||
}
|
||||
|
||||
parseObjectLeaf(objectLeaf, idMap, namespace) {
|
||||
parseObjectLeaf(objectLeaf, idMap, newRootNamespace, oldRootNamespace) {
|
||||
Object.keys(objectLeaf).forEach((nodeKey) => {
|
||||
if (idMap.get(nodeKey)) {
|
||||
const newIdentifier = objectUtils.makeKeyString({
|
||||
namespace,
|
||||
namespace: newRootNamespace,
|
||||
key: idMap.get(nodeKey)
|
||||
});
|
||||
objectLeaf[newIdentifier] = { ...objectLeaf[nodeKey] };
|
||||
delete objectLeaf[nodeKey];
|
||||
objectLeaf[newIdentifier] = this.parseTreeLeaf(newIdentifier, objectLeaf[newIdentifier], idMap, namespace);
|
||||
objectLeaf[newIdentifier] = this.parseTreeLeaf(newIdentifier, objectLeaf[newIdentifier], idMap, newRootNamespace, oldRootNamespace);
|
||||
} else {
|
||||
objectLeaf[nodeKey] = this.parseTreeLeaf(nodeKey, objectLeaf[nodeKey], idMap, namespace);
|
||||
objectLeaf[nodeKey] = this.parseTreeLeaf(nodeKey, objectLeaf[nodeKey], idMap, newRootNamespace, oldRootNamespace);
|
||||
}
|
||||
});
|
||||
|
||||
return objectLeaf;
|
||||
}
|
||||
|
||||
parseArrayLeaf(arrayLeaf, idMap, namespace) {
|
||||
parseArrayLeaf(arrayLeaf, idMap, newRootNamespace, oldRootNamespace) {
|
||||
return arrayLeaf.map((leafValue, index) => this.parseTreeLeaf(
|
||||
null, leafValue, idMap, namespace));
|
||||
null, leafValue, idMap, newRootNamespace, oldRootNamespace));
|
||||
}
|
||||
|
||||
parseBranchedLeaf(branchedLeafValue, idMap, namespace) {
|
||||
parseBranchedLeaf(branchedLeafValue, idMap, newRootNamespace, oldRootNamespace) {
|
||||
if (Array.isArray(branchedLeafValue)) {
|
||||
return this.parseArrayLeaf(branchedLeafValue, idMap, namespace);
|
||||
return this.parseArrayLeaf(branchedLeafValue, idMap, newRootNamespace, oldRootNamespace);
|
||||
} else {
|
||||
return this.parseObjectLeaf(branchedLeafValue, idMap, namespace);
|
||||
return this.parseObjectLeaf(branchedLeafValue, idMap, newRootNamespace, oldRootNamespace);
|
||||
}
|
||||
}
|
||||
|
||||
parseTreeLeaf(leafKey, leafValue, idMap, namespace) {
|
||||
parseTreeLeaf(leafKey, leafValue, idMap, newRootNamespace, oldRootNamespace) {
|
||||
if (leafValue === null || leafValue === undefined) {
|
||||
return leafValue;
|
||||
}
|
||||
|
||||
const hasChild = typeof leafValue === 'object';
|
||||
if (hasChild) {
|
||||
return this.parseBranchedLeaf(leafValue, idMap, namespace);
|
||||
return this.parseBranchedLeaf(leafValue, idMap, newRootNamespace, oldRootNamespace);
|
||||
}
|
||||
|
||||
if (leafKey === 'key') {
|
||||
return idMap.get(leafValue);
|
||||
} else if (leafKey === 'namespace') {
|
||||
return namespace;
|
||||
} else if (leafKey === 'location') {
|
||||
if (idMap.get(leafValue)) {
|
||||
const newLocationIdentifier = objectUtils.makeKeyString({
|
||||
namespace,
|
||||
key: idMap.get(leafValue)
|
||||
});
|
||||
|
||||
return newLocationIdentifier;
|
||||
let mappedLeafValue;
|
||||
if (oldRootNamespace) {
|
||||
mappedLeafValue = idMap.get(objectUtils.makeKeyString({
|
||||
namespace: oldRootNamespace,
|
||||
key: leafValue
|
||||
}));
|
||||
} else {
|
||||
mappedLeafValue = idMap.get(leafValue);
|
||||
}
|
||||
|
||||
return null;
|
||||
} else if (idMap.get(leafValue)) {
|
||||
const newIdentifier = objectUtils.makeKeyString({
|
||||
namespace,
|
||||
key: idMap.get(leafValue)
|
||||
return mappedLeafValue ?? leafValue;
|
||||
} else if (leafKey === 'namespace') {
|
||||
// Only rewrite the namespace if it matches the old root namespace.
|
||||
// This is to prevent rewriting namespaces of objects that are not
|
||||
// children of the root object (e.g.: objects from a telemetry dictionary)
|
||||
return leafValue === oldRootNamespace
|
||||
? newRootNamespace
|
||||
: leafValue;
|
||||
} else if (leafKey === 'location') {
|
||||
const mappedLeafValue = idMap.get(leafValue);
|
||||
if (!mappedLeafValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const newLocationIdentifier = objectUtils.makeKeyString({
|
||||
namespace: newRootNamespace,
|
||||
key: mappedLeafValue
|
||||
});
|
||||
|
||||
return newIdentifier;
|
||||
return newLocationIdentifier;
|
||||
} else {
|
||||
return leafValue;
|
||||
const mappedLeafValue = idMap.get(leafValue);
|
||||
if (mappedLeafValue) {
|
||||
const newIdentifier = objectUtils.makeKeyString({
|
||||
namespace: newRootNamespace,
|
||||
key: mappedLeafValue
|
||||
});
|
||||
|
||||
return newIdentifier;
|
||||
} else {
|
||||
return leafValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rewriteObjectIdentifiers(importData, rootIdentifier) {
|
||||
const namespace = rootIdentifier.namespace;
|
||||
const { namespace: oldRootNamespace } = objectUtils.parseKeyString(importData.rootId);
|
||||
const { namespace: newRootNamespace } = rootIdentifier;
|
||||
const idMap = new Map();
|
||||
const objectTree = importData.openmct;
|
||||
|
||||
@@ -128,7 +148,7 @@ class StaticModelProvider {
|
||||
idMap.set(originalId, newId);
|
||||
});
|
||||
|
||||
const newTree = this.parseTreeLeaf(null, objectTree, idMap, namespace);
|
||||
const newTree = this.parseTreeLeaf(null, objectTree, idMap, newRootNamespace, oldRootNamespace);
|
||||
|
||||
return newTree;
|
||||
}
|
||||
|
||||
@@ -20,130 +20,265 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import testStaticData from './static-provider-test.json';
|
||||
import testStaticDataEmptyNamespace from './test-data/static-provider-test-empty-namespace.json';
|
||||
import testStaticDataFooNamespace from './test-data/static-provider-test-foo-namespace.json';
|
||||
import StaticModelProvider from './StaticModelProvider';
|
||||
|
||||
describe('StaticModelProvider', function () {
|
||||
describe('with empty namespace', function () {
|
||||
|
||||
let staticProvider;
|
||||
|
||||
beforeEach(function () {
|
||||
const staticData = JSON.parse(JSON.stringify(testStaticData));
|
||||
staticProvider = new StaticModelProvider(staticData, {
|
||||
namespace: 'my-import',
|
||||
key: 'root'
|
||||
});
|
||||
});
|
||||
|
||||
describe('rootObject', function () {
|
||||
let rootModel;
|
||||
let staticProvider;
|
||||
|
||||
beforeEach(function () {
|
||||
rootModel = staticProvider.get({
|
||||
const staticData = JSON.parse(JSON.stringify(testStaticDataEmptyNamespace));
|
||||
staticProvider = new StaticModelProvider(staticData, {
|
||||
namespace: 'my-import',
|
||||
key: 'root'
|
||||
});
|
||||
});
|
||||
|
||||
it('is located at top level', function () {
|
||||
expect(rootModel.location).toBe('ROOT');
|
||||
describe('rootObject', function () {
|
||||
let rootModel;
|
||||
|
||||
beforeEach(function () {
|
||||
rootModel = staticProvider.get({
|
||||
namespace: 'my-import',
|
||||
key: 'root'
|
||||
});
|
||||
});
|
||||
|
||||
it('is located at top level', function () {
|
||||
expect(rootModel.location).toBe('ROOT');
|
||||
});
|
||||
|
||||
it('has remapped identifier', function () {
|
||||
expect(rootModel.identifier).toEqual({
|
||||
namespace: 'my-import',
|
||||
key: 'root'
|
||||
});
|
||||
});
|
||||
|
||||
it('has remapped identifiers in composition', function () {
|
||||
expect(rootModel.composition).toContain({
|
||||
namespace: 'my-import',
|
||||
key: '1'
|
||||
});
|
||||
expect(rootModel.composition).toContain({
|
||||
namespace: 'my-import',
|
||||
key: '2'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('has new-format identifier', function () {
|
||||
expect(rootModel.identifier).toEqual({
|
||||
describe('childObjects', function () {
|
||||
let swg;
|
||||
let layout;
|
||||
let fixed;
|
||||
|
||||
beforeEach(function () {
|
||||
swg = staticProvider.get({
|
||||
namespace: 'my-import',
|
||||
key: '1'
|
||||
});
|
||||
layout = staticProvider.get({
|
||||
namespace: 'my-import',
|
||||
key: '2'
|
||||
});
|
||||
fixed = staticProvider.get({
|
||||
namespace: 'my-import',
|
||||
key: '3'
|
||||
});
|
||||
});
|
||||
|
||||
it('match expected ordering', function () {
|
||||
// this is a sanity check to make sure the identifiers map in
|
||||
// the correct order.
|
||||
expect(swg.type).toBe('generator');
|
||||
expect(layout.type).toBe('layout');
|
||||
expect(fixed.type).toBe('telemetry.fixed');
|
||||
});
|
||||
|
||||
it('have remapped identifiers', function () {
|
||||
expect(swg.identifier).toEqual({
|
||||
namespace: 'my-import',
|
||||
key: '1'
|
||||
});
|
||||
expect(layout.identifier).toEqual({
|
||||
namespace: 'my-import',
|
||||
key: '2'
|
||||
});
|
||||
expect(fixed.identifier).toEqual({
|
||||
namespace: 'my-import',
|
||||
key: '3'
|
||||
});
|
||||
});
|
||||
|
||||
it('have remapped composition', function () {
|
||||
expect(layout.composition).toContain({
|
||||
namespace: 'my-import',
|
||||
key: '1'
|
||||
});
|
||||
expect(layout.composition).toContain({
|
||||
namespace: 'my-import',
|
||||
key: '3'
|
||||
});
|
||||
expect(fixed.composition).toContain({
|
||||
namespace: 'my-import',
|
||||
key: '1'
|
||||
});
|
||||
});
|
||||
|
||||
it('rewrites locations', function () {
|
||||
expect(swg.location).toBe('my-import:root');
|
||||
expect(layout.location).toBe('my-import:root');
|
||||
expect(fixed.location).toBe('my-import:2');
|
||||
});
|
||||
|
||||
it('rewrites matched identifiers in objects', function () {
|
||||
expect(layout.configuration.layout.panels['my-import:1'])
|
||||
.toBeDefined();
|
||||
expect(layout.configuration.layout.panels['my-import:3'])
|
||||
.toBeDefined();
|
||||
expect(layout.configuration.layout.panels['483c00d4-bb1d-4b42-b29a-c58e06b322a0'])
|
||||
.not.toBeDefined();
|
||||
expect(layout.configuration.layout.panels['20273193-f069-49e9-b4f7-b97a87ed755d'])
|
||||
.not.toBeDefined();
|
||||
expect(fixed.configuration['fixed-display'].elements[0].id)
|
||||
.toBe('my-import:1');
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
describe('with namespace "foo"', function () {
|
||||
|
||||
let staticProvider;
|
||||
|
||||
beforeEach(function () {
|
||||
const staticData = JSON.parse(JSON.stringify(testStaticDataFooNamespace));
|
||||
staticProvider = new StaticModelProvider(staticData, {
|
||||
namespace: 'my-import',
|
||||
key: 'root'
|
||||
});
|
||||
});
|
||||
|
||||
it('has new-format composition', function () {
|
||||
expect(rootModel.composition).toContain({
|
||||
namespace: 'my-import',
|
||||
key: '1'
|
||||
});
|
||||
expect(rootModel.composition).toContain({
|
||||
namespace: 'my-import',
|
||||
key: '2'
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('rootObject', function () {
|
||||
let rootModel;
|
||||
|
||||
describe('childObjects', function () {
|
||||
let swg;
|
||||
let layout;
|
||||
let fixed;
|
||||
beforeEach(function () {
|
||||
rootModel = staticProvider.get({
|
||||
namespace: 'my-import',
|
||||
key: 'root'
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(function () {
|
||||
swg = staticProvider.get({
|
||||
namespace: 'my-import',
|
||||
key: '1'
|
||||
it('is located at top level', function () {
|
||||
expect(rootModel.location).toBe('ROOT');
|
||||
});
|
||||
layout = staticProvider.get({
|
||||
namespace: 'my-import',
|
||||
key: '2'
|
||||
|
||||
it('has remapped identifier', function () {
|
||||
expect(rootModel.identifier).toEqual({
|
||||
namespace: 'my-import',
|
||||
key: 'root'
|
||||
});
|
||||
});
|
||||
fixed = staticProvider.get({
|
||||
namespace: 'my-import',
|
||||
key: '3'
|
||||
|
||||
it('has remapped composition', function () {
|
||||
expect(rootModel.composition).toContain({
|
||||
namespace: 'my-import',
|
||||
key: '1'
|
||||
});
|
||||
expect(rootModel.composition).toContain({
|
||||
namespace: 'my-import',
|
||||
key: '2'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('match expected ordering', function () {
|
||||
// this is a sanity check to make sure the identifiers map in
|
||||
// the correct order.
|
||||
expect(swg.type).toBe('generator');
|
||||
expect(layout.type).toBe('layout');
|
||||
expect(fixed.type).toBe('telemetry.fixed');
|
||||
});
|
||||
describe('childObjects', function () {
|
||||
let clock;
|
||||
let layout;
|
||||
let swg;
|
||||
let folder;
|
||||
|
||||
it('have new-style identifiers', function () {
|
||||
expect(swg.identifier).toEqual({
|
||||
namespace: 'my-import',
|
||||
key: '1'
|
||||
beforeEach(function () {
|
||||
folder = staticProvider.get({
|
||||
namespace: 'my-import',
|
||||
key: 'root'
|
||||
});
|
||||
layout = staticProvider.get({
|
||||
namespace: 'my-import',
|
||||
key: '1'
|
||||
});
|
||||
swg = staticProvider.get({
|
||||
namespace: 'my-import',
|
||||
key: '2'
|
||||
});
|
||||
clock = staticProvider.get({
|
||||
namespace: 'my-import',
|
||||
key: '3'
|
||||
});
|
||||
});
|
||||
expect(layout.identifier).toEqual({
|
||||
namespace: 'my-import',
|
||||
key: '2'
|
||||
|
||||
it('match expected ordering', function () {
|
||||
// this is a sanity check to make sure the identifiers map in
|
||||
// the correct order.
|
||||
expect(folder.type).toBe('folder');
|
||||
expect(swg.type).toBe('generator');
|
||||
expect(layout.type).toBe('layout');
|
||||
expect(clock.type).toBe('clock');
|
||||
});
|
||||
expect(fixed.identifier).toEqual({
|
||||
namespace: 'my-import',
|
||||
key: '3'
|
||||
|
||||
it('have remapped identifiers', function () {
|
||||
expect(folder.identifier).toEqual({
|
||||
namespace: 'my-import',
|
||||
key: 'root'
|
||||
});
|
||||
expect(layout.identifier).toEqual({
|
||||
namespace: 'my-import',
|
||||
key: '1'
|
||||
});
|
||||
expect(swg.identifier).toEqual({
|
||||
namespace: 'my-import',
|
||||
key: '2'
|
||||
});
|
||||
expect(clock.identifier).toEqual({
|
||||
namespace: 'my-import',
|
||||
key: '3'
|
||||
});
|
||||
});
|
||||
|
||||
it('have remapped identifiers in composition', function () {
|
||||
expect(layout.composition).toContain({
|
||||
namespace: 'my-import',
|
||||
key: '2'
|
||||
});
|
||||
expect(layout.composition).toContain({
|
||||
namespace: 'my-import',
|
||||
key: '3'
|
||||
});
|
||||
});
|
||||
|
||||
it('layout has remapped identifiers in configuration', function () {
|
||||
const identifiers = layout.configuration.items
|
||||
.map(item => item.identifier)
|
||||
.filter(identifier => identifier !== undefined);
|
||||
expect(identifiers).toContain({
|
||||
namespace: 'my-import',
|
||||
key: '2'
|
||||
});
|
||||
expect(identifiers).toContain({
|
||||
namespace: 'my-import',
|
||||
key: '3'
|
||||
});
|
||||
});
|
||||
|
||||
it('rewrites locations', function () {
|
||||
expect(folder.location).toBe('ROOT');
|
||||
expect(swg.location).toBe('my-import:root');
|
||||
expect(layout.location).toBe('my-import:root');
|
||||
expect(clock.location).toBe('my-import:root');
|
||||
});
|
||||
});
|
||||
|
||||
it('have new-style composition', function () {
|
||||
expect(layout.composition).toContain({
|
||||
namespace: 'my-import',
|
||||
key: '1'
|
||||
});
|
||||
expect(layout.composition).toContain({
|
||||
namespace: 'my-import',
|
||||
key: '3'
|
||||
});
|
||||
expect(fixed.composition).toContain({
|
||||
namespace: 'my-import',
|
||||
key: '1'
|
||||
});
|
||||
});
|
||||
|
||||
it('rewrites locations', function () {
|
||||
expect(swg.location).toBe('my-import:root');
|
||||
expect(layout.location).toBe('my-import:root');
|
||||
expect(fixed.location).toBe('my-import:2');
|
||||
});
|
||||
|
||||
it('rewrites matched identifiers in objects', function () {
|
||||
expect(layout.configuration.layout.panels['my-import:1'])
|
||||
.toBeDefined();
|
||||
expect(layout.configuration.layout.panels['my-import:3'])
|
||||
.toBeDefined();
|
||||
expect(layout.configuration.layout.panels['483c00d4-bb1d-4b42-b29a-c58e06b322a0'])
|
||||
.not.toBeDefined();
|
||||
expect(layout.configuration.layout.panels['20273193-f069-49e9-b4f7-b97a87ed755d'])
|
||||
.not.toBeDefined();
|
||||
expect(fixed.configuration['fixed-display'].elements[0].id)
|
||||
.toBe('my-import:1');
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"openmct":{"foo:a6c9e745-89e5-4c3a-8f54-7a34711aaeb1":{"identifier":{"key":"a6c9e745-89e5-4c3a-8f54-7a34711aaeb1","namespace":"foo"},"name":"Folder Foo","type":"folder","composition":[{"key":"95729018-86ed-4484-867d-10c63c41c5a1","namespace":"foo"},{"key":"22c438f0-953b-42c5-8fb2-9d5dbeb88a0c","namespace":"foo"},{"key":"3545554b-53c8-467d-a70d-e90d1a120e4a","namespace":"foo"}],"modified":1681164966705,"location":"foo:mine","created":1681164829371,"persisted":1681164966706},"foo:95729018-86ed-4484-867d-10c63c41c5a1":{"identifier":{"key":"95729018-86ed-4484-867d-10c63c41c5a1","namespace":"foo"},"name":"Display Layout Bar","type":"layout","composition":[{"key":"22c438f0-953b-42c5-8fb2-9d5dbeb88a0c","namespace":"foo"},{"key":"3545554b-53c8-467d-a70d-e90d1a120e4a","namespace":"foo"}],"configuration":{"items":[{"fill":"#666666","stroke":"","x":42,"y":42,"width":20,"height":4,"type":"box-view","id":"14505a5d-b846-4504-961f-8c9bcdf19f39"},{"identifier":{"key":"22c438f0-953b-42c5-8fb2-9d5dbeb88a0c","namespace":"foo"},"x":0,"y":0,"width":40,"height":15,"displayMode":"all","value":"sin","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"05baa95f-2064-4cb0-ad9f-575758491220"},{"width":40,"height":15,"x":0,"y":15,"identifier":{"key":"3545554b-53c8-467d-a70d-e90d1a120e4a","namespace":"foo"},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"70e1b8b7-cd59-4a52-b796-d68fb0c48fc5"}],"layoutGrid":[10,10],"objectStyles":{"05baa95f-2064-4cb0-ad9f-575758491220":{"staticStyle":{"style":{"border":"1px solid #00ff00","backgroundColor":"#0000ff","color":"#ff00ff"}}}}},"modified":1681165037189,"location":"foo:a6c9e745-89e5-4c3a-8f54-7a34711aaeb1","created":1681164838178,"persisted":1681165037190},"foo:22c438f0-953b-42c5-8fb2-9d5dbeb88a0c":{"identifier":{"key":"22c438f0-953b-42c5-8fb2-9d5dbeb88a0c","namespace":"foo"},"name":"SWG Baz","type":"generator","telemetry":{"period":"20","amplitude":"2","offset":"5","dataRateInHz":1,"phase":0,"randomness":0,"loadDelay":0,"infinityValues":false,"staleness":false},"modified":1681164910719,"location":"foo:a6c9e745-89e5-4c3a-8f54-7a34711aaeb1","created":1681164903684,"persisted":1681164910719},"foo:3545554b-53c8-467d-a70d-e90d1a120e4a":{"identifier":{"key":"3545554b-53c8-467d-a70d-e90d1a120e4a","namespace":"foo"},"name":"Clock Qux","type":"clock","configuration":{"baseFormat":"YYYY/MM/DD hh:mm:ss","use24":"clock12","timezone":"UTC"},"modified":1681164989837,"location":"foo:a6c9e745-89e5-4c3a-8f54-7a34711aaeb1","created":1681164966702,"persisted":1681164989837}},"rootId":"foo:a6c9e745-89e5-4c3a-8f54-7a34711aaeb1"}
|
||||
@@ -88,7 +88,7 @@ define([], function () {
|
||||
}
|
||||
|
||||
getContextMenuActions() {
|
||||
return ['viewDatumAction'];
|
||||
return ['viewDatumAction', 'viewHistoricalData'];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -175,14 +175,22 @@ export default {
|
||||
getDatum() {
|
||||
return this.row.fullDatum;
|
||||
},
|
||||
showContextMenu: function (event) {
|
||||
showContextMenu: async function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
this.updateViewContext();
|
||||
this.markRow(event);
|
||||
|
||||
const contextualDomainObject = await this.row.getContextualDomainObject?.(this.openmct, this.row.objectKeyString);
|
||||
|
||||
let objectPath = this.objectPath;
|
||||
if (contextualDomainObject) {
|
||||
objectPath = objectPath.slice();
|
||||
objectPath.unshift(contextualDomainObject);
|
||||
}
|
||||
|
||||
const actions = this.row.getContextMenuActions().map(key => this.openmct.actions.getAction(key));
|
||||
const menuItems = this.openmct.menus.actionsToMenuItems(actions, this.objectPath, this.currentView);
|
||||
const menuItems = this.openmct.menus.actionsToMenuItems(actions, objectPath, this.currentView);
|
||||
if (menuItems.length) {
|
||||
this.openmct.menus.showMenu(event.x, event.y, menuItems);
|
||||
}
|
||||
|
||||
@@ -38,9 +38,8 @@
|
||||
import {getValidatedData} from "../plan/util";
|
||||
import ListView from '../../ui/components/List/ListView.vue';
|
||||
import {getPreciseDuration} from "../../utils/duration";
|
||||
import ticker from 'utils/clock/Ticker';
|
||||
import {SORT_ORDER_OPTIONS} from "./constants";
|
||||
|
||||
import _ from 'lodash';
|
||||
import moment from "moment";
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
@@ -53,16 +52,26 @@ const headerItems = [
|
||||
isSortable: true,
|
||||
property: 'start',
|
||||
name: 'Start Time',
|
||||
format: function (value, object) {
|
||||
return `${moment(value).format(TIME_FORMAT)}Z`;
|
||||
format: function (value, object, key, openmct) {
|
||||
const clock = openmct.time.clock();
|
||||
if (clock && clock.formatTime) {
|
||||
return clock.formatTime(value);
|
||||
} else {
|
||||
return `${moment(value).format(TIME_FORMAT)}Z`;
|
||||
}
|
||||
}
|
||||
}, {
|
||||
defaultDirection: true,
|
||||
isSortable: true,
|
||||
property: 'end',
|
||||
name: 'End Time',
|
||||
format: function (value, object) {
|
||||
return `${moment(value).format(TIME_FORMAT)}Z`;
|
||||
format: function (value, object, key, openmct) {
|
||||
const clock = openmct.time.clock();
|
||||
if (clock && clock.formatTime) {
|
||||
return clock.formatTime(value);
|
||||
} else {
|
||||
return `${moment(value).format(TIME_FORMAT)}Z`;
|
||||
}
|
||||
}
|
||||
}, {
|
||||
defaultDirection: false,
|
||||
@@ -119,7 +128,8 @@ export default {
|
||||
this.unlistenConfig = this.openmct.objects.observe(this.domainObject, 'configuration', this.setViewFromConfig);
|
||||
this.removeStatusListener = this.openmct.status.observe(this.domainObject.identifier, this.setStatus);
|
||||
this.status = this.openmct.status.get(this.domainObject.identifier);
|
||||
this.unlistenTicker = ticker.listen(this.clearPreviousActivities);
|
||||
|
||||
this.updateTimestamp = _.throttle(this.updateTimestamp, 1000);
|
||||
this.openmct.time.on('bounds', this.updateTimestamp);
|
||||
this.openmct.editor.on('isEditing', this.setEditState);
|
||||
|
||||
@@ -144,10 +154,6 @@ export default {
|
||||
this.unlistenConfig();
|
||||
}
|
||||
|
||||
if (this.unlistenTicker) {
|
||||
this.unlistenTicker();
|
||||
}
|
||||
|
||||
if (this.removeStatusListener) {
|
||||
this.removeStatusListener();
|
||||
}
|
||||
@@ -192,8 +198,8 @@ export default {
|
||||
}
|
||||
},
|
||||
updateTimestamp(_bounds, isTick) {
|
||||
if (isTick === true) {
|
||||
this.timestamp = this.openmct.time.clock().currentValue();
|
||||
if (isTick === true && this.openmct.time.clock() !== undefined) {
|
||||
this.updateTimeStampAndListActivities(this.openmct.time.clock().currentValue());
|
||||
}
|
||||
},
|
||||
setViewFromClock(newClock) {
|
||||
@@ -202,12 +208,11 @@ export default {
|
||||
if (isFixedTime) {
|
||||
this.hideAll = false;
|
||||
this.showAll = true;
|
||||
// clear invokes listActivities
|
||||
this.clearPreviousActivities(this.openmct.time.bounds()?.start);
|
||||
this.updateTimeStampAndListActivities(this.openmct.time.bounds()?.start);
|
||||
} else {
|
||||
this.setSort();
|
||||
this.setViewBounds();
|
||||
this.listActivities();
|
||||
this.updateTimeStampAndListActivities(this.openmct.time.clock().currentValue());
|
||||
}
|
||||
},
|
||||
addItem(domainObject) {
|
||||
@@ -346,12 +351,8 @@ export default {
|
||||
// sort by start time
|
||||
this.planActivities = activities.sort(this.sortByStartTime);
|
||||
},
|
||||
clearPreviousActivities(time) {
|
||||
if (time instanceof Date) {
|
||||
this.timestamp = time.getTime();
|
||||
} else {
|
||||
this.timestamp = time;
|
||||
}
|
||||
updateTimeStampAndListActivities(time) {
|
||||
this.timestamp = time;
|
||||
|
||||
this.listActivities();
|
||||
},
|
||||
|
||||
@@ -37,7 +37,7 @@ export default {
|
||||
// eslint-disable-next-line you-dont-need-lodash-underscore/get
|
||||
let value = _.get(this.item, property.key);
|
||||
if (property.format) {
|
||||
value = property.format(value, this.item, property.key);
|
||||
value = property.format(value, this.item, property.key, this.openmct);
|
||||
}
|
||||
|
||||
values.push({
|
||||
|
||||
@@ -74,7 +74,7 @@ export default {
|
||||
return this.domainObject && (this.currentObjectPath || this.objectPath);
|
||||
},
|
||||
objectFontStyle() {
|
||||
return this.domainObject && this.domainObject.configuration && this.domainObject.configuration.fontStyle;
|
||||
return this.domainObject?.configuration?.fontStyle;
|
||||
},
|
||||
fontSize() {
|
||||
return this.objectFontStyle ? this.objectFontStyle.fontSize : this.layoutFontSize;
|
||||
@@ -287,6 +287,8 @@ export default {
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.updateStyle(this.styleRuleManager?.currentStyle);
|
||||
this.setFontSize(this.fontSize);
|
||||
this.setFont(this.font);
|
||||
this.getActionCollection();
|
||||
});
|
||||
},
|
||||
@@ -329,9 +331,9 @@ export default {
|
||||
},
|
||||
initObjectStyles() {
|
||||
if (!this.styleRuleManager) {
|
||||
this.styleRuleManager = new StyleRuleManager((this.domainObject.configuration && this.domainObject.configuration.objectStyles), this.openmct, this.updateStyle.bind(this), true);
|
||||
this.styleRuleManager = new StyleRuleManager((this.domainObject.configuration?.objectStyles), this.openmct, this.updateStyle.bind(this), true);
|
||||
} else {
|
||||
this.styleRuleManager.updateObjectStyleConfig(this.domainObject.configuration && this.domainObject.configuration.objectStyles);
|
||||
this.styleRuleManager.updateObjectStyleConfig(this.domainObject.configuration?.objectStyles);
|
||||
}
|
||||
|
||||
if (this.stopListeningStyles) {
|
||||
@@ -343,9 +345,6 @@ export default {
|
||||
this.styleRuleManager.updateObjectStyleConfig(newObjectStyle);
|
||||
});
|
||||
|
||||
this.setFontSize(this.fontSize);
|
||||
this.setFont(this.font);
|
||||
|
||||
this.stopListeningFontStyles = this.openmct.objects.observe(this.domainObject, 'configuration.fontStyle', (newFontStyle) => {
|
||||
this.setFontSize(newFontStyle.fontSize);
|
||||
this.setFont(newFontStyle.font);
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
v-for="(item, index) in statusBarItems"
|
||||
:key="index"
|
||||
class="c-button"
|
||||
:title="item.name"
|
||||
:class="item.cssClass"
|
||||
@click="item.onItemClicked"
|
||||
>
|
||||
@@ -70,6 +71,7 @@
|
||||
v-if="isViewEditable && !isEditing && !domainObject.locked"
|
||||
class="l-browse-bar__actions__edit c-button c-button--major icon-pencil"
|
||||
title="Edit"
|
||||
aria-label="Edit"
|
||||
@click="edit()"
|
||||
></button>
|
||||
|
||||
@@ -80,6 +82,7 @@
|
||||
<button
|
||||
class="c-button--menu c-button--major icon-save"
|
||||
title="Save"
|
||||
aria-label="Save"
|
||||
@click.stop="toggleSaveMenu"
|
||||
></button>
|
||||
<div
|
||||
|
||||
@@ -96,12 +96,14 @@
|
||||
ref="recentObjectsList"
|
||||
class="l-shell__tree"
|
||||
@openAndScrollTo="openAndScrollTo($event)"
|
||||
@setClearButtonDisabled="setClearButtonDisabled"
|
||||
/>
|
||||
<button
|
||||
slot="controls"
|
||||
class="c-icon-button icon-clear-data"
|
||||
aria-label="Clear Recently Viewed"
|
||||
title="Clear Recently Viewed"
|
||||
:disabled="disableClearButton"
|
||||
@click="handleClearRecentObjects"
|
||||
>
|
||||
</button>
|
||||
@@ -150,19 +152,19 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Inspector from '../inspector/Inspector.vue';
|
||||
import MctTree from './mct-tree.vue';
|
||||
import ObjectView from '../components/ObjectView.vue';
|
||||
import CreateButton from './CreateButton.vue';
|
||||
import GrandSearch from './search/GrandSearch.vue';
|
||||
import multipane from './multipane.vue';
|
||||
import pane from './pane.vue';
|
||||
import BrowseBar from './BrowseBar.vue';
|
||||
import Inspector from '../inspector/Inspector.vue';
|
||||
import Toolbar from '../toolbar/Toolbar.vue';
|
||||
import AppLogo from './AppLogo.vue';
|
||||
import BrowseBar from './BrowseBar.vue';
|
||||
import CreateButton from './CreateButton.vue';
|
||||
import RecentObjectsList from './RecentObjectsList.vue';
|
||||
import MctTree from './mct-tree.vue';
|
||||
import multipane from './multipane.vue';
|
||||
import pane from './pane.vue';
|
||||
import GrandSearch from './search/GrandSearch.vue';
|
||||
import Indicators from './status-bar/Indicators.vue';
|
||||
import NotificationBanner from './status-bar/NotificationBanner.vue';
|
||||
import RecentObjectsList from './RecentObjectsList.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -197,7 +199,8 @@ export default {
|
||||
triggerSync: false,
|
||||
triggerReset: false,
|
||||
headExpanded,
|
||||
isResizing: false
|
||||
isResizing: false,
|
||||
disableClearButton: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -296,7 +299,11 @@ export default {
|
||||
},
|
||||
onEndResizing() {
|
||||
this.isResizing = false;
|
||||
},
|
||||
setClearButtonDisabled(isDisabled) {
|
||||
this.disableClearButton = isDisabled;
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user