Compare commits
	
		
			4 Commits
		
	
	
		
			v2.2.2
			...
			release/2.
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | eb95b1f50b | ||
|   | c6e913245d | ||
|   | cf7016c814 | ||
|   | 91d0c0be10 | 
| @@ -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 * * *" | ||||
|   | ||||
							
								
								
									
										8
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							| @@ -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" | ||||
|   | ||||
							
								
								
									
										33
									
								
								.github/workflows/e2e-couchdb.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										33
									
								
								.github/workflows/e2e-couchdb.yml
									
									
									
									
										vendored
									
									
								
							| @@ -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 | ||||
|   | ||||
							
								
								
									
										10
									
								
								.github/workflows/e2e-pr.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								.github/workflows/e2e-pr.yml
									
									
									
									
										vendored
									
									
								
							| @@ -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
									
								
							
							
						
						
									
										21
									
								
								.github/workflows/e2e.yml
									
									
									
									
										vendored
									
									
										Normal 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 | ||||
							
								
								
									
										1
									
								
								.github/workflows/pr-platform.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/pr-platform.yml
									
									
									
									
										vendored
									
									
								
							| @@ -16,6 +16,7 @@ jobs: | ||||
|           - macos-latest | ||||
|           - windows-latest | ||||
|         node_version: | ||||
|           - 14 | ||||
|           - 16 | ||||
|           - 18 | ||||
|         architecture: | ||||
|   | ||||
| @@ -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 you’re testing a view, you do not need to test the whole application UI, you can just fetch the view provider using the public API and render the view into an element that you have created.  | ||||
|     * You do not need to test that the view switcher works, there should be separate tests for that.  | ||||
|     * You do not need to test that telemetry providers work, you can mock openmct.telemetry.request() to feed test data to the view. | ||||
|     * Use your best judgement when deciding on appropriate scope. | ||||
| * Automated tests for plugins should start by actually installing the plugin being tested, and then test that installing the plugin adds the desired features and behavior to Open MCT, observing the above rules. | ||||
| * All variables used in a test spec, including any instances of the Open MCT API should be declared inside of an appropriate block scope (not at the root level of the source file), and should be initialized in the relevant beforeEach block. `beforeEach` is preferable to `beforeAll` to avoid leaking of state between tests. | ||||
| * A `afterEach` or `afterAll` should be used to do any clean up necessary to prevent leakage of state between test specs. This can happen when functions on `window` are wrapped, or when the URL is changed. [A convenience function](https://github.com/nasa/openmct/blob/master/src/utils/testing.js#L59) is provided for resetting the URL and clearing builtin spies between tests. | ||||
| * If writing unit tests for legacy Angular code be sure to follow [best practices in order to avoid memory leaks](https://www.thecodecampus.de/blog/avoid-memory-leaks-angularjs-unit-tests/). | ||||
|  | ||||
| #### Examples | ||||
| * [Example of an automated test spec for an object view plugin](https://github.com/nasa/openmct/blob/master/src/plugins/telemetryTable/pluginSpec.js) | ||||
| * [Example of an automated test spec for API](https://github.com/nasa/openmct/blob/master/src/api/time/TimeAPISpec.js) | ||||
|  | ||||
| ### Commit Message Standards | ||||
|  | ||||
| Commit messages should: | ||||
| @@ -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.``` | ||||
|   | ||||
							
								
								
									
										50
									
								
								TESTING.md
									
									
									
									
									
								
							
							
						
						
									
										50
									
								
								TESTING.md
									
									
									
									
									
								
							| @@ -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 you’re testing a view, you do not need to test the whole application UI, you can just fetch the view provider using the public API and render the view into an element that you have created.  | ||||
|     * You do not need to test that the view switcher works, there should be separate tests for that.  | ||||
|     * You do not need to test that telemetry providers work, you can mock openmct.telemetry.request() to feed test data to the view. | ||||
|     * Use your best judgement when deciding on appropriate scope. | ||||
| * Automated tests for plugins should start by actually installing the plugin being tested, and then test that installing the plugin adds the desired features and behavior to Open MCT, observing the above rules. | ||||
| * All variables used in a test spec, including any instances of the Open MCT API should be declared inside of an appropriate block scope (not at the root level of the source file), and should be initialized in the relevant beforeEach block. `beforeEach` is preferable to `beforeAll` to avoid leaking of state between tests. | ||||
| * A `afterEach` or `afterAll` should be used to do any clean up necessary to prevent leakage of state between test specs. This can happen when functions on `window` are wrapped, or when the URL is changed. [A convenience function](https://github.com/nasa/openmct/blob/master/src/utils/testing.js#L59) is provided for resetting the URL and clearing builtin spies between tests. | ||||
|  | ||||
| #### Unit Test Examples | ||||
| * [Example of an automated test spec for an object view plugin](https://github.com/nasa/openmct/blob/master/src/plugins/telemetryTable/pluginSpec.js) | ||||
| * [Example of an automated test spec for API](https://github.com/nasa/openmct/blob/master/src/api/time/TimeAPISpec.js) | ||||
|  | ||||
| #### Unit Testing Execution | ||||
|  | ||||
| The unit tests can be executed in one of two ways: | ||||
| `npm run test` which runs the entire suite against headless chrome | ||||
| `npm run test:debug` for debugging the tests in realtime in an active chrome session. | ||||
|  | ||||
| ## e2e, performance, and visual testing | ||||
| Documentation located [here](./e2e/README.md) | ||||
|  | ||||
| ## Code Coverage | ||||
|  | ||||
| * 100% statement coverage is achievable and desirable. | ||||
|  | ||||
| Codecov.io will combine each of the above commands with [Codecov.io Flags](https://docs.codecov.com/docs/flags). Effectively, this allows us to combine multiple reports which are run at various stages of our CI Pipeline or run as part of a parallel process. | ||||
|  | ||||
| This e2e coverage is combined with our unit test report to give a comprehensive (if flawed) view of line coverage. | ||||
|  | ||||
| ### Limitations in our code coverage reporting | ||||
|  | ||||
| Our code coverage implementation has two known limitations: | ||||
| - [Variability and accuracy](https://github.com/nasa/openmct/issues/5811) | ||||
| - [Vue instrumentation](https://github.com/nasa/openmct/issues/4973) | ||||
							
								
								
									
										10
									
								
								codecov.yml
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								codecov.yml
									
									
									
									
									
								
							| @@ -15,14 +15,14 @@ coverage: | ||||
|  | ||||
| flags: | ||||
|   unit: | ||||
|     carryforward: 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 | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -170,6 +170,5 @@ exports.test = base.test.extend({ | ||||
|         } | ||||
|     } | ||||
| }); | ||||
|  | ||||
| exports.expect = expect; | ||||
| exports.waitForAnimations = waitForAnimations; | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|  | ||||
| /** | ||||
|   | ||||
| @@ -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(); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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'] | ||||
|     ] | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -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; | ||||
| }; | ||||
|   | ||||
| @@ -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")'), | ||||
|   | ||||
| @@ -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(); | ||||
|   | ||||
| @@ -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', () => { | ||||
|   | ||||
| @@ -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'; | ||||
|  | ||||
|   | ||||
| @@ -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'); | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|  */ | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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); | ||||
|     }); | ||||
| }); | ||||
|   | ||||
| @@ -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(); | ||||
|     } | ||||
| }); | ||||
|   | ||||
| @@ -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" | ||||
|   | ||||
| @@ -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})`); | ||||
|     }); | ||||
| }); | ||||
| @@ -33,8 +33,6 @@ export default function (staticFaults = false) { | ||||
|                 return Promise.resolve(faultsData); | ||||
|             }, | ||||
|             subscribe(domainObject, callback) { | ||||
|                 callback({ type: 'global-alarm-status' }); | ||||
|  | ||||
|                 return () => {}; | ||||
|             }, | ||||
|             supportsRequest(domainObject) { | ||||
|   | ||||
							
								
								
									
										40
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										40
									
								
								package.json
									
									
									
									
									
								
							| @@ -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" | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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": "" | ||||
|  *         } | ||||
|  *     } | ||||
|  * } | ||||
|  */ | ||||
|   | ||||
| @@ -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" | ||||
|     > | ||||
|   | ||||
| @@ -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 }} | ||||
|   | ||||
| @@ -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 = []; | ||||
|   | ||||
| @@ -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); | ||||
|                 }); | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -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'}` | ||||
|             }; | ||||
|         }, | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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) { | ||||
|   | ||||
| @@ -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(); | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|   | ||||
| @@ -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); | ||||
|     } | ||||
| } | ||||
| @@ -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(); | ||||
|   | ||||
| @@ -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); | ||||
|         } | ||||
|   | ||||
| @@ -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: { | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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 = {}; | ||||
|   | ||||
| @@ -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); | ||||
|                     } | ||||
|                 } | ||||
|             }); | ||||
|         }, | ||||
|   | ||||
| @@ -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; | ||||
|     } | ||||
|   | ||||
| @@ -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'); | ||||
|         }); | ||||
|  | ||||
|     }); | ||||
| }); | ||||
|  | ||||
|   | ||||
| @@ -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"} | ||||
| @@ -29,7 +29,7 @@ define([ | ||||
|     './TelemetryTableColumn', | ||||
|     './TelemetryTableUnitColumn', | ||||
|     './TelemetryTableConfiguration', | ||||
|     '../../utils/staleness' | ||||
|     '@/utils/staleness' | ||||
| ], function ( | ||||
|     EventEmitter, | ||||
|     _, | ||||
|   | ||||
| @@ -88,7 +88,7 @@ define([], function () { | ||||
|         } | ||||
|  | ||||
|         getContextMenuActions() { | ||||
|             return ['viewDatumAction', 'viewHistoricalData']; | ||||
|             return ['viewDatumAction']; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -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); | ||||
|             } | ||||
|   | ||||
| @@ -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(); | ||||
|         } | ||||
|   | ||||
| @@ -286,7 +286,6 @@ export default { | ||||
|             this.openmct.objectViews.on('clearData', this.clearData); | ||||
|  | ||||
|             this.$nextTick(() => { | ||||
|                 this.updateStyle(this.styleRuleManager?.currentStyle); | ||||
|                 this.getActionCollection(); | ||||
|             }); | ||||
|         }, | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -21,7 +21,7 @@ | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| <template> | ||||
| <div class="c-inspector js-inspector"> | ||||
| <div class="c-inspector"> | ||||
|     <object-name /> | ||||
|     <InspectorTabs | ||||
|         :selection="selection" | ||||
|   | ||||
| @@ -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> | ||||
|   | ||||
| @@ -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> | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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>`); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user