Compare commits

..

23 Commits

Author SHA1 Message Date
Jamie V
2f3b263eb3 testing lad tables in telemetry tables 2024-04-10 16:21:02 -07:00
Jamie V
8ec3a19550 maps 2024-04-10 15:46:59 -07:00
Jamie V
3b28dd6238 Refactors all bad usage of innerHTML out of our codebase (#7242) 2023-11-30 10:06:28 -08:00
Jamie V
cbf52eb7d5 [Plots] Fix Plots breaking on exceptionally large values (#7120) 2023-10-13 11:50:24 -07:00
David Tsay
d65821ca81 rename table configuration tab to Config 2023-10-12 11:28:14 -07:00
Jamie V
798eb4444d reimplement fix for smushed keys 2023-09-12 13:53:21 -07:00
Jamie V
59ae7cddc5 Merge branch 'omm-release/5.1' of https://github.com/nasa/openmct into omm-release/5.1
mergin
2023-07-07 11:14:50 -07:00
Jamie V
321c7a3af5 suppressing abort errors as they are expected 2023-07-07 11:13:43 -07:00
David Tsay
609cf72bd1 chicken needs to come first 2023-07-06 14:39:26 -07:00
Jamie V
a447b0ada8 removing eval-source-map, dont debuggin 2023-06-21 16:32:57 -07:00
Jamie V
5788f4cc69 temp source maps for debugin 2023-06-21 14:11:23 -07:00
Jamie V
f94b4e53c7 adding back in abort on item close in tree 2023-06-21 13:31:12 -07:00
Jamie V
faf71f1e67 omm-cherry-pick(#6602) : [ExportAsJson] Multiple Aliases in Export an… (#6662)
omm-cherry-pick(#6602) : [ExportAsJson] Multiple Aliases in Export and Conditional Styles Fixes (#6602)

Fixes issues that prevent import and export from being completed successfully. Specifically:

* if multiple aliases are detected, the first is created as a new object and and added to it's parent's composition, any subsequent aliases of the same object will not be recreated, but the originally created one will be added to the current parent's composition, creating an alias.

* Also, there are cases were conditionSetIdentifiers are stored in an object keyed by an item id in the configuration.objectstyles object, this fix will handle these as well.

* Replaces an errant `return` statement with a `continue` statement to prevent early exit from a recursive function.

---------

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2023-05-15 13:12:37 -07:00
David Tsay
23310f85ae cherry pick retain styles on url change 2023-05-02 13:38:13 -07:00
David Tsay
d80819634b revert opening in new tab 2023-04-12 15:01:53 -07:00
Shefali
483b62c152 Only decrement and save if there is composition but no child object reference 2023-04-07 14:08:58 -07:00
Jamie V
1254279635 Fix ExportAsJSONAction to not lose layout configurations on linked objects (#6562) 2023-04-05 23:05:27 -07:00
David Tsay
c768a71656 free findSubscriptionProvider 2023-03-31 15:50:38 -07:00
David Tsay
678a92bd29 Revert "enable source maps in prod"
This reverts commit 34b488944a.
2023-03-31 14:43:02 -07:00
David Tsay
34b488944a enable source maps in prod 2023-03-31 14:42:39 -07:00
David Tsay
4d1dd2f51d add temporary debugging 2023-03-31 12:23:23 -07:00
David Tsay
080f7b8f4b pass openmct and object key to function
re-enable historical row action
2023-03-29 17:09:35 -07:00
David Tsay
483f2feac8 allow contextual domain objects to row actions 2023-03-29 16:58:51 -07:00
157 changed files with 2526 additions and 4681 deletions

View File

@@ -2,15 +2,11 @@ version: 2.1
executors:
pw-focal-development:
docker:
- image: mcr.microsoft.com/playwright:v1.32.3-focal
- image: mcr.microsoft.com/playwright:v1.29.0-focal
environment:
NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed
PERCY_POSTINSTALL_BROWSER: 'true' # Needed to store the percy browser in cache deps
PERCY_LOGLEVEL: 'debug' # Enable DEBUG level logging for Percy (Issue: https://github.com/nasa/openmct/issues/5742)
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!"
@@ -27,8 +23,9 @@ commands:
- restore_cache_cmd:
node-version: << parameters.node-version >>
- node/install:
install-npm: true
node-version: << parameters.node-version >>
- run: npm install --no-audit --progress=false
- run: npm install --prefer-offline --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:
@@ -40,7 +37,7 @@ commands:
equal: [false, << pipeline.parameters.BUST_CACHE >> ]
steps:
- restore_cache:
key: deps--{{ arch }}--{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}-{{ checksum ".circleci/config.yml" }}
key: deps-{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}-{{ checksum ".circleci/config.yml" }}
save_cache_cmd:
description: "Custom command for saving cache."
parameters:
@@ -48,7 +45,7 @@ commands:
type: string
steps:
- save_cache:
key: deps--{{ arch }}--{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}-{{ checksum ".circleci/config.yml" }}
key: deps-{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}-{{ checksum ".circleci/config.yml" }}
paths:
- ~/.npm
- node_modules
@@ -56,8 +53,8 @@ commands:
description: "Track important packages and files"
steps:
- run: |
[[ $EUID -ne 0 ]] && (sudo mkdir -p /tmp/artifacts && sudo chmod 777 /tmp/artifacts) || (mkdir -p /tmp/artifacts && chmod 777 /tmp/artifacts)
printenv NODE_ENV >> /tmp/artifacts/NODE_ENV.txt || true
mkdir /tmp/artifacts
printenv NODE_ENV >> /tmp/artifacts/NODE_ENV.txt
npm -v >> /tmp/artifacts/npm-version.txt
node -v >> /tmp/artifacts/node-version.txt
ls -latR >> /tmp/artifacts/dir.txt
@@ -72,7 +69,7 @@ commands:
- run: npm run cov:e2e:report || true
- run: npm run cov:e2e:<<parameters.suite>>:publish
orbs:
node: circleci/node@5.1.0
node: circleci/node@4.9.0
browser-tools: circleci/browser-tools@1.3.0
jobs:
npm-audit:
@@ -113,11 +110,7 @@ jobs:
path: dist/reports/tests/
- store_artifacts:
path: coverage
- 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
- generate_and_store_version_and_filesystem_artifacts
e2e-test:
parameters:
node-version:
@@ -135,12 +128,8 @@ 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}
- 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>>
- generate_e2e_code_cov_report:
suite: <<parameters.suite>>
- store_test_results:
path: test-results/results.xml
- store_artifacts:
@@ -149,46 +138,7 @@ jobs:
path: coverage
- store_artifacts:
path: html-test-results
- when:
condition:
equal: [ 42, 42 ] # Always generate version artifacts regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2
steps:
- generate_and_store_version_and_filesystem_artifacts
e2e-couchdb:
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
- generate_and_store_version_and_filesystem_artifacts
perf-test:
parameters:
node-version:
@@ -204,11 +154,7 @@ jobs:
path: test-results
- store_artifacts:
path: html-test-results
- 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
- generate_and_store_version_and_filesystem_artifacts
visual-test:
parameters:
node-version:
@@ -224,49 +170,46 @@ jobs:
path: test-results
- 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
- generate_and_store_version_and_filesystem_artifacts
workflows:
overall-circleci-commit-status: #These jobs run on every commit
jobs:
- lint:
name: node16-lint
node-version: lts/gallium
name: node14-lint
node-version: lts/fermium
- unit-test:
name: node18-chrome
node-version: lts/hydrogen
node-version: "18"
- e2e-test:
name: e2e-stable
node-version: lts/hydrogen
node-version: lts/gallium
suite: stable
- perf-test:
node-version: lts/hydrogen
node-version: lts/gallium
- visual-test:
node-version: lts/hydrogen
node-version: lts/gallium
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: lts/hydrogen
node-version: "18"
- npm-audit:
node-version: lts/hydrogen
node-version: lts/gallium
- e2e-test:
name: e2e-full-nightly
node-version: lts/hydrogen
node-version: lts/gallium
suite: full
- perf-test:
node-version: lts/hydrogen
node-version: lts/gallium
- visual-test:
node-version: lts/hydrogen
- e2e-couchdb:
node-version: lts/hydrogen
node-version: lts/gallium
triggers:
- schedule:
cron: "0 0 * * *"

View File

@@ -4,13 +4,13 @@ updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
interval: "daily"
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,15 +25,11 @@ 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"

View File

@@ -5,39 +5,34 @@ 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' }} || ${{ github.event.action == 'opened' }}
if: ${{ github.event.label.name == 'pr:e2e:couchdb' }}
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: 'lts/gallium'
- run: npx playwright@1.32.3 install
node-version: '16'
- run: npx playwright@1.29.0 install
- run: npm install
- name: Start CouchDB Docker Container and Init with Setup Scripts
run : |
export $(cat src/plugins/persistence/couch/.env.ci | xargs)
docker-compose -f src/plugins/persistence/couch/couchdb-compose.yaml up --detach
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
- run: sh src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh
- run: npm run test:e2e:couchdb
- run: ls -latr
- 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

View File

@@ -5,6 +5,7 @@ on:
types:
- labeled
- opened
jobs:
e2e-full:
if: ${{ github.event.label.name == 'pr:e2e' }}
@@ -29,18 +30,11 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version: '16'
- run: npx playwright@1.32.3 install
- run: npx playwright@1.29.0 install
- run: npx playwright install chrome-beta
- run: npm install
- 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
- run: npm run test:e2e:full
- name: Archive test results
if: success() || failure()
uses: actions/upload-artifact@v3
with:
path: test-results

21
.github/workflows/e2e.yml vendored Normal file
View File

@@ -0,0 +1,21 @@
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

View File

@@ -16,6 +16,7 @@ jobs:
- macos-latest
- windows-latest
node_version:
- 14
- 16
- 18
architecture:

View File

@@ -53,11 +53,7 @@ module.exports = merge(common, {
},
client: {
progress: true,
overlay: {
// Disable overlay for runtime errors.
// See: https://github.com/webpack/webpack-dev-server/issues/4771
runtimeErrors: false
}
overlay: true
}
}
});

View File

@@ -23,5 +23,5 @@ module.exports = merge(common, {
__OPENMCT_ROOT_RELATIVE__: '""'
})
],
devtool: "source-map"
devtool: "eval-source-map"
});

View File

@@ -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. Unit Test specs should reside alongside the source code they test, not in a separate directory.
1. 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,6 +222,44 @@ 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 youre 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:
@@ -263,7 +301,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. Complex workarounds exist.
* _Critical_: Significant loss of functionality or impairment of use. Display of telemetry data is not affected though.
* _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
@@ -272,4 +310,22 @@ 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.```

View File

@@ -1,50 +0,0 @@
# 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 youre 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)

View File

@@ -15,14 +15,14 @@ coverage:
flags:
unit:
carryforward: false
e2e-stable:
carryforward: false
carryforward: true
e2e-ci:
carryforward: true
e2e-full:
carryforward: true
comment:
layout: "diff,flags,files,footer"
layout: "reach,diff,flags,files,footer"
behavior: default
require_changes: false
show_carryforward_flags: true
show_carryforward_flags: true

View File

@@ -200,7 +200,6 @@ 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
@@ -352,10 +351,6 @@ When looking at the reports run in CI, you'll leverage this same HTML Report whi
### 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```
@@ -366,6 +361,10 @@ 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

View File

