Compare commits

..

4 Commits

Author SHA1 Message Date
Shefali Joshi
eb95b1f50b cherry-pick(#6568): Only decrement and save if there is composition b… (#6599)
cherry-pick(#6568): Only decrement and save if there is composition but no child object reference (#6568)

Co-authored-by: David Tsay <3614296+davetsay@users.noreply.github.com>
2023-04-17 14:26:41 -07:00
Jamie V
c6e913245d cherry-pick #6562 : including identifier checks for exporting json (#6595) 2023-04-17 14:08:16 -07:00
Shefali Joshi
cf7016c814 cherry-pick(#6495): Remove ticker and rely solely on the clock ticks to update the timelist durations (#6529)
cherry-pick(#6495): Remove ticker and rely solely on the clock ticks to update the timelist durations (#6495)
* Remove ticker for timelist and rename a function for readability
* Use formatting for remote clock if available.
* throttle updates to the timestamp and listing activities
---------

Co-authored-by: Andrew Henry <akhenry@gmail.com>
2023-03-30 20:48:05 +00:00
Jesse Mazzella
91d0c0be10 chore: bump version to 2.2.0 2023-03-21 11:01:31 -07:00
63 changed files with 615 additions and 1351 deletions

View File

@@ -7,10 +7,6 @@ executors:
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.29.0 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,13 +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"]
- 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'
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' }}
@@ -32,15 +33,8 @@ jobs:
- run: npx playwright@1.29.0 install
- run: npx playwright install chrome-beta
- run: npm install
- run: npm run test:e2e:full -- --maxFailures=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

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

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

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

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

@@ -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,29 +27,26 @@ test.describe('Testing LAD table configuration', () => {
test.beforeEach(async ({ page }) => {
await page.goto('./', { waitUntil: 'networkidle' });
// Create LAD table
const ladTable = await createDomainObjectWithDefaults(page, {
type: 'LAD Table',
name: "Test LAD Table"
});
// Create Sine Wave Generator
await createDomainObjectWithDefaults(page, {
type: 'Sine Wave Generator',
name: "Test Sine Wave Generator",
parent: ladTable.uuid
name: "Test Sine Wave Generator"
});
await page.goto(ladTable.url);
// Create LAD table
await createDomainObjectWithDefaults(page, {
type: 'LAD Table',
name: "Test LAD Table"
});
});
test('in edit mode, LAD Tables provide ability to hide columns', async ({ page }) => {
// Edit LAD table
await page.locator('[title="Edit"]').click();
// // Expand the 'My Items' folder in the left tree
// await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();
// // Add the Sine Wave Generator to the LAD table and save changes
// await page.dragAndDrop('role=treeitem[name=/Test Sine Wave Generator/]', '.c-lad-table-wrapper');
// Expand the 'My Items' folder in the left tree
await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click();
// Add the Sine Wave Generator to the LAD table and save changes
await page.dragAndDrop('role=treeitem[name=/Test Sine Wave Generator/]', '.c-lad-table-wrapper');
// select configuration tab in inspector
await selectInspectorTab(page, 'LAD Table Configuration');
@@ -116,24 +113,6 @@ test.describe('Testing LAD table configuration', () => {
await expect(page.getByRole('cell', { name: 'Units' })).toBeVisible();
await expect(page.getByRole('cell', { name: 'Type' })).toBeVisible();
});
test('LAD Tables don\'t allow selection of rows but does show context click menus', async ({ page }) => {
const cell = await page.locator('.js-first-data');
const userSelectable = await cell.evaluate((el) => {
return window.getComputedStyle(el).getPropertyValue('user-select');
});
expect(userSelectable).toBe('none');
// Right-click on the LAD table row
await cell.click({
button: 'right'
});
const menuOptions = page.locator('.c-menu ul');
await expect.soft(menuOptions).toContainText('View Full Datum');
await expect.soft(menuOptions).toContainText('View Historical Data');
await expect.soft(menuOptions).toContainText('Remove');
// await page.locator('li[title="Remove this object from its containing object."]').click();
});
});
test.describe('Testing LAD table @unstable', () => {

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');
@@ -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: 'networkidle' });
// 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 }) => {});
@@ -249,15 +219,7 @@ test.describe('Notebook entry tests', () => {
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
await createDomainObjectWithDefaults(page, {
@@ -301,25 +263,7 @@ test.describe('Notebook entry tests', () => {
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

@@ -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: '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,33 +169,6 @@ 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
*/

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: 'networkidle' });
});
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 }) => {
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"
@@ -280,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

@@ -268,9 +268,6 @@ async function getCanvasPixelsWithData(page) {
* @param {import('@playwright/test').Page} page
*/
async function assertLimitLinesExistAndAreVisible(page) {
// Wait for plot series data to load
await expect(page.locator('.js-series-data-loaded')).toBeVisible();
// Wait for limit lines to be created
await page.waitForSelector('.js-limit-area', { state: 'attached' });
const limitLineCount = await page.locator('.c-plot-limit-line').count();
// There should be 10 limit lines created by default

View File

@@ -28,14 +28,6 @@ const { test, expect } = require('../../../../pluginFixtures');
const { createDomainObjectWithDefaults, setRealTimeMode, setFixedTimeMode } = require('../../../../appActions');
test.describe('Plot Tagging', () => {
/**
* Given a canvas and a set of points, tags the points on the canvas.
* @param {import('@playwright/test').Page} page
* @param {HTMLCanvasElement} canvas a telemetry item with a plot
* @param {Number} xEnd a telemetry item with a plot
* @param {Number} yEnd a telemetry item with a plot
* @returns {Promise}
*/
async function createTags({page, canvas, xEnd, yEnd}) {
await canvas.hover({trial: true});
@@ -72,20 +64,12 @@ test.describe('Plot Tagging', () => {
await page.getByText('Science').click();
}
/**
* Given a telemetry item (e.g., a Sine Wave Generator) with a plot, tests that the plot can be tagged.
* @param {import('@playwright/test').Page} page
* @param {import('../../../../appActions').CreatedObjectInfo} telemetryItem a telemetry item with a plot
* @returns {Promise}
*/
async function testTelemetryItem(page, telemetryItem) {
async function testTelemetryItem(page, canvas, telemetryItem) {
// Check that telemetry item also received the tag
await page.goto(telemetryItem.url);
await expect(page.getByText('No tags to display for this item')).toBeVisible();
const canvas = page.locator('canvas').nth(1);
//Wait for canvas to stablize.
await canvas.hover({trial: true});
@@ -101,32 +85,20 @@ test.describe('Plot Tagging', () => {
await expect(page.getByText('Driving')).toBeHidden();
}
/**
* Given a page, tests that tags are searchable, deletable, and persist across reloads.
* @param {import('@playwright/test').Page} page
* @returns {Promise}
*/
async function basicTagsTests(page) {
// Search for Driving
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
// Clicking elsewhere should cause annotation selection to be cleared
await expect(page.getByText('No tags to display for this item')).toBeVisible();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('driv');
// click on the search result
await page.getByRole('searchbox', { name: 'OpenMCT Search' }).getByText(/Sine Wave/).first().click();
// Delete Driving
await page.hover('[aria-label="Tag"]:has-text("Driving")');
await page.locator('[aria-label="Remove tag Driving"]').click();
async function basicTagsTests(page, canvas) {
// Search for Science
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc');
await expect(page.locator('[aria-label="Search Result"]').nth(0)).toContainText("Science");
await expect(page.locator('[aria-label="Search Result"]').nth(0)).not.toContainText("Drilling");
// Delete Driving
await page.hover('[aria-label="Tag"]:has-text("Driving")');
await page.locator('[aria-label="Remove tag Driving"]').click();
await expect(page.locator('[aria-label="Tags Inspector"]')).toContainText("Science");
await expect(page.locator('[aria-label="Tags Inspector"]')).not.toContainText("Driving");
// Search for Driving
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('driv');
@@ -137,13 +109,12 @@ test.describe('Plot Tagging', () => {
page.reload(),
page.waitForLoadState('networkidle')
]);
// wait for plots to load
await expect(page.locator('.js-series-data-loaded')).toBeVisible();
// wait for plot progress bar to disappear
await page.locator('.l-view-section.c-progress-bar').waitFor({ state: 'detached' });
await page.getByText('Annotations').click();
await expect(page.getByText('No tags to display for this item')).toBeVisible();
const canvas = page.locator('canvas').nth(1);
// click on the tagged plot point
await canvas.click({
position: {
@@ -200,8 +171,8 @@ test.describe('Plot Tagging', () => {
// changing to fixed time mode rebuilds canvas?
canvas = page.locator('canvas').nth(1);
await basicTagsTests(page);
await testTelemetryItem(page, alphaSineWave);
await basicTagsTests(page, canvas);
await testTelemetryItem(page, canvas, alphaSineWave);
// set to real time mode
await setRealTimeMode(page);
@@ -211,8 +182,8 @@ test.describe('Plot Tagging', () => {
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc');
// click on the search result
await page.getByRole('searchbox', { name: 'OpenMCT Search' }).getByText('Alpha Sine Wave').first().click();
// wait for plots to load
await expect(page.locator('.js-series-data-loaded')).toBeVisible();
// wait for plot progress bar to disappear
await page.locator('.l-view-section.c-progress-bar').waitFor({ state: 'detached' });
// expect plot to be paused
await expect(page.locator('[title="Resume displaying real-time data"]')).toBeVisible();
@@ -231,7 +202,7 @@ test.describe('Plot Tagging', () => {
xEnd: 700,
yEnd: 480
});
await basicTagsTests(page);
await basicTagsTests(page, canvas);
});
test('Tags work with Stacked Plots', async ({ page }) => {
@@ -261,7 +232,7 @@ test.describe('Plot Tagging', () => {
xEnd: 700,
yEnd: 215
});
await basicTagsTests(page);
await testTelemetryItem(page, alphaSineWave);
await basicTagsTests(page, canvas);
await testTelemetryItem(page, canvas, alphaSineWave);
});
});

View File

@@ -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'});
@@ -191,7 +191,7 @@ test.describe('Recent Objects', () => {
expect(await clockBreadcrumbs.count()).toBe(2);
expect(await clockBreadcrumbs.nth(0).innerText()).not.toEqual(await clockBreadcrumbs.nth(1).innerText());
});
test("Enforces a limit of 20 recent objects and clears the recent objects", async ({ page }) => {
test("Enforces a limit of 20 recent objects", async ({ page }) => {
// Creating 21 objects takes a while, so increase the timeout
test.slow();
@@ -242,67 +242,14 @@ test.describe('Recent Objects', () => {
// Assert that the Clock treeitem is no longer highlighted
await expect(lastClockTreeItem.locator('.c-tree__item')).not.toHaveClass(/is-targeted-item/);
// 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);
});
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

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

@@ -1,17 +1,16 @@
{
"name": "openmct",
"version": "2.2.2",
"version": "2.2.0",
"description": "The Open MCT core platform",
"devDependencies": {
"@babel/eslint-parser": "7.18.9",
"@braintree/sanitize-url": "6.0.2",
"@deploysentinel/playwright": "0.3.3",
"@percy/cli": "1.23.0",
"@percy/cli": "1.17.0",
"@percy/playwright": "1.0.4",
"@playwright/test": "1.29.0",
"@types/eventemitter3": "1.2.0",
"@types/jasmine": "4.3.1",
"@types/lodash": "4.14.192",
"@types/lodash": "4.14.191",
"babel-loader": "9.1.0",
"babel-plugin-istanbul": "6.1.1",
"codecov": "3.8.3",
@@ -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.10.0",
"eslint-plugin-vue": "9.9.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",
@@ -52,28 +51,28 @@
"nyc": "15.1.0",
"painterro": "1.2.78",
"playwright-core": "1.29.0",
"plotly.js-basic-dist": "2.20.0",
"plotly.js-gl2d-dist": "2.20.0",
"plotly.js-basic-dist": "2.17.0",
"plotly.js-gl2d-dist": "2.17.1",
"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.57.1",
"sass-loader": "13.2.0",
"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.74.0",
"webpack-cli": "5.0.0",
"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

@@ -43,7 +43,7 @@
</div>
<div
v-if="!hideOptions && filteredOptions.length > 0"
class="c-menu c-input--autocomplete__options js-autocomplete-options"
class="c-menu c-input--autocomplete__options"
aria-label="Autocomplete Options"
@blur="hideOptions = true"
>

View File

@@ -25,7 +25,6 @@
:is="urlDefined ? 'a' : 'span'"
class="c-condition-widget u-style-receiver js-style-receiver"
:href="url"
:target="url ? '_BLANK' : ''"
>
<div class="c-condition-widget__label">
{{ label }}

View File

@@ -32,7 +32,7 @@ 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 = [];

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

@@ -322,7 +322,7 @@ export default {
rgba(125,125,125,.2) 8px
)`
) : ''}`,
transform: `scale(${this.zoomFactor}) translate(${this.imageTranslateX / 2}px, ${this.imageTranslateY / 2}px)`,
transform: `scale(${this.zoomFactor}) translate(${this.imageTranslateX}px, ${this.imageTranslateY}px)`,
transition: `${!this.pan && this.animateZoom ? 'transform 250ms ease-in' : 'initial'}`,
width: `${this.sizedImageWidth}px`,
height: `${this.sizedImageHeight}px`
@@ -709,7 +709,7 @@ export default {
getVisibleLayerStyles(layer) {
return {
backgroundImage: `url(${layer.source})`,
transform: `scale(${this.zoomFactor}) translate(${this.imageTranslateX / 2}px, ${this.imageTranslateY / 2}px)`,
transform: `scale(${this.zoomFactor}) translate(${this.imageTranslateX}px, ${this.imageTranslateY}px)`,
transition: `${!this.pan && this.animateZoom ? 'transform 250ms ease-in' : 'initial'}`
};
},

View File

@@ -195,7 +195,7 @@
margin-bottom: 1px;
padding-bottom: $interiorMarginSm;
&.animate-scroll {
scroll-behavior: smooth;
scroll-behavior: smooth;
}
}
@@ -320,7 +320,7 @@
flex-direction: row;
position: absolute;
left: $interiorMargin; top: $interiorMargin;
z-index: 10;
z-index: 70;
background: $colorLocalControlOvrBg;
border-radius: $basicCr;
align-items: center;

View File

@@ -29,7 +29,7 @@ export default class ImportAsJSONAction {
this.key = 'import.JSON';
this.description = '';
this.cssClass = "icon-import";
this.group = "import";
this.group = "json";
this.priority = 2;
this.openmct = openmct;

View File

@@ -23,17 +23,11 @@
import Annotations from './AnnotationsInspectorView.vue';
import Vue from 'vue';
export default function AnnotationsViewProvider(openmct) {
export default function ElementsViewProvider(openmct) {
return {
key: 'annotationsView',
name: 'Annotations',
canView: function (selection) {
const availableTags = openmct.annotation.getAvailableTags();
if (availableTags.length < 1) {
return false;
}
return selection.length;
},
view: function (selection) {

View File

@@ -177,9 +177,10 @@ export default {
if (this.$refs.TagEditor) {
const clickedInsideTagEditor = this.$refs.TagEditor.contains(event.target);
if (!clickedInsideTagEditor) {
// Remove last tag when user clicks outside of TagSelection
this.addedTags.pop();
// Hide TagSelection and show "Add Tag" button
this.userAddingTag = false;
this.tagsChanged();
}
}
},

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

@@ -22,9 +22,7 @@
<template>
<div
v-if="loaded"
ref="plot"
class="gl-plot"
:class="{ 'js-series-data-loaded' : seriesDataLoaded }"
>
<slot></slot>
<div class="plot-wrapper-axis-and-display-area flex-elem grows">
@@ -349,9 +347,6 @@ export default {
const parentLeftTickWidth = this.parentYTickWidth.leftTickWidth;
return parentLeftTickWidth || leftTickWidth;
},
seriesDataLoaded() {
return ((this.pending === 0) && this.loaded);
}
},
watch: {
@@ -417,7 +412,6 @@ export default {
this.openmct.selection.off('change', this.updateSelection);
document.removeEventListener('keydown', this.handleKeyDown);
document.removeEventListener('keyup', this.handleKeyUp);
document.body.removeEventListener('click', this.cancelSelection);
this.destroy();
},
methods: {
@@ -450,19 +444,6 @@ export default {
//This section is common to all entry points for annotation display
this.prepareExistingAnnotationSelection(selectedAnnotations);
},
cancelSelection(event) {
if (this.$refs?.plot) {
const clickedInsidePlot = this.$refs.plot.contains(event.target);
const clickedInsideInspector = event.target.closest('.js-inspector') !== null;
const clickedOption = event.target.closest('.js-autocomplete-options') !== null;
if (!clickedInsidePlot && !clickedInsideInspector && !clickedOption) {
this.rectangles = [];
this.annotationSelections = [];
this.selectPlot();
document.body.removeEventListener('click', this.cancelSelection);
}
}
},
waitForAxesToLoad() {
return new Promise(resolve => {
// When there is no plot data, the ranges can be undefined
@@ -1295,8 +1276,6 @@ export default {
}
this.openmct.selection.select(selection, true);
document.body.addEventListener('click', this.cancelSelection);
},
selectNewPlotAnnotations(boundingBoxPerYAxis, pointsInBox, event) {
let targetDomainObjects = {};

View File

@@ -603,20 +603,19 @@ export default {
const mainYAxisId = this.config.yAxis.get('id');
//There has to be at least one yAxis
const yAxisIds = [mainYAxisId].concat(this.config.additionalYAxes.map(yAxis => yAxis.get('id')));
// Repeat drawing for all yAxes
yAxisIds.filter(this.canDraw).forEach((id, yAxisIndex) => {
this.updateViewport(id);
this.drawSeries(id);
if (yAxisIndex === 0) {
yAxisIds.forEach((id) => {
if (this.canDraw(id)) {
this.updateViewport(id);
this.drawSeries(id);
this.drawRectangles(id);
}
this.drawHighlights(id);
this.drawHighlights(id);
// only draw these in fixed time mode or plot is paused
if (this.annotationViewingAndEditingAllowed) {
this.drawAnnotatedPoints(id);
this.drawAnnotationSelections(id);
// only draw these in fixed time mode or plot is paused
if (this.annotationViewingAndEditingAllowed) {
this.drawAnnotatedPoints(id);
this.drawAnnotationSelections(id);
}
}
});
},

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

View File

@@ -1 +0,0 @@
{"openmct":{"foo:a6c9e745-89e5-4c3a-8f54-7a34711aaeb1":{"identifier":{"key":"a6c9e745-89e5-4c3a-8f54-7a34711aaeb1","namespace":"foo"},"name":"Folder Foo","type":"folder","composition":[{"key":"95729018-86ed-4484-867d-10c63c41c5a1","namespace":"foo"},{"key":"22c438f0-953b-42c5-8fb2-9d5dbeb88a0c","namespace":"foo"},{"key":"3545554b-53c8-467d-a70d-e90d1a120e4a","namespace":"foo"}],"modified":1681164966705,"location":"foo:mine","created":1681164829371,"persisted":1681164966706},"foo:95729018-86ed-4484-867d-10c63c41c5a1":{"identifier":{"key":"95729018-86ed-4484-867d-10c63c41c5a1","namespace":"foo"},"name":"Display Layout Bar","type":"layout","composition":[{"key":"22c438f0-953b-42c5-8fb2-9d5dbeb88a0c","namespace":"foo"},{"key":"3545554b-53c8-467d-a70d-e90d1a120e4a","namespace":"foo"}],"configuration":{"items":[{"fill":"#666666","stroke":"","x":42,"y":42,"width":20,"height":4,"type":"box-view","id":"14505a5d-b846-4504-961f-8c9bcdf19f39"},{"identifier":{"key":"22c438f0-953b-42c5-8fb2-9d5dbeb88a0c","namespace":"foo"},"x":0,"y":0,"width":40,"height":15,"displayMode":"all","value":"sin","stroke":"","fill":"","color":"","fontSize":"default","font":"default","type":"telemetry-view","id":"05baa95f-2064-4cb0-ad9f-575758491220"},{"width":40,"height":15,"x":0,"y":15,"identifier":{"key":"3545554b-53c8-467d-a70d-e90d1a120e4a","namespace":"foo"},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"70e1b8b7-cd59-4a52-b796-d68fb0c48fc5"}],"layoutGrid":[10,10],"objectStyles":{"05baa95f-2064-4cb0-ad9f-575758491220":{"staticStyle":{"style":{"border":"1px solid #00ff00","backgroundColor":"#0000ff","color":"#ff00ff"}}}}},"modified":1681165037189,"location":"foo:a6c9e745-89e5-4c3a-8f54-7a34711aaeb1","created":1681164838178,"persisted":1681165037190},"foo:22c438f0-953b-42c5-8fb2-9d5dbeb88a0c":{"identifier":{"key":"22c438f0-953b-42c5-8fb2-9d5dbeb88a0c","namespace":"foo"},"name":"SWG Baz","type":"generator","telemetry":{"period":"20","amplitude":"2","offset":"5","dataRateInHz":1,"phase":0,"randomness":0,"loadDelay":0,"infinityValues":false,"staleness":false},"modified":1681164910719,"location":"foo:a6c9e745-89e5-4c3a-8f54-7a34711aaeb1","created":1681164903684,"persisted":1681164910719},"foo:3545554b-53c8-467d-a70d-e90d1a120e4a":{"identifier":{"key":"3545554b-53c8-467d-a70d-e90d1a120e4a","namespace":"foo"},"name":"Clock Qux","type":"clock","configuration":{"baseFormat":"YYYY/MM/DD hh:mm:ss","use24":"clock12","timezone":"UTC"},"modified":1681164989837,"location":"foo:a6c9e745-89e5-4c3a-8f54-7a34711aaeb1","created":1681164966702,"persisted":1681164989837}},"rootId":"foo:a6c9e745-89e5-4c3a-8f54-7a34711aaeb1"}

View File

@@ -29,7 +29,7 @@ define([
'./TelemetryTableColumn',
'./TelemetryTableUnitColumn',
'./TelemetryTableConfiguration',
'../../utils/staleness'
'@/utils/staleness'
], function (
EventEmitter,
_,

View File

@@ -88,7 +88,7 @@ define([], function () {
}
getContextMenuActions() {
return ['viewDatumAction', 'viewHistoricalData'];
return ['viewDatumAction'];
}
}

View File

@@ -175,22 +175,14 @@ export default {
getDatum() {
return this.row.fullDatum;
},
showContextMenu: async function (event) {
showContextMenu: function (event) {
event.preventDefault();
this.updateViewContext();
this.markRow(event);
const contextualDomainObject = await this.row.getContextualDomainObject?.(this.openmct, this.row.objectKeyString);
let objectPath = this.objectPath;
if (contextualDomainObject) {
objectPath = objectPath.slice();
objectPath.unshift(contextualDomainObject);
}
const actions = this.row.getContextMenuActions().map(key => this.openmct.actions.getAction(key));
const menuItems = this.openmct.menus.actionsToMenuItems(actions, objectPath, this.currentView);
const menuItems = this.openmct.menus.actionsToMenuItems(actions, this.objectPath, this.currentView);
if (menuItems.length) {
this.openmct.menus.showMenu(event.x, event.y, menuItems);
}

View File

@@ -212,15 +212,7 @@ div.c-table {
text-overflow: ellipsis;
}
tbody tr {
&:hover {
background: $colorItemTreeHoverBg;
}
}
td {
user-select: none; // Table supports context-click to display Actions menu, don't allow text selection.
&.is-stale {
@include isStaleElement();
}

View File

@@ -286,7 +286,6 @@ export default {
this.openmct.objectViews.on('clearData', this.clearData);
this.$nextTick(() => {
this.updateStyle(this.styleRuleManager?.currentStyle);
this.getActionCollection();
});
},

View File

@@ -61,7 +61,7 @@
pointer-events: none;
position: absolute;
top: 0; right: 0; bottom: auto; left: 0;
z-index: 10;
z-index: 2;
.c-object-label {
visibility: hidden;
@@ -99,8 +99,6 @@
}
}
}
&.c-so-view--flexible-layout,
&.c-so-view--layout {
// For sub-layouts with hidden frames, completely hide the header to avoid overlapping buttons
> .c-so-view__header {

View File

@@ -21,7 +21,7 @@
*****************************************************************************/
<template>
<div class="c-inspector js-inspector">
<div class="c-inspector">
<object-name />
<InspectorTabs
:selection="selection"

View File

@@ -93,20 +93,9 @@
:persist-position="true"
>
<RecentObjectsList
ref="recentObjectsList"
class="l-shell__tree"
@openAndScrollTo="openAndScrollTo($event)"
@setClearButtonDisabled="setClearButtonDisabled"
/>
<button
slot="controls"
class="c-icon-button icon-clear-data"
aria-label="Clear Recently Viewed"
title="Clear Recently Viewed"
:disabled="disableClearButton"
@click="handleClearRecentObjects"
>
</button>
</pane>
</multipane>
</pane>
@@ -152,19 +141,19 @@
</template>
<script>
import ObjectView from '../components/ObjectView.vue';
import Inspector from '../inspector/Inspector.vue';
import Toolbar from '../toolbar/Toolbar.vue';
import AppLogo from './AppLogo.vue';
import BrowseBar from './BrowseBar.vue';
import CreateButton from './CreateButton.vue';
import RecentObjectsList from './RecentObjectsList.vue';
import MctTree from './mct-tree.vue';
import ObjectView from '../components/ObjectView.vue';
import CreateButton from './CreateButton.vue';
import GrandSearch from './search/GrandSearch.vue';
import multipane from './multipane.vue';
import pane from './pane.vue';
import GrandSearch from './search/GrandSearch.vue';
import BrowseBar from './BrowseBar.vue';
import Toolbar from '../toolbar/Toolbar.vue';
import AppLogo from './AppLogo.vue';
import Indicators from './status-bar/Indicators.vue';
import NotificationBanner from './status-bar/NotificationBanner.vue';
import RecentObjectsList from './RecentObjectsList.vue';
export default {
components: {
@@ -199,8 +188,7 @@ export default {
triggerSync: false,
triggerReset: false,
headExpanded,
isResizing: false,
disableClearButton: false
isResizing: false
};
},
computed: {
@@ -291,19 +279,12 @@ export default {
handleTreeReset() {
this.triggerReset = !this.triggerReset;
},
handleClearRecentObjects() {
this.$refs.recentObjectsList.clearRecentObjects();
},
onStartResizing() {
this.isResizing = true;
},
onEndResizing() {
this.isResizing = false;
},
setClearButtonDisabled(isDisabled) {
this.disableClearButton = isDisabled;
}
}
};
</script>

View File

@@ -181,10 +181,6 @@ export default {
*/
setSavedRecentItems() {
localStorage.setItem(LOCAL_STORAGE_KEY__RECENT_OBJECTS, JSON.stringify(this.recents));
// send event to parent for enabled button
if (this.recents.length === 1) {
this.$emit("setClearButtonDisabled", false);
}
},
/**
* Returns true if the `domainObject` supports composition and we are not already
@@ -195,35 +191,11 @@ export default {
shouldTrackCompositionFor(domainObject, navigationPath) {
return this.compositionCollections[navigationPath] === undefined
&& this.openmct.composition.supportsComposition(domainObject);
},
/**
* Clears the Recent Objects list in localStorage and in the component.
* Before clearing, prompts the user to confirm the action with a dialog.
*/
clearRecentObjects() {
const dialog = this.openmct.overlays.dialog({
title: 'Clear Recently Viewed Objects',
iconClass: 'alert',
message: 'This action will clear the Recently Viewed Objects list. Are you sure you want to continue?',
buttons: [
{
label: 'OK',
callback: () => {
localStorage.removeItem(LOCAL_STORAGE_KEY__RECENT_OBJECTS);
this.recents = [];
dialog.dismiss();
this.$emit("setClearButtonDisabled", true);
}
},
{
label: 'Cancel',
callback: () => {
dialog.dismiss();
}
}
]
});
}
}
};
</script>
<style>
</style>

View File

@@ -355,18 +355,17 @@ export default {
this.abortItemLoad(path);
}
const pathIndex = this.openTreeItems.indexOf(path);
let pathIndex = this.openTreeItems.indexOf(path);
if (pathIndex === -1) {
return;
}
this.treeItems = this.treeItems.filter((item) => {
const otherPath = item.navigationPath;
if (otherPath !== path
&& this.isTreeItemAChildOf(otherPath, path)) {
this.destroyObserverByPath(otherPath);
this.destroyMutableByPath(otherPath);
this.treeItems = this.treeItems.filter((checkItem) => {
if (checkItem.navigationPath !== path
&& checkItem.navigationPath.includes(path)) {
this.destroyObserverByPath(checkItem.navigationPath);
this.destroyMutableByPath(checkItem.navigationPath);
return false;
}
@@ -961,24 +960,6 @@ export default {
isTreeItemPathOpen(path) {
return this.openTreeItems.includes(path);
},
isTreeItemAChildOf(childNavigationPath, parentNavigationPath) {
const childPathKeys = childNavigationPath.split('/');
const parentPathKeys = parentNavigationPath.split('/');
// If child path is shorter than or same length as
// the parent path, then it's not a child.
if (childPathKeys.length <= parentPathKeys.length) {
return false;
}
for (let i = 0; i < parentPathKeys.length; i++) {
if (childPathKeys[i] !== parentPathKeys[i]) {
return false;
}
}
return true;
},
getElementStyleValue(el, style) {
if (!el) {
return;

View File

@@ -49,19 +49,9 @@ export default {
},
computed: {
highlightedText() {
const highlight = this.highlight;
let regex = new RegExp(`(?<!<[^>]*)(${this.highlight})`, 'gi');
const normalCharsRegex = /^[^A-Za-z0-9]+$/g;
const newHighLight = normalCharsRegex.test(highlight)
? `\\${highlight}`
: highlight;
const highlightRegex = new RegExp(`(?<!<[^>]*)(${newHighLight})`, 'gi');
const replacement = `<span class="${this.highlightClass}">${highlight}</span>`;
return this.text.replace(highlightRegex, replacement);
return this.text.replace(regex, `<span class="${this.highlightClass}">${this.highlight}</span>`);
}
}
};