@@ -74,6 +74,7 @@ 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")');
@@ -139,7 +140,6 @@ 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,7 +272,6 @@ 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()

View File

@@ -170,6 +170,5 @@ exports.test = base.test.extend({
}
}
});
exports.expect = expect;
exports.waitForAnimations = waitForAnimations;

View File

@@ -58,14 +58,8 @@ async function navigateToFaultManagementWithoutExample(page) {
async function navigateToFaultItemInTree(page) {
await page.goto('./', { waitUntil: 'networkidle' });
const faultManagementTreeItem = page.getByRole('tree', {
name: "Main Tree"
}).getByRole('treeitem', {
name: "Fault Management"
});
// Navigate to "Fault Management" from the tree
await faultManagementTreeItem.click();
// Click text=Fault Management
await page.click('text=Fault Management'); // this verifies the plugin has been added
}
/**
@@ -147,7 +141,8 @@ async function clearSearch(page) {
* @param {import('@playwright/test').Page} page
*/
async function selectFaultItem(page, rowNumber) {
await page.locator(`.c-fault-mgmt-item > input >> nth=${rowNumber - 1}`).check();
// 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
}
/**

View File

@@ -28,7 +28,7 @@ const NOTEBOOK_DROP_AREA = '.c-notebook__drag-area';
* @param {import('@playwright/test').Page} page
*/
async function enterTextEntry(page, text) {
// Click the 'Add Notebook Entry' area
// Click .c-notebook__drag-area
await page.locator(NOTEBOOK_DROP_AREA).click();
// enter text
@@ -58,7 +58,6 @@ 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();
}

View File

@@ -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 max-failures=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 maxFailures = 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,8 +74,7 @@ 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'],
['@deploysentinel/playwright']
['github']
]
};

View File

@@ -150,17 +150,3 @@ 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;
};

View File

@@ -25,7 +25,7 @@ const { createDomainObjectWithDefaults, createNotification, expandEntireTree } =
test.describe('AppActions', () => {
test('createDomainObjectsWithDefaults', async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
const e2eFolder = await createDomainObjectWithDefaults(page, {
type: 'Folder',
@@ -86,7 +86,7 @@ test.describe('AppActions', () => {
});
});
test("createNotification", async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
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: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
const rootFolder = await createDomainObjectWithDefaults(page, {
type: 'Folder'

View File

@@ -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: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
//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: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
//Verify that ../fixtures.js detects console log errors
await Promise.all([

View File

@@ -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: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
// We provide some helper functions in appActions like `createDomainObjectWithDefaults()`.
// This example will create a Timer object with default properties, under the root folder:

View File

@@ -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: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
const overlayPlot = await createDomainObjectWithDefaults(page, { type: 'Overlay Plot' });
// click create button

View File

@@ -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: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
});
});

View File

@@ -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: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
// 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: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
// Click About button
await page.click('.l-shell__app-logo');

View File

@@ -100,7 +100,7 @@ test.describe("CouchDB initialization with mocked responses @couchdb", () => {
&& req.method() === 'GET');
// Go to baseURL.
await page.goto('./', { waitUntil: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
// Wait for both requests to resolve.
await Promise.all([

View File

@@ -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: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
//Create a name for the object
const newObjectName = 'Test Event Generator';

View File

@@ -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: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
//Click the Create button
await page.click('button:has-text("Create")');

View File

@@ -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: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
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: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
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: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
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: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
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: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
// Create a new 'Clock' object with default settings
const clock = await createDomainObjectWithDefaults(page, {
@@ -166,13 +166,12 @@ test.describe('Persistence operations @couchdb', () => {
timeout: 1000
}).toEqual(1);
});
test('Can create an object after a conflict error @couchdb @2p', async ({ page, openmctConfig }) => {
test('Can create an object after a conflict error @couchdb @2p', async ({ page }) => {
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
@@ -181,10 +180,6 @@ 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")'),

View File

@@ -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: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
await page.locator('text=Persistence Testing').first().click({
button: 'right'

View File

@@ -35,7 +35,7 @@ test.describe('Notifications List', () => {
});
// Go to baseURL
await page.goto('./', { waitUntil: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
// 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: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
// Create a new Display Layout object
await createDomainObjectWithDefaults(page, { type: 'Display Layout' });

View File

@@ -29,7 +29,7 @@ const { getPreciseDuration } = require('../../../../src/utils/duration');
test.describe("Gantt Chart", () => {
let ganttChart;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
ganttChart = await createDomainObjectWithDefaults(page, {
type: 'Gantt Chart'
});

View File

@@ -27,7 +27,7 @@ const { assertPlanActivities } = require('../../../helper/planningUtils');
test.describe("Plan", () => {
let plan;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
plan = await createPlanFromJSON(page, {
json: testPlan1
});

View File

@@ -80,7 +80,7 @@ test.describe("Time Strip", () => {
const activityBounds = page.locator('.activity-bounds');
// Goto baseURL
await page.goto('./', { waitUntil: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
const timestrip = await test.step("Create a Time Strip", async () => {
const createdTimeStrip = await createDomainObjectWithDefaults(page, { type: 'Time Strip' });

View File

@@ -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: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
//Click the Create button
await page.click('button:has-text("Create")');

View File

@@ -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: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
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: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
//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: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
});
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: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
//Click the Create button
await page.click('button:has-text("Create")');

View File

@@ -27,7 +27,7 @@ test.describe('Display Layout', () => {
/** @type {import('../../../../appActions').CreatedObjectInfo} */
let sineWaveObject;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
await setRealTimeMode(page);
// Create Sine Wave Generator

View File

@@ -22,7 +22,6 @@
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 }) => {
@@ -39,7 +38,6 @@ 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();
@@ -54,7 +52,6 @@ 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();

View File

@@ -27,7 +27,7 @@ test.describe('Flexible Layout', () => {
let sineWaveObject;
let clockObject;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
// Create Sine Wave Generator
sineWaveObject = await createDomainObjectWithDefaults(page, {

View File

@@ -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: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
});
test('Can add and remove telemetry sources @unstable', async ({ page }) => {

View File

@@ -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: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
// 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: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
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: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
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: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
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: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
timeStripObject = await createDomainObjectWithDefaults(page, {
type: 'Time Strip'
});

View File

@@ -25,7 +25,7 @@ const { createDomainObjectWithDefaults, setStartOffset, setFixedTimeMode, setRea
test.describe('Testing LAD table configuration', () => {
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
// 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: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
await setRealTimeMode(page);
// Create Sine Wave Generator

View File

@@ -24,7 +24,7 @@
This test suite is dedicated to tests which verify the basic operations surrounding Notebooks.
*/
const { test, expect, streamToString } = require('../../../../pluginFixtures');
const { test, expect } = 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: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
// 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: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
// Create Notebook
await createDomainObjectWithDefaults(page, {
@@ -198,36 +198,6 @@ 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 }) => {});
@@ -243,24 +213,16 @@ 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: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
notebookObject = await createDomainObjectWithDefaults(page, {
type: NOTEBOOK_NAME
});
});
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.fixme('When a new entry is created, it should be focused', async ({ page }) => {});
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
const overlayPlot = await createDomainObjectWithDefaults(page, {
await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot'
});
@@ -270,17 +232,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=/${overlayPlot.name}/]`, '.c-notebook__drag-area');
await page.dragAndDrop('role=treeitem[name=/Dropped Overlay Plot/]', '.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(overlayPlot.name);
expect(embedName).toBe('Dropped Overlay Plot');
});
test('When an object is dropped into a notebooks existing entry, it should be focused @unstable', async ({ page }) => {
// Create Overlay Plot
const overlayPlot = await createDomainObjectWithDefaults(page, {
await createDomainObjectWithDefaults(page, {
type: 'Overlay Plot'
});
@@ -291,35 +253,17 @@ 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=/${overlayPlot.name}/]`, 'text=Entry to drop into');
await page.dragAndDrop('role=treeitem[name=/Dropped Overlay Plot/]', '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(overlayPlot.name);
expect(embedName).toBe('Dropped Overlay Plot');
});
test.fixme('new entries persist through navigation events without save', 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.fixme('previous and new entries can be deleted', async ({ page }) => {});
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';

View File

@@ -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: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
// Create Notebook
// const notebook = await createDomainObjectWithDefaults(page, {

View File

@@ -26,49 +26,46 @@ 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: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
// Create Notebook
testNotebook = await createDomainObjectWithDefaults(page, {type: 'Notebook' });
await page.goto(testNotebook.url, { waitUntil: 'networkidle'});
testNotebook = await createDomainObjectWithDefaults(page, {
type: 'Notebook',
name: "TestNotebook"
});
});
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 notebookElementsRequests = [];
page.on('request', (request) => notebookElementsRequests.push(request));
let addingNotebookElementsRequests = [];
page.on('request', (request) => addingNotebookElementsRequests.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"]')
page.click('[aria-label="Add Page"]'),
// Ensures that there are no other network requests
page.waitForLoadState('networkidle')
]);
// 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(notebookElementsRequests.length).toBe(2);
expect(addingNotebookElementsRequests.length).toBe(2);
// Assert on request object
expect(notebookUrlRequest.postDataJSON().metadata.name).toBe(testNotebook.name);
expect(notebookUrlRequest.postDataJSON().metadata.name).toBe('TestNotebook');
expect(notebookUrlRequest.postDataJSON().model.persisted).toBeGreaterThanOrEqual(notebookUrlRequest.postDataJSON().model.modified);
expect(allDocsRequest.postDataJSON().keys).toContain(testNotebook.uuid);
@@ -76,10 +73,13 @@ 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
notebookElementsRequests = [];
await nbUtils.enterTextEntry(page, 'First Entry');
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');
await page.waitForLoadState('networkidle');
expect(notebookElementsRequests.length).toBeLessThanOrEqual(2);
expect(addingNotebookElementsRequests.length).toBeLessThanOrEqual(2);
// Add some tags
// Network Requests are for each tag creation are:
@@ -95,17 +95,32 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => {
// 10) Entry is timestamped
// 11) The shared worker event from 👆 POST request
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=Driving').click();
await page.waitForSelector('[aria-label="Tag"]:has-text("Driving")');
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=Drilling').click();
await page.waitForSelector('[aria-label="Tag"]:has-text("Drilling")');
page.waitForLoadState('networkidle');
expect(filterNonFetchRequests(addingNotebookElementsRequests).length).toBeLessThanOrEqual(11);
notebookElementsRequests = [];
await addTagAndAwaitNetwork(page, 'Science');
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);
// Delete all the tags
// Network requests are:
@@ -114,25 +129,58 @@ 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
notebookElementsRequests = [];
await removeTagAndAwaitNetwork(page, 'Driving');
await removeTagAndAwaitNetwork(page, 'Drilling');
await removeTagAndAwaitNetwork(page, 'Science');
expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(12);
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);
// Add two more pages
await page.click('[aria-label="Add Page"]');
await page.click('[aria-label="Add Page"]');
// Add three entries
await nbUtils.enterTextEntry(page, 'First Entry');
await nbUtils.enterTextEntry(page, 'Second Entry');
await nbUtils.enterTextEntry(page, 'Third Entry');
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');
// Add three tags
await addTagAndAwaitNetwork(page, 'Science');
await addTagAndAwaitNetwork(page, 'Drilling');
await addTagAndAwaitNetwork(page, 'Driving');
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');
// Add a fourth entry
// Network requests are:
@@ -140,11 +188,14 @@ 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
notebookElementsRequests = [];
await nbUtils.enterTextEntry(page, 'Fourth Entry');
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');
page.waitForLoadState('networkidle');
expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(4);
expect(filterNonFetchRequests(addingNotebookElementsRequests).length).toBeLessThanOrEqual(4);
// Add a fifth entry
// Network requests are:
@@ -152,22 +203,28 @@ 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
notebookElementsRequests = [];
await nbUtils.enterTextEntry(page, 'Fifth Entry');
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');
page.waitForLoadState('networkidle');
expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(4);
expect(filterNonFetchRequests(addingNotebookElementsRequests).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
notebookElementsRequests = [];
await nbUtils.enterTextEntry(page, 'Sixth Entry');
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');
page.waitForLoadState('networkidle');
expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(4);
expect(filterNonFetchRequests(addingNotebookElementsRequests).length).toBeLessThanOrEqual(4);
});
test('Search tests', async ({ page }) => {
@@ -176,21 +233,35 @@ test.describe('Notebook Tests with CouchDB @couchdb', () => {
description: 'https://github.com/akhenry/openmct-yamcs/issues/69'
});
await page.getByText('Annotations').click();
await nbUtils.enterTextEntry(page, 'First Entry');
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');
// Add three tags
await addTagAndAwaitNetwork(page, 'Science');
await addTagAndAwaitNetwork(page, 'Drilling');
await addTagAndAwaitNetwork(page, 'Driving');
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 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();
@@ -204,40 +275,3 @@ 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');
}

View File

@@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
const { test, expect, streamToString } = require('../../../../pluginFixtures');
const { test, expect } = require('../../../../pluginFixtures');
const { openObjectTreeContextMenu, createDomainObjectWithDefaults } = require('../../../../appActions');
const path = require('path');
const nbUtils = require('../../../../helper/notebookUtils');
@@ -169,40 +169,13 @@ 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: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
return createDomainObjectWithDefaults(page, { type: CUSTOM_NAME });
}

View File

@@ -21,7 +21,7 @@
*****************************************************************************/
/*
This test suite is dedicated to tests which verify notebook tag functionality.
This test suite is dedicated to tests which verify form functionality.
*/
const { test, expect } = require('../../../../pluginFixtures');
@@ -34,6 +34,9 @@ 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++) {
@@ -78,13 +81,12 @@ 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();
@@ -108,24 +110,12 @@ 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"
@@ -214,7 +204,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: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Unnamed');
await expect(page.locator('text=No results found')).toBeVisible();
@@ -225,13 +215,37 @@ test.describe('Tagging in Notebooks @addInit', () => {
});
test('Tags persist across reload', async ({ page }) => {
//Go to baseURL
await page.goto('./', { waitUntil: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
const clock = await createDomainObjectWithDefaults(page, { type: 'Clock' });
const ITERATIONS = 4;
const notebook = await createNotebookEntryAndTags(page, ITERATIONS);
await page.goto(notebook.url);
// 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");
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();
for (let iteration = 0; iteration < ITERATIONS; iteration++) {
const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`;
await expect(page.locator(entryLocator)).toContainText("Science");
@@ -239,9 +253,14 @@ test.describe('Tagging in Notebooks @addInit', () => {
}
//Reload Page
await page.reload({ waitUntil: 'domcontentloaded' });
await Promise.all([
page.reload(),
page.waitForLoadState('networkidle')
]);
// Click Notebook
await page.click(`text="${notebook.name}"`);
// 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");
@@ -251,6 +270,9 @@ 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

View File

@@ -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: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
});
// verify that operator status is visible

View File

@@ -40,7 +40,7 @@ test.describe('Autoscale', () => {
//This is necessary due to the size of the test suite.
test.slow();
await page.goto('./', { waitUntil: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
await setTimeRange(page);
@@ -73,7 +73,6 @@ 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' });
@@ -173,7 +172,7 @@ async function createSinewaveOverlayPlot(page, myItemsFolderName) {
*/
async function turnOffAutoscale(page) {
// uncheck autoscale
await page.getByRole('checkbox', { name: 'Auto scale' }).uncheck();
await page.getByRole('listitem').filter({ hasText: 'Auto scale' }).getByRole('checkbox').uncheck();
}
/**
@@ -183,9 +182,14 @@ async function turnOffAutoscale(page) {
*/
async function setUserDefinedMinAndMax(page, min, max) {
// set minimum value
await page.getByRole('spinbutton').first().fill(min);
const minRangeInput = page.getByRole('listitem').filter({ hasText: 'Minimum Value' }).locator('input[type="number"]');
await minRangeInput.click();
await minRangeInput.fill(min);
// set maximum value
await page.getByRole('spinbutton').nth(1).fill(max);
const maxRangeInput = page.getByRole('listitem').filter({ hasText: 'Maximum Value' }).locator('input[type="number"]');
await maxRangeInput.click();
await maxRangeInput.fill(max);
}
/**

View File

@@ -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: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
// 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 = page.locator('.gl-plot-y-tick-label');
const yTicks = await 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 = page.locator('.gl-plot-y-tick-label');
const yTicks = await 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,24 +180,27 @@ async function testLogTicks(page) {
*/
async function enableEditMode(page) {
// turn on edit mode
await page.getByRole('button', { name: 'Edit' }).click();
await expect(page.getByRole('button', { name: 'Save' })).toBeVisible();
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();
}
/**
* @param {import('@playwright/test').Page} page
*/
async function enableLogMode(page) {
await expect(page.getByRole('checkbox', { name: 'Log mode' })).not.toBeChecked();
await page.getByRole('checkbox', { name: 'Log mode' }).check();
// 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();
}
/**
* @param {import('@playwright/test').Page} page
*/
async function disableLogMode(page) {
await expect(page.getByRole('checkbox', { name: 'Log mode' })).toBeChecked();
await page.getByRole('checkbox', { name: 'Log mode' }).uncheck();
// 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();
}
/**

View File

@@ -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: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
// create stacked plot
await page.locator('button.c-create-button').click();

View File

@@ -30,7 +30,7 @@ const { createDomainObjectWithDefaults, selectInspectorTab } = require('../../..
test.describe('Overlay Plot', () => {
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
});
test('Plot legend color is in sync with plot series color', async ({ page }) => {
@@ -214,17 +214,12 @@ test.describe('Overlay Plot', () => {
});
await page.goto(overlayPlot.url);
// Wait for plot series data to load and be drawn
await expect(page.locator('.js-series-data-loaded')).toBeVisible();
await page.click('button[title="Edit"]');
await selectInspectorTab(page, 'Elements');
await page.locator(`#inspector-elements-tree >> text=${swgA.name}`).click();
// Wait for "View Large" plot series data to load and be drawn
await expect(page.locator('.c-overlay .js-series-data-loaded')).toBeVisible();
const plotPixelSize = await getCanvasPixelsWithData(page);
expect(plotPixelSize).toBeGreaterThan(0);
});

View File

@@ -33,7 +33,7 @@ test.describe('Plot Integrity Testing @unstable', () => {
test.beforeEach(async ({ page }) => {
//Open a browser, navigate to the main page, and wait until all networkevents to resolve
await page.goto('./', { waitUntil: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
sineWaveGeneratorObject = await createDomainObjectWithDefaults(page, { type: 'Sine Wave Generator' });
});

View File

@@ -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: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
// Create the Scatter Plot
scatterPlot = await createDomainObjectWithDefaults(page, { type: 'Scatter Plot' });

View File

@@ -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: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
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('heading', { name: "Y Axis" })).toBeVisible();
await expect(page.getByRole('list', { name: "Y Axis Properties" }).locator("h2")).toContainText("Y Axis");
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('heading', { name: 'Y Axis' })).toBeVisible();
await expect(page.getByRole('list', { name: "Y Axis Properties" }).locator("h2")).toContainText("Y Axis");
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('heading', { name: 'Y Axis' })).toBeVisible();
await expect(page.getByRole('list', { name: "Y Axis Properties" }).locator("h2")).toContainText("Y Axis");
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('heading', { name: 'Y Axis' })).toBeVisible();
await expect(page.getByRole('list', { name: "Y Axis Properties" }).locator("h2")).toContainText("Y Axis");
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('heading', { name: 'Y Axis' })).toBeVisible();
await expect(page.getByRole('list', { name: "Y Axis Properties" }).locator("h2")).toContainText("Y Axis");
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('heading', { name: 'Y Axis' })).toBeVisible();
await expect(page.getByRole('list', { name: "Y Axis Properties" }).locator("h2")).toContainText("Y Axis");
await expect(page.locator('[aria-label="Plot Series Properties"] .c-object-label')).toContainText(swgC.name);
});
});

View File

@@ -157,7 +157,7 @@ test.describe('Plot Tagging', () => {
}
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
});
test('Tags work with Overlay Plots', async ({ page }) => {

View File

@@ -30,7 +30,7 @@ test.describe('Telemetry Table', () => {
description: 'https://github.com/nasa/openmct/issues/5113'
});
await page.goto('./', { waitUntil: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
const table = await createDomainObjectWithDefaults(page, { type: 'Telemetry Table' });
await createDomainObjectWithDefaults(page, {

View File

@@ -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: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
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: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
// 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: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
// Switch to real-time mode
await setRealTimeMode(page);

View File

@@ -26,7 +26,7 @@ const { openObjectTreeContextMenu, createDomainObjectWithDefaults } = require('.
test.describe('Timer', () => {
let timer;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
timer = await createDomainObjectWithDefaults(page, { type: 'timer' });
});

View File

@@ -32,7 +32,7 @@ test.describe('Recent Objects', () => {
/** @type {import('@playwright/test').Locator} */
let folderA;
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
// 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
await assertInitialRecentObjectsListState();
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 }) => {
await assertInitialRecentObjectsListState();
assertInitialRecentObjectsListState();
await page.reload();
await assertInitialRecentObjectsListState();
assertInitialRecentObjectsListState();
});
test("Displays objects and aliases uniquely", async ({ page }) => {
const mainTree = page.getByRole('tree', { name: 'Main Tree'});
@@ -252,57 +252,13 @@ 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() {
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()
]);
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();
}
});

View File

@@ -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: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
//Click the Create button
await page.click('button:has-text("Create")');

View File

@@ -28,7 +28,7 @@ const {
test.describe('Main Tree', () => {
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'domcontentloaded' });
await page.goto('./', { waitUntil: 'networkidle' });
});
test('Creating a child object within a folder and immediately opening it shows the created object in the tree @couchdb', async ({ page }) => {

View File

@@ -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, theme }) => {
test('Create a clock, click on \'Save successful\' banner and dismiss it', async ({ page }) => {
// 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 - ${theme}`);
await percySnapshot(page, 'Notification banner');
// 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"

View File

@@ -24,9 +24,7 @@ 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 = '.c-object-view';
const examplePlanLarge = require('../../test-data/examplePlans/ExamplePlan_Large.json');
test.describe('Visual - Planning', () => {
test.beforeEach(async ({ page }) => {
@@ -34,25 +32,21 @@ test.describe('Visual - Planning', () => {
});
test('Plan View', async ({ page, theme }) => {
const plan = await createPlanFromJSON(page, {
json: examplePlanSmall
json: examplePlanLarge
});
await setBoundsToSpanAllActivities(page, examplePlanSmall, plan.url);
await percySnapshot(page, `Plan View (theme: ${theme})`, {
scope: snapshotScope
});
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: examplePlanSmall,
json: examplePlanLarge,
parent: ganttChart.uuid
});
await setBoundsToSpanAllActivities(page, examplePlanSmall, ganttChart.url);
await percySnapshot(page, `Gantt Chart View (theme: ${theme})`, {
scope: snapshotScope
});
await setBoundsToSpanAllActivities(page, examplePlanLarge, ganttChart.url);
await percySnapshot(page, `Gantt Chart View (theme: ${theme})`);
});
});

View File

@@ -33,8 +33,6 @@ export default function (staticFaults = false) {
return Promise.resolve(faultsData);
},
subscribe(domainObject, callback) {
callback({ type: 'global-alarm-status' });
return () => {};
},
supportsRequest(domainObject) {

View File

@@ -200,8 +200,6 @@
openmct.install(openmct.plugins.Timelist());
openmct.install(openmct.plugins.BarChart());
openmct.install(openmct.plugins.ScatterPlot());
document.addEventListener('DOMContentLoaded', function () {
openmct.start();
});
openmct.start();
</script>
</html>

View File

@@ -1,14 +1,13 @@
{
"name": "openmct",
"version": "2.2.3-SNAPSHOT",
"version": "2.2.1-SNAPSHOT",
"description": "The Open MCT core platform",
"devDependencies": {
"@babel/eslint-parser": "7.18.9",
"@braintree/sanitize-url": "6.0.2",
"@deploysentinel/playwright": "0.3.4",
"@percy/cli": "1.23.0",
"@percy/cli": "1.17.0",
"@percy/playwright": "1.0.4",
"@playwright/test": "1.32.3",
"@playwright/test": "1.29.0",
"@types/eventemitter3": "1.2.0",
"@types/jasmine": "4.3.1",
"@types/lodash": "4.14.192",
@@ -21,10 +20,10 @@
"d3-axis": "3.0.0",
"d3-scale": "3.3.0",
"d3-selection": "3.0.0",
"eslint": "8.37.0",
"eslint-plugin-compat": "4.1.4",
"eslint": "8.36.0",
"eslint-plugin-compat": "4.1.1",
"eslint-plugin-playwright": "0.12.0",
"eslint-plugin-vue": "9.11.0",
"eslint-plugin-vue": "9.10.0",
"eslint-plugin-you-dont-need-lodash-underscore": "6.12.0",
"eventemitter3": "1.2.0",
"file-saver": "2.0.5",
@@ -39,10 +38,10 @@
"karma-coverage-istanbul-reporter": "3.0.3",
"karma-jasmine": "5.1.0",
"karma-junit-reporter": "2.0.1",
"karma-sourcemap-loader": "0.4.0",
"karma-sourcemap-loader": "0.3.8",
"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",
@@ -51,29 +50,29 @@
"moment-timezone": "0.5.41",
"nyc": "15.1.0",
"painterro": "1.2.78",
"playwright-core": "1.32.3",
"playwright-core": "1.29.0",
"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.62.0",
"sass-loader": "13.2.2",
"sass": "1.59.3",
"sass-loader": "13.2.1",
"sinon": "15.0.1",
"style-loader": "3.3.2",
"typescript": "5.0.4",
"style-loader": "^3.3.1",
"typescript": "4.9.5",
"uuid": "9.0.0",
"vue": "2.6.14",
"vue-eslint-parser": "9.1.0",
"vue-loader": "15.9.8",
"vue-template-compiler": "2.6.14",
"webpack": "5.79.0",
"webpack": "5.76.3",
"webpack-cli": "5.0.0",
"webpack-dev-server": "4.13.3",
"webpack-dev-server": "4.11.1",
"webpack-merge": "5.8.0"
},
"scripts": {
"clean": "rm -rf ./dist ./node_modules ./package-lock.json ./coverage ./html-test-results ./test-results ./.nyc_output ",
"clean": "rm -rf ./dist ./node_modules ./package-lock.json",
"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",
@@ -87,7 +86,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 --workers=1",
"test:e2e:couchdb": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @couchdb",
"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",
@@ -108,15 +107,14 @@
"url": "https://github.com/nasa/openmct.git"
},
"engines": {
"node": ">=16.19.1"
"node": ">=14.19.1"
},
"browserslist": [
"Firefox ESR",
"not IE 11",
"last 2 Chrome versions",
"unreleased Chrome versions",
"ios_saf >= 16",
"Safari >= 16"
"ios_saf > 15"
],
"author": "",
"license": "Apache-2.0"

View File

@@ -31,7 +31,7 @@ class ActionsAPI extends EventEmitter {
this._actionCollections = new WeakMap();
this._openmct = openmct;
this._groupOrder = ['windowing', 'undefined', 'view', 'action', 'export', 'import'];
this._groupOrder = ['windowing', 'undefined', 'view', 'action', 'json'];
this.register = this.register.bind(this);
this.getActionsCollection = this.getActionsCollection.bind(this);

View File

@@ -21,31 +21,18 @@
*****************************************************************************/
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();
@@ -54,11 +41,6 @@ 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();
@@ -67,55 +49,58 @@ 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} 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
/** @typedef {object} Fault
* @property {string} type
* @property {Fault} fault
* @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": ""
* }
* }
* }
*/

View File

@@ -239,9 +239,15 @@ export default class ObjectAPI {
return domainObject;
}).catch((error) => {
console.warn(`Failed to retrieve ${keystring}:`, error);
let result;
delete this.cache[keystring];
const result = this.applyGetInterceptors(identifier);
// suppress abort errors
if (error.name !== 'AbortError') {
console.warn(`Failed to retrieve ${keystring}:`, error);
result = this.applyGetInterceptors(identifier);
}
return result;
});

View File

@@ -55,6 +55,13 @@ define([
*/
function parseKeyString(keyString) {
if (isIdentifier(keyString)) {
// TODO REMOVE FOR OMM-RELEASE-5.0
if (!keyString.namespace && keyString.key.includes(':')) {
console.warn(`smushed key: ${keyString.key}`);
return parseKeyString(keyString.key);
}
return keyString;
}

View File

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

View File

@@ -20,15 +20,13 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import TimeContext from "./TimeContext";
import { TIME_CONTEXT_EVENTS } from './constants';
import TimeContext, { TIME_CONTEXT_EVENTS } from "./TimeContext";
/**
* The IndependentTimeContext handles getting and setting time of the openmct application in general.
* Views will use the GlobalTimeContext unless they specify an alternate/independent time context here.
*/
class IndependentTimeContext extends TimeContext {
constructor(openmct, globalTimeContext, objectPath) {
super();
this.openmct = openmct;
@@ -48,7 +46,7 @@ class IndependentTimeContext extends TimeContext {
this.globalTimeContext.on('removeOwnContext', this.removeIndependentContext);
}
bounds() {
bounds(newBounds) {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.bounds(...arguments);
} else {
@@ -56,23 +54,7 @@ class IndependentTimeContext extends TimeContext {
}
}
getBounds() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.getBounds();
} else {
return super.getBounds();
}
}
setBounds() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.setBounds(...arguments);
} else {
return super.setBounds(...arguments);
}
}
tick() {
tick(timestamp) {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.tick(...arguments);
} else {
@@ -80,7 +62,7 @@ class IndependentTimeContext extends TimeContext {
}
}
clockOffsets() {
clockOffsets(offsets) {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.clockOffsets(...arguments);
} else {
@@ -88,27 +70,11 @@ class IndependentTimeContext extends TimeContext {
}
}
getClockOffsets() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.getClockOffsets();
} else {
return super.getClockOffsets();
}
}
setClockOffsets() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.setClockOffsets(...arguments);
} else {
return super.setClockOffsets(...arguments);
}
}
stopClock() {
if (this.upstreamTimeContext) {
// this.upstreamTimeContext.stopClock();
this.upstreamTimeContext.stopClock();
} else {
// super.stopClock();
super.stopClock();
}
}
@@ -120,16 +86,6 @@ class IndependentTimeContext extends TimeContext {
return this.globalTimeContext.timeSystem(...arguments);
}
/**
* Get the time system of the TimeAPI.
* @returns {TimeSystem} The currently applied time system
* @memberof module:openmct.TimeAPI#
* @method getTimeSystem
*/
getTimeSystem() {
return this.globalTimeContext.getTimeSystem();
}
/**
* Set the active clock. Tick source will be immediately subscribed to
* and ticking will begin. Offsets from 'now' must also be provided. A clock
@@ -190,121 +146,6 @@ class IndependentTimeContext extends TimeContext {
return this.activeClock;
}
/**
* Get the active clock.
* @return {Clock} the currently active clock;
*/
getClock() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.getClock();
}
return this.activeClock;
}
/**
* Set the active clock. Tick source will be immediately subscribed to
* and the currently ticking will begin.
* Offsets from 'now', if provided, will be used to set realtime mode offsets
*
* @param {Clock || string} keyOrClock The clock to activate, or its key
* @param {ClockOffsets} offsets on each tick these will be used to calculate
* the start and end bounds when in realtime mode.
* This maintains a sliding time window of a fixed width that automatically updates.
* @fires module:openmct.TimeAPI~clock
* @return {Clock} the currently active clock;
*/
setClock(keyOrClock, offsets) {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.setClock(...arguments);
}
let clock;
if (typeof keyOrClock === 'string') {
clock = this.globalTimeContext.clocks.get(keyOrClock);
if (clock === undefined) {
throw `Unknown clock ${keyOrClock}. Has it been registered with 'addClock'?`;
}
} else if (typeof keyOrClock === 'object') {
clock = keyOrClock;
if (!this.globalTimeContext.clocks.has(clock.key)) {
throw `Unknown clock ${keyOrClock.key}. Has it been registered with 'addClock'?`;
}
}
// this.setMode(REALTIME_MODE_KEY);
const previousClock = this.activeClock;
if (previousClock) {
previousClock.off('tick', this.tick);
}
this.activeClock = clock;
this.activeClock.on('tick', this.tick);
/**
* The active clock has changed.
* @event clock
* @memberof module:openmct.TimeAPI~
* @property {Clock} clock The newly activated clock, or undefined
* if the system is no longer following a clock source
*/
this.emit(TIME_CONTEXT_EVENTS.clockChanged, this.activeClock);
if (offsets !== undefined) {
this.setClockOffsets(offsets);
}
return this.activeClock;
}
/**
* Get the current mode.
* @return {Mode} the current mode;
*/
getMode() {
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.getMode();
}
return this.mode;
}
/**
* Set the mode to either fixed or realtime.
*
* @param {Mode} mode The mode to activate
* @fires module:openmct.TimeAPI~clock
* @return {Mode} the currently active mode;
*/
setMode(mode) {
if (!mode || mode === this.mode) {
return;
}
if (this.upstreamTimeContext) {
return this.upstreamTimeContext.setMode(...arguments);
}
this.mode = mode;
/**
* The active clock has changed. Clock can be unset by calling {@link stopClock}
* @event clock
* @memberof module:openmct.TimeAPI~
* @property {Clock} clock The newly activated clock, or undefined
* if the system is no longer following a clock source
*/
this.emit(TIME_CONTEXT_EVENTS.modeChanged, this.#copy(this.mode));
return this.mode;
}
#copy(object) {
return JSON.parse(JSON.stringify(object));
}
/**
* Causes this time context to follow another time context (either the global context, or another upstream time context)
* This allows views to have their own time context which points to the appropriate upstream context as necessary, achieving nesting.
@@ -312,7 +153,7 @@ class IndependentTimeContext extends TimeContext {
followTimeContext() {
this.stopFollowingTimeContext();
if (this.upstreamTimeContext) {
Object.values(TIME_CONTEXT_EVENTS).forEach((eventName) => {
TIME_CONTEXT_EVENTS.forEach((eventName) => {
const thisTimeContext = this;
this.upstreamTimeContext.on(eventName, passthrough);
this.unlisteners.push(() => {
@@ -346,7 +187,6 @@ class IndependentTimeContext extends TimeContext {
*/
refreshContext(viewKey) {
const key = this.openmct.objects.makeKeyString(this.objectPath[0].identifier);
if (viewKey && key === viewKey) {
return;
}
@@ -359,7 +199,6 @@ class IndependentTimeContext extends TimeContext {
// Emit bounds so that views that are changing context get the upstream bounds
this.emit('bounds', this.bounds());
this.emit('boundsChanged', this.getBounds());
}
hasOwnContext() {
@@ -397,7 +236,6 @@ class IndependentTimeContext extends TimeContext {
*/
removeIndependentContext(viewKey) {
const key = this.openmct.objects.makeKeyString(this.objectPath[0].identifier);
if (viewKey && key === viewKey) {
//this is necessary as the upstream context gets reassigned after this
this.stopFollowingTimeContext();
@@ -423,8 +261,7 @@ class IndependentTimeContext extends TimeContext {
this.followTimeContext();
// Emit bounds so that views that are changing context get the upstream bounds
this.emit('bounds', this.getBounds());
this.emit('boundsChanged', this.getBounds());
this.emit('bounds', this.bounds());
// now that the view's context is set, tell others to check theirs in case they were following this view's context.
this.globalTimeContext.emit('refreshContext', viewKey);
}

View File

@@ -22,7 +22,6 @@
import GlobalTimeContext from "./GlobalTimeContext";
import IndependentTimeContext from "@/api/time/IndependentTimeContext";
import {FIXED_MODE_KEY, REALTIME_MODE_KEY} from "@/api/time/constants";
/**
* The public API for setting and querying the temporal state of the
@@ -135,27 +134,14 @@ class TimeAPI extends GlobalTimeContext {
*/
addIndependentContext(key, value, clockKey) {
let timeContext = this.getIndependentContext(key);
let upstreamClock;
if (timeContext.upstreamTimeContext) {
upstreamClock = timeContext.upstreamTimeContext.getClock();
}
//stop following upstream time context since the view has it's own
timeContext.resetContext();
if (clockKey) {
timeContext.setMode(REALTIME_MODE_KEY);
timeContext.setClock(clockKey, value);
timeContext.clock(clockKey, value);
} else {
timeContext.setMode(FIXED_MODE_KEY);
//TODO: Should the clock be stopped here?
timeContext.stopClock();
//upstream clock was active, but now we don't have one
if (upstreamClock) {
timeContext.emit('clockChanged', timeContext.activeClock);
}
timeContext.setBounds(value);
timeContext.bounds(value);
}
// Notify any nested views to update, pass in the viewKey so that particular view can skip getting an upstream context
@@ -193,14 +179,12 @@ class TimeAPI extends GlobalTimeContext {
const viewKey = objectPath.length && this.openmct.objects.makeKeyString(objectPath[0].identifier);
if (!viewKey) {
// Return the global time contex
// Return the global time context
return this;
}
let viewTimeContext = this.getIndependentContext(viewKey);
if (!viewTimeContext) {
console.log('no view context for viewKey', viewKey, this.openmct.objects.getRelativePath(objectPath));
// If the context doesn't exist yet, create it.
viewTimeContext = new IndependentTimeContext(this.openmct, this, objectPath);
this.independentContexts.set(viewKey, viewTimeContext);
@@ -209,8 +193,6 @@ class TimeAPI extends GlobalTimeContext {
const currentPath = this.openmct.objects.getRelativePath(viewTimeContext.objectPath);
const newPath = this.openmct.objects.getRelativePath(objectPath);
console.log('view context exists, paths match?', !(currentPath !== newPath), currentPath, newPath, 'view key: ', viewKey);
if (currentPath !== newPath) {
// If the path has changed, update the context.
this.independentContexts.delete(viewKey);

View File

@@ -21,7 +21,13 @@
*****************************************************************************/
import EventEmitter from 'EventEmitter';
import { TIME_CONTEXT_EVENTS, MODES, REALTIME_MODE_KEY } from './constants';
export const TIME_CONTEXT_EVENTS = [
'bounds',
'clock',
'timeSystem',
'clockOffsets'
];
class TimeContext extends EventEmitter {
constructor() {
@@ -41,7 +47,6 @@ class TimeContext extends EventEmitter {
this.activeClock = undefined;
this.offsets = undefined;
this.mode = undefined;
this.tick = this.tick.bind(this);
}
@@ -56,8 +61,6 @@ class TimeContext extends EventEmitter {
* @method timeSystem
*/
timeSystem(timeSystemOrKey, bounds) {
this.#warnMethodDeprecated('"timeSystem"', '"getTimeSystem" and "setTimeSystem"');
if (arguments.length >= 1) {
if (arguments.length === 1 && !this.activeClock) {
throw new Error(
@@ -88,7 +91,7 @@ class TimeContext extends EventEmitter {
throw "Attempt to set invalid time system in Time API. Please provide a previously registered time system object or key";
}
this.system = this.#copy(timeSystem);
this.system = timeSystem;
/**
* The time system used by the time
@@ -99,7 +102,7 @@ class TimeContext extends EventEmitter {
* @property {TimeSystem} The value of the currently applied
* Time System
* */
this.emit('timeSystem', this.#copy(this.system));
this.emit('timeSystem', this.system);
if (bounds) {
this.bounds(bounds);
}
@@ -160,8 +163,6 @@ class TimeContext extends EventEmitter {
* @method bounds
*/
bounds(newBounds) {
this.#warnMethodDeprecated('"bounds"', '"getBounds" and "setBounds"');
if (arguments.length > 0) {
const validationResult = this.validateBounds(newBounds);
if (validationResult.valid !== true) {
@@ -169,7 +170,7 @@ class TimeContext extends EventEmitter {
}
//Create a copy to avoid direct mutation of conductor bounds
this.boundsVal = this.#copy(newBounds);
this.boundsVal = JSON.parse(JSON.stringify(newBounds));
/**
* The start time, end time, or both have been updated.
* @event bounds
@@ -182,7 +183,7 @@ class TimeContext extends EventEmitter {
}
//Return a copy to prevent direct mutation of time conductor bounds.
return this.#copy(this.boundsVal);
return JSON.parse(JSON.stringify(this.boundsVal));
}
/**
@@ -246,8 +247,6 @@ class TimeContext extends EventEmitter {
* @returns {ClockOffsets}
*/
clockOffsets(offsets) {
this.#warnMethodDeprecated('"clockOffsets"', '"getClockOffsets" and "setClockOffsets"');
if (arguments.length > 0) {
const validationResult = this.validateOffsets(offsets);
@@ -279,14 +278,11 @@ class TimeContext extends EventEmitter {
}
/**
* Stop following the currently active clock. This will
* Stop the currently active clock from ticking, and unset it. This will
* revert all views to showing a static time frame defined by the current
* bounds.
*/
stopClock() {
console.log('stop clock');
this.#warnMethodDeprecated('"stopClock"');
if (this.activeClock) {
this.clock(undefined, undefined);
}
@@ -305,8 +301,6 @@ class TimeContext extends EventEmitter {
* @return {Clock} the currently active clock;
*/
clock(keyOrClock, offsets) {
this.#warnMethodDeprecated('"clock"', '"getClock" and "setClock"');
if (arguments.length === 2) {
let clock;
@@ -360,300 +354,25 @@ class TimeContext extends EventEmitter {
return;
}
if (this.mode === REALTIME_MODE_KEY) {
const newBounds = {
start: timestamp + this.offsets.start,
end: timestamp + this.offsets.end
};
const newBounds = {
start: timestamp + this.offsets.start,
end: timestamp + this.offsets.end
};
this.boundsVal = newBounds;
// "bounds" will be deprecated in a future release
this.emit('bounds', this.boundsVal, true);
this.emit(TIME_CONTEXT_EVENTS.boundsChanged, this.boundsVal, true);
}
this.boundsVal = newBounds;
this.emit('bounds', this.boundsVal, true);
}
/**
* Get the time system of the TimeAPI.
* @returns {TimeSystem} The currently applied time system
* @memberof module:openmct.TimeAPI#
* @method getTimeSystem
*/
getTimeSystem() {
return this.system;
}
/**
* Set the time system of the TimeAPI.
* @param {TimeSystem | string} timeSystemOrKey
* @param {module:openmct.TimeAPI~TimeConductorBounds} bounds
* @fires module:openmct.TimeAPI~timeSystem
* @returns {TimeSystem} The currently applied time system
* @memberof module:openmct.TimeAPI#
* @method setTimeSystem
*/
setTimeSystem(timeSystemOrKey, bounds) {
if (!this.isRealTime() && !bounds) {
throw new Error(
'Must specify bounds when changing time system without an active clock.'
);
}
if (timeSystemOrKey === undefined) {
throw 'Please provide a time system';
}
let timeSystem;
if (typeof timeSystemOrKey === 'string') {
timeSystem = this.timeSystems.get(timeSystemOrKey);
if (timeSystem === undefined) {
throw `Unknown time system ${timeSystemOrKey}. Has it been registered with 'addTimeSystem'?`;
}
} else if (typeof timeSystemOrKey === 'object') {
timeSystem = timeSystemOrKey;
if (!this.timeSystems.has(timeSystem.key)) {
throw `Unknown time system ${timeSystemOrKey.key}. Has it been registered with 'addTimeSystem'?`;
}
} else {
throw 'Attempt to set invalid time system in Time API. Please provide a previously registered time system object or key';
}
this.system = this.#copy(timeSystem);
/**
* The time system used by the time
* conductor has changed. A change in Time System will always be
* followed by a bounds event specifying new query bounds.
*
* @event module:openmct.TimeAPI~timeSystem
* @property {TimeSystem} The value of the currently applied
* Time System
* */
this.emit(TIME_CONTEXT_EVENTS.timeSystemChanged, this.#copy(this.system));
if (bounds) {
this.setBounds(bounds);
}
return this.system;
}
/**
* Get the start and end time of the time conductor. Basic validation
* of bounds is performed.
* @returns {module:openmct.TimeAPI~TimeConductorBounds}
* @memberof module:openmct.TimeAPI#
* @method bounds
*/
getBounds() {
//Return a copy to prevent direct mutation of time conductor bounds.
return this.#copy(this.boundsVal);
}
/**
* Set the start and end time of the time conductor. Basic validation
* of bounds is performed.
*
* @param {module:openmct.TimeAPI~TimeConductorBounds} newBounds
* @throws {Error} Validation error
* @fires module:openmct.TimeAPI~bounds
* @returns {module:openmct.TimeAPI~TimeConductorBounds}
* @memberof module:openmct.TimeAPI#
* @method bounds
*/
setBounds(newBounds) {
const validationResult = this.validateBounds(newBounds);
if (validationResult.valid !== true) {
throw new Error(validationResult.message);
}
//Create a copy to avoid direct mutation of conductor bounds
this.boundsVal = this.#copy(newBounds);
/**
* The start time, end time, or both have been updated.
* @event bounds
* @memberof module:openmct.TimeAPI~
* @property {TimeConductorBounds} bounds The newly updated bounds
* @property {boolean} [tick] `true` if the bounds update was due to
* a "tick" event (i.e. was an automatic update), false otherwise.
*/
this.emit(TIME_CONTEXT_EVENTS.boundsChanged, this.boundsVal, false);
//Return a copy to prevent direct mutation of time conductor bounds.
return this.#copy(this.boundsVal);
}
/**
* Get the active clock.
* @return {Clock} the currently active clock;
*/
getClock() {
return this.activeClock;
}
/**
* Set the active clock. Tick source will be immediately subscribed to
* and the currently ticking will begin.
* Offsets from 'now', if provided, will be used to set realtime mode offsets
*
* @param {Clock || string} keyOrClock The clock to activate, or its key
* @param {ClockOffsets} offsets on each tick these will be used to calculate
* the start and end bounds when in realtime mode.
* This maintains a sliding time window of a fixed width that automatically updates.
* @fires module:openmct.TimeAPI~clock
* @return {Clock} the currently active clock;
*/
setClock(keyOrClock, offsets) {
let clock;
if (typeof keyOrClock === 'string') {
clock = this.clocks.get(keyOrClock);
if (clock === undefined) {
throw `Unknown clock ${keyOrClock}. Has it been registered with 'addClock'?`;
}
} else if (typeof keyOrClock === 'object') {
clock = keyOrClock;
if (!this.clocks.has(clock.key)) {
throw `Unknown clock ${keyOrClock.key}. Has it been registered with 'addClock'?`;
}
}
// this.setMode(REALTIME_MODE_KEY);
const previousClock = this.activeClock;
if (previousClock) {
previousClock.off('tick', this.tick);
}
this.activeClock = clock;
this.activeClock.on('tick', this.tick);
/**
* The active clock has changed.
* @event clock
* @memberof module:openmct.TimeAPI~
* @property {Clock} clock The newly activated clock, or undefined
* if the system is no longer following a clock source
*/
this.emit(TIME_CONTEXT_EVENTS.clockChanged, this.activeClock);
if (offsets !== undefined) {
this.setClockOffsets(offsets);
}
return this.activeClock;
}
/**
* Get the current mode.
* @return {Mode} the current mode;
*/
getMode() {
return this.mode;
}
/**
* Set the mode to either fixed or realtime.
*
* @param {Mode} mode The mode to activate
* @fires module:openmct.TimeAPI~clock
* @return {Mode} the currently active mode;
*/
setMode(mode) {
if (!mode || mode === this.mode) {
return;
}
this.mode = mode;
/**
* The active clock has changed. Clock can be unset by calling {@link stopClock}
* @event clock
* @memberof module:openmct.TimeAPI~
* @property {Clock} clock The newly activated clock, or undefined
* if the system is no longer following a clock source
*/
this.emit(TIME_CONTEXT_EVENTS.modeChanged, this.#copy(this.mode));
return this.mode;
}
/**
* Checks if this time context is in realtime mode or not.
* Checks if this time context is in real-time mode or not.
* @returns {boolean} true if this context is in real-time mode, false if not
*/
isRealTime() {
return this.mode === MODES.realtime;
}
/**
* Checks if this time context is in fixed mode or not.
* @returns {boolean} true if this context is in fixed mode, false if not
*/
isFixed() {
return this.mode === MODES.fixed;
}
/**
* Get the currently applied clock offsets.
* @returns {ClockOffsets}
*/
getClockOffsets() {
return this.offsets;
}
/**
* Set the currently applied clock offsets. If no parameter is provided,
* the current value will be returned. If provided, the new value will be
* used as the new clock offsets.
* @param {ClockOffsets} offsets
* @returns {ClockOffsets}
*/
setClockOffsets(offsets) {
const validationResult = this.validateOffsets(offsets);
if (validationResult.valid !== true) {
throw new Error(validationResult.message);
if (this.clock()) {
return true;
}
this.offsets = this.#copy(offsets);
const currentValue = this.activeClock.currentValue();
const newBounds = {
start: currentValue + offsets.start,
end: currentValue + offsets.end
};
this.setBounds(newBounds);
/**
* Event that is triggered when clock offsets change.
* @event clockOffsets
* @memberof module:openmct.TimeAPI~
* @property {ClockOffsets} clockOffsets The newly activated clock
* offsets.
*/
this.emit(TIME_CONTEXT_EVENTS.clockOffsetsChanged, this.#copy(offsets));
return this.offsets;
}
#warnMethodDeprecated(method, newMethod) {
let message = `[DEPRECATION WARNING]: The ${method} API method is deprecated and will be removed in a future version of Open MCT.`;
if (newMethod) {
message += ` Please use the ${newMethod} API method(s) instead.`;
}
// TODO: add docs and point to them in warning.
// For more information and migration instructions, visit [link to documentation or migration guide].
console.warn(message);
}
#copy(object) {
return JSON.parse(JSON.stringify(object));
return false;
}
}

View File

@@ -1,22 +0,0 @@
export const TIME_CONTEXT_EVENTS = {
//old API events - to be deprecated
bounds: 'bounds',
clock: 'clock',
timeSystem: 'timeSystem',
clockOffsets: 'clockOffsets',
//new API events
tick: 'tick',
modeChanged: 'modeChanged',
boundsChanged: 'boundsChanged',
clockChanged: 'clockChanged',
timeSystemChanged: 'timeSystemChanged',
clockOffsetsChanged: 'clockOffsetsChanged'
};
export const REALTIME_MODE_KEY = 'realtime';
export const FIXED_MODE_KEY = 'fixed';
export const MODES = {
[FIXED_MODE_KEY]: FIXED_MODE_KEY,
[REALTIME_MODE_KEY]: REALTIME_MODE_KEY
};

View File

@@ -20,15 +20,14 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
const TIME_EVENTS = ['timeSystemChanged', 'modeChanged', 'clockChanged', 'clockOffsetsChanged'];
const TIME_EVENTS = ['timeSystem', 'clock', 'clockOffsets'];
const SEARCH_MODE = 'tc.mode';
const SEARCH_TIME_SYSTEM = 'tc.timeSystem';
const SEARCH_START_BOUND = 'tc.startBound';
const SEARCH_END_BOUND = 'tc.endBound';
const SEARCH_START_DELTA = 'tc.startDelta';
const SEARCH_END_DELTA = 'tc.endDelta';
import { FIXED_MODE_KEY } from "../../api/time/constants";
const MODE_FIXED = 'fixed';
export default class URLTimeSettingsSynchronizer {
constructor(openmct) {
@@ -68,7 +67,7 @@ export default class URLTimeSettingsSynchronizer {
}
updateTimeSettings() {
const timeParameters = this.parseParametersFromUrl();
let timeParameters = this.parseParametersFromUrl();
if (this.areTimeParametersValid(timeParameters)) {
this.setTimeApiFromUrl(timeParameters);
@@ -79,18 +78,21 @@ export default class URLTimeSettingsSynchronizer {
}
parseParametersFromUrl() {
const searchParams = this.openmct.router.getAllSearchParams();
const mode = searchParams.get(SEARCH_MODE);
const timeSystem = searchParams.get(SEARCH_TIME_SYSTEM);
const startBound = parseInt(searchParams.get(SEARCH_START_BOUND), 10);
const endBound = parseInt(searchParams.get(SEARCH_END_BOUND), 10);
const bounds = {
let searchParams = this.openmct.router.getAllSearchParams();
let mode = searchParams.get(SEARCH_MODE);
let timeSystem = searchParams.get(SEARCH_TIME_SYSTEM);
let startBound = parseInt(searchParams.get(SEARCH_START_BOUND), 10);
let endBound = parseInt(searchParams.get(SEARCH_END_BOUND), 10);
let bounds = {
start: startBound,
end: endBound
};
const startOffset = parseInt(searchParams.get(SEARCH_START_DELTA), 10);
const endOffset = parseInt(searchParams.get(SEARCH_END_DELTA), 10);
const clockOffsets = {
let startOffset = parseInt(searchParams.get(SEARCH_START_DELTA), 10);
let endOffset = parseInt(searchParams.get(SEARCH_END_DELTA), 10);
let clockOffsets = {
start: 0 - startOffset,
end: endOffset
};
@@ -104,30 +106,30 @@ export default class URLTimeSettingsSynchronizer {
}
setTimeApiFromUrl(timeParameters) {
const timeSystem = this.openmct.time.getTimeSystem();
if (timeParameters.mode === FIXED_MODE_KEY) {
// should update timesystem
if (timeSystem.key !== timeParameters.timeSystem) {
this.openmct.time.setTimeSystem(timeParameters.timeSystem, timeParameters.bounds);
} else if (!this.areStartAndEndEqual(this.openmct.time.getBounds(), timeParameters.bounds)) {
this.openmct.time.setBounds(timeParameters.bounds);
if (timeParameters.mode === 'fixed') {
if (this.openmct.time.timeSystem().key !== timeParameters.timeSystem) {
this.openmct.time.timeSystem(
timeParameters.timeSystem,
timeParameters.bounds
);
} else if (!this.areStartAndEndEqual(this.openmct.time.bounds(), timeParameters.bounds)) {
this.openmct.time.bounds(timeParameters.bounds);
}
this.openmct.time.setMode('fixed');
if (this.openmct.time.clock()) {
this.openmct.time.stopClock();
}
} else {
const clock = this.openmct.time.getClock();
console.log('setting as realtime', clock?.key, timeParameters.mode);
if (clock?.key !== timeParameters.mode) {
this.openmct.time.setClock(timeParameters.mode, timeParameters.clockOffsets);
} else if (!this.areStartAndEndEqual(this.openmct.time.getClockOffsets(), timeParameters.clockOffsets)) {
this.openmct.time.setClockOffsets(timeParameters.clockOffsets);
if (!this.openmct.time.clock()
|| this.openmct.time.clock().key !== timeParameters.mode) {
this.openmct.time.clock(timeParameters.mode, timeParameters.clockOffsets);
} else if (!this.areStartAndEndEqual(this.openmct.time.clockOffsets(), timeParameters.clockOffsets)) {
this.openmct.time.clockOffsets(timeParameters.clockOffsets);
}
this.openmct.time.setMode('realtime');
if (timeSystem?.key !== timeParameters.timeSystem) {
this.openmct.time.setTimeSystem(timeParameters.timeSystem);
if (!this.openmct.time.timeSystem()
|| this.openmct.time.timeSystem().key !== timeParameters.timeSystem) {
this.openmct.time.timeSystem(timeParameters.timeSystem);
}
}
}
@@ -139,14 +141,13 @@ export default class URLTimeSettingsSynchronizer {
}
setUrlFromTimeApi() {
const searchParams = this.openmct.router.getAllSearchParams();
const clock = this.openmct.time.getClock();
const mode = this.openmct.time.getMode();
const bounds = this.openmct.time.getBounds();
const clockOffsets = this.openmct.time.getClockOffsets();
let searchParams = this.openmct.router.getAllSearchParams();
let clock = this.openmct.time.clock();
let bounds = this.openmct.time.bounds();
let clockOffsets = this.openmct.time.clockOffsets();
if (mode === FIXED_MODE_KEY) {
searchParams.set(SEARCH_MODE, FIXED_MODE_KEY);
if (clock === undefined) {
searchParams.set(SEARCH_MODE, MODE_FIXED);
searchParams.set(SEARCH_START_BOUND, bounds.start);
searchParams.set(SEARCH_END_BOUND, bounds.end);
@@ -167,9 +168,8 @@ export default class URLTimeSettingsSynchronizer {
searchParams.delete(SEARCH_END_BOUND);
}
searchParams.set(SEARCH_TIME_SYSTEM, this.openmct.time.getTimeSystem().key);
// this.openmct.router.setAllSearchParams(searchParams);
this.openmct.router.updateParams(searchParams);
searchParams.set(SEARCH_TIME_SYSTEM, this.openmct.time.timeSystem().key);
this.openmct.router.setAllSearchParams(searchParams);
}
areTimeParametersValid(timeParameters) {
@@ -178,7 +178,7 @@ export default class URLTimeSettingsSynchronizer {
if (this.isModeValid(timeParameters.mode)
&& this.isTimeSystemValid(timeParameters.timeSystem)) {
if (timeParameters.mode === FIXED_MODE_KEY) {
if (timeParameters.mode === 'fixed') {
isValid = this.areStartAndEndValid(timeParameters.bounds);
} else {
isValid = this.areStartAndEndValid(timeParameters.clockOffsets);
@@ -200,9 +200,8 @@ export default class URLTimeSettingsSynchronizer {
isTimeSystemValid(timeSystem) {
let isValid = timeSystem !== undefined;
if (isValid) {
const timeSystemObject = this.openmct.time.timeSystems.get(timeSystem);
let timeSystemObject = this.openmct.time.timeSystems.get(timeSystem);
isValid = timeSystemObject !== undefined;
}
@@ -218,7 +217,7 @@ export default class URLTimeSettingsSynchronizer {
}
if (isValid) {
if (mode.toLowerCase() === FIXED_MODE_KEY) {
if (mode.toLowerCase() === MODE_FIXED) {
isValid = true;
} else {
isValid = this.openmct.time.clocks.get(mode) !== undefined;
@@ -229,7 +228,7 @@ export default class URLTimeSettingsSynchronizer {
}
areStartAndEndEqual(firstBounds, secondBounds) {
return firstBounds?.start === secondBounds.start
&& firstBounds?.end === secondBounds.end;
return firstBounds.start === secondBounds.start
&& firstBounds.end === secondBounds.end;
}
}

View File

@@ -103,11 +103,11 @@ export default {
},
followTimeContext() {
this.timeContext.on('boundsChanged', this.refreshData);
this.timeContext.on('bounds', this.refreshData);
},
stopFollowingTimeContext() {
if (this.timeContext) {
this.timeContext.off('boundsChanged', this.refreshData);
this.timeContext.off('bounds', this.refreshData);
}
},
addToComposition(telemetryObject) {

View File

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

View File

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

View File

@@ -36,6 +36,7 @@ export default function plugin() {
domainObject.configuration = {};
domainObject.label = 'Condition Widget';
domainObject.conditionalLabel = '';
domainObject.url = '';
},
form: [
{

View File

@@ -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 = "export";
this.group = "json";
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,65 +234,47 @@ 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
*/
@@ -204,72 +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);
}
}
});
if (!childObjectReferenceId) {
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) {
@@ -277,4 +338,8 @@ export default class ExportAsJSONAction {
this._saveAs(this._wrapTree());
}
}
_copy(object) {
return JSON.parse(JSON.stringify(object));
}
}

View File

@@ -42,6 +42,8 @@ export default {
};
},
mounted() {
this.updateFaultList();
this.unsubscribe = this.openmct.faults
.subscribe(this.domainObject, this.updateFault);
},
@@ -66,11 +68,7 @@ export default {
this.openmct.faults
.request(this.domainObject)
.then(faultsData => {
if (faultsData?.length > 0) {
this.faultsList = faultsData.map(fd => fd.fault);
} else {
this.faultsList = [];
}
this.faultsList = faultsData.map(fd => fd.fault);
});
}
}

View File

@@ -56,6 +56,10 @@ define([
});
},
showTab: function (isEditing) {
if (isEditing) {
return true;
}
const hasPersistedFilters = Boolean(domainObject?.configuration?.filters);
const hasGlobalFilters = Boolean(domainObject?.configuration?.globalFilters);

View File

@@ -154,8 +154,11 @@ export default {
let originalClassName = this.dragGhost.classList[0];
this.dragGhost.className = '';
this.dragGhost.classList.add(originalClassName, iconClass);
this.dragGhost.textContent = '';
const span = document.createElement('span');
span.textContent = this.domainObject.name;
this.dragGhost.appendChild(span);
this.dragGhost.innerHTML = `<span>${this.domainObject.name}</span>`;
event.dataTransfer.setDragImage(this.dragGhost, 0, 0);
}

View File

@@ -64,6 +64,11 @@ export default class CreateAction extends PropertiesAction {
const parentDomainObject = this.openmct.objects.toMutable(parentDomainObjectPath[0]);
// TODO REMOVE FOR OMM-RELEASE-5.0
if (!parentDomainObject.identifier.namespace && parentDomainObject.key) {
console.error(`parent namespace in key: ${parentDomainObject.key}`);
}
this.domainObject.modified = Date.now();
this.domainObject.location = this.openmct.objects.makeKeyString(parentDomainObject.identifier);
this.domainObject.identifier.namespace = parentDomainObject.identifier.namespace;

View File

@@ -29,8 +29,9 @@ export default class ImportAsJSONAction {
this.key = 'import.JSON';
this.description = '';
this.cssClass = "icon-import";
this.group = "import";
this.group = "json";
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

View File

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

View File

@@ -1,167 +0,0 @@
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);
}
}

View File

@@ -125,7 +125,7 @@
v-if="selectedPage && !selectedPage.isLocked"
:class="{ 'disabled': activeTransaction }"
class="c-notebook__drag-area icon-plus"
@click="newEntry(null, $event)"
@click="newEntry()"
@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, selectEntry } from '../utils/notebook-entries';
import { addNotebookEntry, createNewEmbed, getEntryPosById, getNotebookEntries, mutateObject } from '../utils/notebook-entries';
import { saveNotebookImageDomainObject, updateNamespaceOfDomainObject } from '../utils/notebook-image';
import { isNotebookViewType, RESTRICTED_NOTEBOOK_TYPE } from '../notebook-constants';
@@ -793,29 +793,15 @@ export default {
return section.id;
},
async newEntry(embed, event) {
async newEntry(embed = null) {
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();

View File

@@ -32,7 +32,7 @@
@dragover="changeCursor"
@drop.capture="cancelEditMode"
@drop.prevent="dropOnEntry"
@click="selectAndEmitEntry($event, entry)"
@click="selectEntry($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, selectEntry } from '../utils/notebook-entries';
import { createNewEmbed } from '../utils/notebook-entries';
import { saveNotebookImageDomainObject, updateNamespaceOfDomainObject } from '../utils/notebook-image';
import sanitizeHtml from 'sanitize-html';
@@ -479,18 +479,37 @@ export default {
updateEntryValue($event) {
this.editMode = false;
const value = $event.target.innerText;
this.entry.text = sanitizeHtml(value, SANITIZATION_SCHEMA);
this.timestampAndUpdate();
if (value !== this.entry.text && value.match(/\S/)) {
this.entry.text = sanitizeHtml(value, SANITIZATION_SCHEMA);
this.timestampAndUpdate();
} else {
this.$emit('cancelEdit');
}
},
selectAndEmitEntry(event, entry) {
selectEntry({
element: event.currentTarget,
entryId: entry.id,
domainObject: this.domainObject,
openmct: this.openmct,
onAnnotationChange: this.timestampAndUpdate,
notebookAnnotations: this.notebookAnnotations
});
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);
event.stopPropagation();
this.$emit('entry-selection', this.entry);
}

View File

@@ -21,7 +21,6 @@
*****************************************************************************/
import CopyToNotebookAction from './actions/CopyToNotebookAction';
import ExportNotebookAsTextAction from './actions/ExportNotebookAsTextAction';
import NotebookSnapshotIndicator from './components/NotebookSnapshotIndicator.vue';
import NotebookViewProvider from './NotebookViewProvider';
import NotebookType from './NotebookType';
@@ -81,7 +80,6 @@ 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: {

View File

@@ -2,7 +2,7 @@ import objectLink from '../../../ui/mixins/object-link';
import { v4 as uuid } from 'uuid';
async function getUsername(openmct) {
let username = null;
let username = '';
if (openmct.user.hasProvider()) {
const user = await openmct.user.getCurrentUser();
@@ -44,35 +44,6 @@ 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;

View File

@@ -288,7 +288,7 @@ export default {
seriesModels: [],
legend: {},
pending: 0,
isRealTime: this.openmct.time.isRealTime(),
isRealTime: this.openmct.time.clock() !== undefined,
loaded: false,
isTimeOutOfSync: false,
isFrozenOnMouseDown: false,
@@ -369,7 +369,7 @@ export default {
document.addEventListener('keydown', this.handleKeyDown);
document.addEventListener('keyup', this.handleKeyUp);
eventHelpers.extend(this);
this.updateMode = this.updateMode.bind(this);
this.updateRealTime = this.updateRealTime.bind(this);
this.updateDisplayBounds = this.updateDisplayBounds.bind(this);
this.setTimeContext = this.setTimeContext.bind(this);
@@ -533,20 +533,19 @@ export default {
this.stopFollowingTimeContext();
this.timeContext = this.openmct.time.getContextForView(this.path);
console.log('time context mctplot', this.timeContext, this.path);
this.followTimeContext();
},
followTimeContext() {
this.updateDisplayBounds(this.timeContext.getBounds());
this.timeContext.on('modeChanged', this.updateMode);
this.timeContext.on('boundsChanged', this.updateDisplayBounds);
this.updateDisplayBounds(this.timeContext.bounds());
this.timeContext.on('clock', this.updateRealTime);
this.timeContext.on('bounds', this.updateDisplayBounds);
this.synchronized(true);
},
stopFollowingTimeContext() {
if (this.timeContext) {
this.timeContext.off("clockChanged", this.updateMode);
this.timeContext.off("boundsChanged", this.updateDisplayBounds);
this.timeContext.off("clock", this.updateRealTime);
this.timeContext.off("bounds", this.updateDisplayBounds);
}
},
getConfig() {
@@ -754,8 +753,8 @@ export default {
const displayRange = series.getDisplayRange(xKey);
this.config.xAxis.set('range', displayRange);
},
updateMode() {
this.isRealTime = this.timeContext.isRealTime();
updateRealTime(clock) {
this.isRealTime = clock !== undefined;
},
/**
@@ -816,13 +815,13 @@ export default {
* displays can update accordingly.
*/
synchronized(value) {
const isRealTime = this.timeContext.isRealTime();
const isLocalClock = this.timeContext.clock();
if (typeof value !== 'undefined') {
this._synchronized = value;
this.isTimeOutOfSync = value !== true;
const isUnsynced = isRealTime && !value;
const isUnsynced = isLocalClock && !value;
this.setStatus(isUnsynced);
}

View File

@@ -86,17 +86,17 @@ export default {
this.xAxis = this.getXAxisFromConfig();
this.loaded = true;
this.setUpXAxisOptions();
this.openmct.time.on('timeSystemChanged', this.syncXAxisToTimeSystem);
this.openmct.time.on('timeSystem', this.syncXAxisToTimeSystem);
this.listenTo(this.xAxis, 'change', this.setUpXAxisOptions);
},
beforeDestroy() {
this.openmct.time.off('timeSystemChanged', this.syncXAxisToTimeSystem);
this.openmct.time.off('timeSystem', this.syncXAxisToTimeSystem);
},
methods: {
isEnabledXKeyToggle() {
const isSinglePlot = this.xKeyOptions && this.xKeyOptions.length > 1 && this.seriesModel;
const isFrozen = this.xAxis.get('frozen');
const inRealTimeMode = this.openmct.time.getClock();
const inRealTimeMode = this.openmct.time.clock();
return isSinglePlot && !isFrozen && !inRealTimeMode;
},

View File

@@ -20,12 +20,10 @@
at runtime from the About dialog for additional information.
-->
<!-- eslint-disable vue/no-v-html -->
<template>
<div class="gl-plot-chart-area">
<span v-html="canvasTemplate"></span>
<span v-html="canvasTemplate"></span>
<canvas :style="canvasStyle"></canvas>
<canvas :style="canvasStyle"></canvas>
<div
ref="limitArea"
class="js-limit-area"
@@ -132,10 +130,15 @@ export default {
required: true
}
},
data() {
return {
canvasTemplate: '<canvas style="position: absolute; background: none; width: 100%; height: 100%;"></canvas>'
};
computed: {
canvasStyle() {
return {
position: 'absolute',
background: 'none',
width: '100%',
height: '100%'
};
}
},
watch: {
highlights() {
@@ -418,7 +421,10 @@ export default {
// Have to throw away the old canvas elements and replace with new
// canvas elements in order to get new drawing contexts.
const div = document.createElement('div');
div.innerHTML = this.canvasTemplate + this.canvasTemplate;
div.innerHTML = `
<canvas style="position: absolute; background: none; width: 100%; height: 100%;"></canvas>
<canvas style="position: absolute; background: none; width: 100%; height: 100%;"></canvas>
`;
const mainCanvas = div.querySelectorAll("canvas")[1];
const overlayCanvas = div.querySelectorAll("canvas")[0];
this.canvas.parentNode.replaceChild(mainCanvas, this.canvas);

View File

@@ -63,6 +63,10 @@ import { symlog } from '../mathUtils';
*
* @extends {Model<PlotSeriesModelType, PlotSeriesModelOptions>}
*/
const FLOAT32_MAX = 3.4e38;
const FLOAT32_MIN = -3.4e38;
export default class PlotSeries extends Model {
logMode = false;
@@ -357,7 +361,7 @@ export default class PlotSeries extends Model {
let stats = this.get('stats');
let changed = false;
if (!stats) {
if ([Infinity, -Infinity].includes(value)) {
if ([Infinity, -Infinity].includes(value) || !this.isValidFloat32(value)) {
return;
}
@@ -369,13 +373,13 @@ export default class PlotSeries extends Model {
};
changed = true;
} else {
if (stats.maxValue < value && value !== Infinity) {
if (stats.maxValue < value && value !== Infinity && this.isValidFloat32(value)) {
stats.maxValue = value;
stats.maxPoint = point;
changed = true;
}
if (stats.minValue > value && value !== -Infinity) {
if (stats.minValue > value && value !== -Infinity && this.isValidFloat32(value)) {
stats.minValue = value;
stats.minPoint = point;
changed = true;
@@ -411,7 +415,7 @@ export default class PlotSeries extends Model {
const lastYVal = this.getYVal(data[insertIndex - 1]);
if (this.isValueInvalid(currentYVal) && this.isValueInvalid(lastYVal)) {
console.warn('[Plot] Invalid Y Values detected');
console.warn(`[Plot] Invalid Y Values detected: ${currentYVal} ${lastYVal}`);
return;
}
@@ -439,7 +443,15 @@ export default class PlotSeries extends Model {
* @private
*/
isValueInvalid(val) {
return Number.isNaN(val) || this.unPlottableValues.includes(val);
return Number.isNaN(val) || this.unPlottableValues.includes(val) || !this.isValidFloat32(val);
}
/**
*
* @private
*/
isValidFloat32(val) {
return val < FLOAT32_MAX && val > FLOAT32_MIN;
}
/**

View File

@@ -19,7 +19,6 @@
</li>
<li class="grid-row">
<div
id="log-mode-checkbox"
class="grid-cell label"
title="Enable log mode."
>
@@ -30,7 +29,6 @@
<input
v-model="logMode"
class="js-log-mode-input"
aria-labelledby="log-mode-checkbox"
type="checkbox"
@change="updateForm('logMode')"
/>
@@ -38,14 +36,12 @@
</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>

View File

@@ -46,7 +46,6 @@ export default class RemoteClock extends DefaultClock {
this.timeTelemetryObject = undefined;
this.parseTime = undefined;
this.formatTime = undefined;
this.metadata = undefined;
this.lastTick = 0;
@@ -138,10 +137,6 @@ export default class RemoteClock extends DefaultClock {
this.parseTime = (datum) => {
return timeFormatter.parse(datum);
};
this.formatTime = (datum) => {
return timeFormatter.format(datum);
};
}
/**

View File

@@ -46,96 +46,76 @@ class StaticModelProvider {
throw new Error(keyString + ' not found in import models.');
}
parseObjectLeaf(objectLeaf, idMap, newRootNamespace, oldRootNamespace) {
parseObjectLeaf(objectLeaf, idMap, namespace) {
Object.keys(objectLeaf).forEach((nodeKey) => {
if (idMap.get(nodeKey)) {
const newIdentifier = objectUtils.makeKeyString({
namespace: newRootNamespace,
namespace,
key: idMap.get(nodeKey)
});
objectLeaf[newIdentifier] = { ...objectLeaf[nodeKey] };
delete objectLeaf[nodeKey];
objectLeaf[newIdentifier] = this.parseTreeLeaf(newIdentifier, objectLeaf[newIdentifier], idMap, newRootNamespace, oldRootNamespace);
objectLeaf[newIdentifier] = this.parseTreeLeaf(newIdentifier, objectLeaf[newIdentifier], idMap, namespace);
} else {
objectLeaf[nodeKey] = this.parseTreeLeaf(nodeKey, objectLeaf[nodeKey], idMap, newRootNamespace, oldRootNamespace);
objectLeaf[nodeKey] = this.parseTreeLeaf(nodeKey, objectLeaf[nodeKey], idMap, namespace);
}
});
return objectLeaf;
}
parseArrayLeaf(arrayLeaf, idMap, newRootNamespace, oldRootNamespace) {
parseArrayLeaf(arrayLeaf, idMap, namespace) {
return arrayLeaf.map((leafValue, index) => this.parseTreeLeaf(
null, leafValue, idMap, newRootNamespace, oldRootNamespace));
null, leafValue, idMap, namespace));
}
parseBranchedLeaf(branchedLeafValue, idMap, newRootNamespace, oldRootNamespace) {
parseBranchedLeaf(branchedLeafValue, idMap, namespace) {
if (Array.isArray(branchedLeafValue)) {
return this.parseArrayLeaf(branchedLeafValue, idMap, newRootNamespace, oldRootNamespace);
return this.parseArrayLeaf(branchedLeafValue, idMap, namespace);
} else {
return this.parseObjectLeaf(branchedLeafValue, idMap, newRootNamespace, oldRootNamespace);
return this.parseObjectLeaf(branchedLeafValue, idMap, namespace);
}
}
parseTreeLeaf(leafKey, leafValue, idMap, newRootNamespace, oldRootNamespace) {
parseTreeLeaf(leafKey, leafValue, idMap, namespace) {
if (leafValue === null || leafValue === undefined) {
return leafValue;
}
const hasChild = typeof leafValue === 'object';
if (hasChild) {
return this.parseBranchedLeaf(leafValue, idMap, newRootNamespace, oldRootNamespace);
return this.parseBranchedLeaf(leafValue, idMap, namespace);
}
if (leafKey === 'key') {
let mappedLeafValue;
if (oldRootNamespace) {
mappedLeafValue = idMap.get(objectUtils.makeKeyString({
namespace: oldRootNamespace,
key: leafValue
}));
} else {
mappedLeafValue = idMap.get(leafValue);
}
return mappedLeafValue ?? leafValue;
return idMap.get(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;
return namespace;
} else if (leafKey === 'location') {
const mappedLeafValue = idMap.get(leafValue);
if (!mappedLeafValue) {
return null;
}
const newLocationIdentifier = objectUtils.makeKeyString({
namespace: newRootNamespace,
key: mappedLeafValue
});
return newLocationIdentifier;
} else {
const mappedLeafValue = idMap.get(leafValue);
if (mappedLeafValue) {
const newIdentifier = objectUtils.makeKeyString({
namespace: newRootNamespace,
key: mappedLeafValue
if (idMap.get(leafValue)) {
const newLocationIdentifier = objectUtils.makeKeyString({
namespace,
key: idMap.get(leafValue)
});
return newIdentifier;
} else {
return leafValue;
return newLocationIdentifier;
}
return null;
} else if (idMap.get(leafValue)) {
const newIdentifier = objectUtils.makeKeyString({
namespace,
key: idMap.get(leafValue)
});
return newIdentifier;
} else {
return leafValue;
}
}
rewriteObjectIdentifiers(importData, rootIdentifier) {
const { namespace: oldRootNamespace } = objectUtils.parseKeyString(importData.rootId);
const { namespace: newRootNamespace } = rootIdentifier;
const namespace = rootIdentifier.namespace;
const idMap = new Map();
const objectTree = importData.openmct;
@@ -148,7 +128,7 @@ class StaticModelProvider {
idMap.set(originalId, newId);
});
const newTree = this.parseTreeLeaf(null, objectTree, idMap, newRootNamespace, oldRootNamespace);
const newTree = this.parseTreeLeaf(null, objectTree, idMap, namespace);
return newTree;
}

View File

@@ -20,265 +20,130 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import testStaticDataEmptyNamespace from './test-data/static-provider-test-empty-namespace.json';
import testStaticDataFooNamespace from './test-data/static-provider-test-foo-namespace.json';
import testStaticData from './static-provider-test.json';
import StaticModelProvider from './StaticModelProvider';
describe('StaticModelProvider', function () {
describe('with empty namespace', function () {
let staticProvider;
beforeEach(function () {
const staticData = JSON.parse(JSON.stringify(testStaticDataEmptyNamespace));
staticProvider = new StaticModelProvider(staticData, {
namespace: 'my-import',
key: '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'
});
});
});
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');
});
let staticProvider;
beforeEach(function () {
const staticData = JSON.parse(JSON.stringify(testStaticData));
staticProvider = new StaticModelProvider(staticData, {
namespace: 'my-import',
key: 'root'
});
});
describe('with namespace "foo"', function () {
let staticProvider;
describe('rootObject', function () {
let rootModel;
beforeEach(function () {
const staticData = JSON.parse(JSON.stringify(testStaticDataFooNamespace));
staticProvider = new StaticModelProvider(staticData, {
rootModel = staticProvider.get({
namespace: 'my-import',
key: 'root'
});
});
describe('rootObject', function () {
let rootModel;
it('is located at top level', function () {
expect(rootModel.location).toBe('ROOT');
});
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 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({
namespace: 'my-import',
key: 'root'
});
});
describe('childObjects', function () {
let clock;
let layout;
let swg;
let folder;
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'
});
it('has new-format composition', function () {
expect(rootModel.composition).toContain({
namespace: 'my-import',
key: '1'
});
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');
});
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');
expect(rootModel.composition).toContain({
namespace: 'my-import',
key: '2'
});
});
});
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 new-style 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 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');
});
});
});

Some files were not shown because too many files have changed in this diff Show More