Compare commits
	
		
			151 Commits
		
	
	
		
			emit-click
			...
			mct5526-pr
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | fe812c5f23 | ||
|   | d85be3b88e | ||
|   | c407cb0a3b | ||
|   | 4d48cf3180 | ||
|   | 413cb13edf | ||
|   | 70115be727 | ||
|   | 97f5528dfc | ||
|   | 0e1cc5dc30 | ||
|   | 0062191416 | ||
|   | eedc523078 | ||
|   | db97acb61e | ||
|   | 43a4bf9606 | ||
|   | 0f352087f5 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 8ce15521de | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | c0b0c44dc2 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | b9a644cd4f | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 1afc5ef245 | ||
|   | 34442c53c6 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 451ca075fe | ||
|   | 84f1a61a8d | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | ea041aaaf9 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | ac9420bfa1 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 0ebab10578 | ||
|   | cbdb9fc437 | ||
|   | 63d2246345 | ||
|   | 78002f0a24 | ||
|   | f08fd58486 | ||
|   | 730272e165 | ||
|   | 0f0c6a7b17 | ||
|   | 370e6a0c37 | ||
|   | 815506cf17 | ||
|   | bdb1867c73 | ||
|   | e288fdffea | ||
|   | 194060f30a | ||
|   | 45bc317a59 | ||
|   | e103ea44d8 | ||
|   | d13d7dc8f3 | ||
|   | 05e3303828 | ||
|   | aa0fc70e54 | ||
|   | 9fbb695379 | ||
|   | 584d11a2ef | ||
|   | 162cc6bc77 | ||
|   | 111b0d0d68 | ||
|   | 59c0da1b57 | ||
|   | 3c70cf1767 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 2aec1ee854 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | ab60e3c3bd | ||
|   | 4445d7116a | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 93abc18001 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 7fb37de721 | ||
|   | 1c525f50c8 | ||
|   | 40a7451064 | ||
|   | 04ee6f49d6 | ||
|   | f5796c984e | ||
|   | 50b642fabe | ||
|   | dfb726b924 | ||
|   | 8d761f729b | ||
|   | d88ead502c | ||
|   | c46849b166 | ||
|   | 6c71fa01f5 | ||
|   | c56d458ecb | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | f74a35f45a | ||
|   | d9ac0182c3 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 7bb108c36b | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 77804cff75 | ||
|   | 2d73296b36 | ||
|   | 405418b9d5 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | f999b9e12b | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 664ba399ea | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | c6078a234a | ||
|   | 17c16eba50 | ||
|   | 9f9c69ee68 | ||
|   | 037886aa01 | ||
|   | 48916564e4 | ||
|   | 1ca5271c3e | ||
|   | 6521b888d6 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 85fce3c456 | ||
|   | 8d577a8958 | ||
|   | 9c8ee09960 | ||
|   | 9568da9d5f | ||
|   | 2aa3b810ba | ||
|   | 1cdbb34e21 | ||
|   | 95299336d0 | ||
|   | b8ff5c7f33 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 9ede023cfa | ||
|   | 308e621b5d | ||
|   | e6b5870234 | ||
|   | 03e7d912be | ||
|   | 09da373d1c | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | b8d9e41c01 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 815e7d169c | ||
|   | 58387e0902 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 0a0826f87e | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | e063442d8c | ||
|   | 6a5823ab5c | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 0493e5ae3c | ||
|   | 24f13b6249 | ||
|   | 221fb4d6bf | ||
|   | 257742b45b | ||
|   | 44edec4f04 | ||
|   | ab4d0dd37f | ||
|   | c089a4760d | ||
|   | b77a4066f2 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 20d7e80502 | ||
|   | d63fec51a7 | ||
|   | d30c4fcb53 | ||
|   | fff3ce0acf | ||
|   | db5cb2517f | ||
|   | 5236f1c796 | ||
|   | 1ed253cb07 | ||
|   | a6553ba010 | ||
|   | cf6bc5be2d | ||
|   | a53a3a0297 | ||
|   | 402cd15726 | ||
|   | a5580912e3 | ||
|   | 54d1b8991c | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 7b6acee793 | ||
|   | 04e1c60e5c | ||
|   | 91bcd78d40 | ||
|   | a3c0e073c8 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 21ae9f45c1 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 0a40c8dd0b | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | ef1ea8e712 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 5c4fad77ff | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | dbac9e6cd2 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 4b7bcf9c89 | ||
|   | 2b42abd495 | ||
|   | 1f2102b845 | ||
|   | 2ccb90aa41 | ||
|   | 525496fbca | ||
|   | 47099786cb | ||
|   | 3a11291a3b | ||
|   | 476f1b2579 | ||
|   | 6153ad8e1e | ||
|   | 77c0b16050 | ||
|   | d19088cec6 | ||
|   | 43afb39e56 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | cd8c332fb5 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | b899475939 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | cc1f7659f9 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 0d5539be96 | ||
|   | 0a511e6155 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 47b6d19de8 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 3fd93f47bc | ||
|   | 651e61954c | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | d30ec4c757 | ||
|   | 3c24733476 | ||
|   | 04d00fac3d | ||
|   | 150909d4b9 | ||
|   | 2b599a7ff4 | ||
|   | 824a597825 | 
| @@ -2,7 +2,7 @@ version: 2.1 | ||||
| executors: | ||||
|   pw-focal-development: | ||||
|     docker: | ||||
|       - image: mcr.microsoft.com/playwright:v1.19.1-focal | ||||
|       - image: mcr.microsoft.com/playwright:v1.23.0-focal | ||||
|     environment: | ||||
|       NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed | ||||
| parameters: | ||||
| @@ -12,7 +12,7 @@ parameters: | ||||
|     type: boolean | ||||
| commands: | ||||
|   build_and_install: | ||||
|     description: "All steps used to build and install. Will not work on node10" | ||||
|     description: "All steps used to build and install. Will use cache if found" | ||||
|     parameters: | ||||
|       node-version: | ||||
|         type: string | ||||
| @@ -23,7 +23,7 @@ commands: | ||||
|       - node/install: | ||||
|           install-npm: true | ||||
|           node-version: << parameters.node-version >> | ||||
|       - run: npm install | ||||
|       - 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: | ||||
| @@ -31,7 +31,7 @@ commands: | ||||
|         type: string | ||||
|     steps: | ||||
|       - when: | ||||
|           condition:  | ||||
|           condition: | ||||
|             equal: [false, << pipeline.parameters.BUST_CACHE >> ] | ||||
|           steps: | ||||
|             - restore_cache: | ||||
| @@ -41,7 +41,7 @@ commands: | ||||
|     parameters: | ||||
|       node-version: | ||||
|         type: string | ||||
|     steps:     | ||||
|     steps: | ||||
|       - save_cache: | ||||
|           key: deps-{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}-{{ checksum ".circleci/config.yml" }} | ||||
|           paths: | ||||
| @@ -58,13 +58,17 @@ commands: | ||||
|           ls -latR >> /tmp/artifacts/dir.txt | ||||
|       - store_artifacts: | ||||
|           path: /tmp/artifacts/ | ||||
|   upload_code_covio: | ||||
|     description: "Command to upload code coverage reports to codecov.io" | ||||
|     steps: | ||||
|         - run: curl -Os https://uploader.codecov.io/latest/linux/codecov;chmod +x codecov;./codecov  | ||||
|   generate_e2e_code_cov_report: | ||||
|    description: "Generate e2e code coverage artifacts and publish to codecov.io. Needed to that we can ignore the exit code status of the npm run test" | ||||
|    parameters: | ||||
|       suite: | ||||
|         type: string | ||||
|    steps: | ||||
|     - run: npm run cov:e2e:report  | ||||
|     - run: npm run cov:e2e:<<parameters.suite>>:publish        | ||||
| orbs: | ||||
|   node: circleci/node@4.9.0 | ||||
|   browser-tools: circleci/browser-tools@1.2.3 | ||||
|   browser-tools: circleci/browser-tools@1.3.0 | ||||
| jobs: | ||||
|   npm-audit: | ||||
|     parameters: | ||||
| @@ -101,7 +105,7 @@ jobs: | ||||
|             equal: [ "FirefoxESR", <<parameters.browser>> ] | ||||
|           steps: | ||||
|             - browser-tools/install-firefox: | ||||
|                 version: "91.4.0esr" #https://archive.mozilla.org/pub/firefox/releases/           | ||||
|                 version: "91.7.1esr" #https://archive.mozilla.org/pub/firefox/releases/ | ||||
|       - when: | ||||
|           condition: | ||||
|             equal: [ "FirefoxHeadless", <<parameters.browser>> ] | ||||
| @@ -114,12 +118,13 @@ jobs: | ||||
|             - browser-tools/install-chrome: | ||||
|                 replace-existing: false | ||||
|       - run: npm run test -- --browsers=<<parameters.browser>> | ||||
|       - run: npm run cov:unit:publish | ||||
|       - save_cache_cmd: | ||||
|           node-version: <<parameters.node-version>> | ||||
|       - store_test_results: | ||||
|           path: dist/reports/tests/ | ||||
|       - store_artifacts: | ||||
|           path: dist/reports/ | ||||
|           path: coverage | ||||
|       - generate_and_store_version_and_filesystem_artifacts | ||||
|   e2e-test: | ||||
|     parameters: | ||||
| @@ -128,49 +133,69 @@ jobs: | ||||
|       suite: | ||||
|         type: string | ||||
|     executor: pw-focal-development | ||||
|     parallelism: 4 | ||||
|     steps: | ||||
|       - build_and_install: | ||||
|           node-version: <<parameters.node-version>> | ||||
|       - run: npx playwright install | ||||
|       - run: npm run test:e2e:<<parameters.suite>> | ||||
|       - when: #Only install chrome-beta when running the full suite to save $$$ | ||||
|           condition: | ||||
|             equal: [ "full", <<parameters.suite>> ] | ||||
|           steps: | ||||
|             - run: npx playwright install chrome-beta | ||||
|       - run: SHARD="$((${CIRCLE_NODE_INDEX}+1))"; npm run test:e2e:<<parameters.suite>> -- --shard=${SHARD}/${CIRCLE_NODE_TOTAL} | ||||
|       - generate_e2e_code_cov_report: | ||||
|          suite: <<parameters.suite>>           | ||||
|       - store_test_results: | ||||
|           path: test-results/results.xml | ||||
|       - store_artifacts: | ||||
|           path: test-results | ||||
|       - store_artifacts: | ||||
|           path: coverage | ||||
|       - store_artifacts: | ||||
|           path: html-test-results | ||||
|       - generate_and_store_version_and_filesystem_artifacts | ||||
|   perf-test: | ||||
|     parameters: | ||||
|       node-version: | ||||
|         type: string | ||||
|     executor: pw-focal-development | ||||
|     steps: | ||||
|       - build_and_install: | ||||
|           node-version: <<parameters.node-version>> | ||||
|       - run: npm run test:perf | ||||
|       - store_test_results: | ||||
|           path: test-results/results.xml | ||||
|       - store_artifacts: | ||||
|           path: test-results | ||||
|       - store_artifacts: | ||||
|           path: html-test-results | ||||
|       - generate_and_store_version_and_filesystem_artifacts | ||||
| workflows: | ||||
|   overall-circleci-commit-status: #These jobs run on every commit | ||||
|     jobs: | ||||
|       - lint: | ||||
|           node-version: lts/gallium | ||||
|       - unit-test: | ||||
|           name: node12-chrome | ||||
|           node-version: lts/erbium | ||||
|           browser: ChromeHeadless | ||||
|       - unit-test:  | ||||
|           name: node14-chrome | ||||
|           name: node14-lint | ||||
|           node-version: lts/fermium | ||||
|           browser: ChromeHeadless | ||||
|           post-steps: | ||||
|             - upload_code_covio   | ||||
|       - unit-test: | ||||
|           name: node16-chrome | ||||
|           node-version: lts/gallium | ||||
|           browser: ChromeHeadless              | ||||
|           browser: ChromeHeadless | ||||
|       - unit-test: | ||||
|           name: node18-chrome | ||||
|           node-version: "18" | ||||
|           browser: ChromeHeadless | ||||
|       - e2e-test: | ||||
|           name: e2e-ci | ||||
|           node-version: lts/gallium | ||||
|           suite: ci | ||||
|       - perf-test: | ||||
|           node-version: lts/gallium | ||||
|   the-nightly: #These jobs do not run on PRs, but against master at night | ||||
|     jobs: | ||||
|       - unit-test: | ||||
|           name: node12-firefoxESR-nightly | ||||
|           node-version: lts/erbium | ||||
|           name: node16-firefoxESR-nightly | ||||
|           node-version: lts/gallium | ||||
|           browser: FirefoxESR | ||||
|       - unit-test: | ||||
|           name: node12-chrome-nightly | ||||
|           node-version: lts/erbium | ||||
|           browser: ChromeHeadless | ||||
|       - unit-test: | ||||
|           name: node14-firefox-nightly | ||||
|           node-version: lts/fermium | ||||
| @@ -183,6 +208,10 @@ workflows: | ||||
|           name: node16-chrome-nightly | ||||
|           node-version: lts/gallium | ||||
|           browser: ChromeHeadless | ||||
|       - unit-test: | ||||
|           name: node18-chrome | ||||
|           node-version: "18" | ||||
|           browser: ChromeHeadless | ||||
|       - npm-audit: | ||||
|           node-version: lts/gallium | ||||
|       - e2e-test: | ||||
|   | ||||
| @@ -29,6 +29,7 @@ module.exports = { | ||||
|         "you-dont-need-lodash-underscore/omit": "off", | ||||
|         "you-dont-need-lodash-underscore/throttle": "off", | ||||
|         "you-dont-need-lodash-underscore/flatten": "off", | ||||
|         "you-dont-need-lodash-underscore/get": "off", | ||||
|         "no-bitwise": "error", | ||||
|         "curly": "error", | ||||
|         "eqeqeq": "error", | ||||
|   | ||||
							
								
								
									
										4
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
								
							| @@ -27,7 +27,7 @@ assignees: '' | ||||
|  | ||||
| #### Environment | ||||
| <!--- If encountered on local machine, execute the following: | ||||
| <!--- npx envinfo --system --browsers --npmPackages --binaries --languages --markdown --> | ||||
| <!--- npx envinfo --system --browsers --npmPackages --binaries --markdown --> | ||||
| * Open MCT Version: <!--- date of build, version, or SHA --> | ||||
| * Deployment Type: <!--- npm dev? VIPER Dev? openmct-yamcs? --> | ||||
| * OS: | ||||
| @@ -40,6 +40,8 @@ assignees: '' | ||||
| - [ ] Is there a workaround available? | ||||
| - [ ] Does this impact a critical component? | ||||
| - [ ] Is this just a visual bug with no functional impact? | ||||
| - [ ] Does this block the execution of e2e tests? | ||||
| - [ ] Does this have an impact on Performance? | ||||
|  | ||||
| #### Additional Information | ||||
| <!--- Include any screenshots, gifs, or logs which will expedite triage --> | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/PULL_REQUEST_TEMPLATE.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/PULL_REQUEST_TEMPLATE.md
									
									
									
									
										vendored
									
									
								
							| @@ -16,7 +16,7 @@ Closes <!--- Insert Issue Number(s) this PR addresses. Start by typing # will op | ||||
| * [ ] Unit tests included and/or updated with changes? | ||||
| * [ ] Command line build passes? | ||||
| * [ ] Has this been smoke tested? | ||||
| * [ ] Testing instructions included in associated issue? | ||||
| * [ ] Testing instructions included in associated issue OR is this a dependency/testcase change? | ||||
|  | ||||
| ### Reviewer Checklist | ||||
|  | ||||
|   | ||||
							
								
								
									
										6
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							| @@ -32,12 +32,12 @@ jobs: | ||||
|  | ||||
|     # Initializes the CodeQL tools for scanning. | ||||
|     - name: Initialize CodeQL | ||||
|       uses: github/codeql-action/init@v1 | ||||
|       uses: github/codeql-action/init@v2 | ||||
|       with: | ||||
|         languages: javascript | ||||
|  | ||||
|     - name: Autobuild | ||||
|       uses: github/codeql-action/autobuild@v1 | ||||
|       uses: github/codeql-action/autobuild@v2 | ||||
|  | ||||
|     - name: Perform CodeQL Analysis | ||||
|       uses: github/codeql-action/analyze@v1 | ||||
|       uses: github/codeql-action/analyze@v2 | ||||
|   | ||||
							
								
								
									
										5
									
								
								.github/workflows/e2e-pr.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								.github/workflows/e2e-pr.yml
									
									
									
									
										vendored
									
									
								
							| @@ -30,11 +30,12 @@ jobs: | ||||
|       - uses: actions/setup-node@v3 | ||||
|         with: | ||||
|           node-version: '16' | ||||
|       - run: npx playwright@1.19.2 install | ||||
|       - run: npx playwright@1.23.0 install | ||||
|       - run: npx playwright install chrome-beta | ||||
|       - run: npm install | ||||
|       - run: npm run test:e2e:full | ||||
|       - name: Archive test results | ||||
|         uses: actions/upload-artifact@v2 | ||||
|         uses: actions/upload-artifact@v3 | ||||
|         with: | ||||
|           path: test-results | ||||
|       - name: Test success | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/workflows/e2e-visual.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/e2e-visual.yml
									
									
									
									
										vendored
									
									
								
							| @@ -17,7 +17,7 @@ jobs: | ||||
|       - uses: actions/setup-node@v3 | ||||
|         with: | ||||
|           node-version: '16' | ||||
|       - run: npx playwright@1.19.2 install | ||||
|       - run: npx playwright@1.23.0 install | ||||
|       - run: npm install | ||||
|       - name: Run the e2e visual tests | ||||
|         run: npm run test:e2e:visual | ||||
|   | ||||
							
								
								
									
										14
									
								
								.github/workflows/lighthouse.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										14
									
								
								.github/workflows/lighthouse.yml
									
									
									
									
										vendored
									
									
								
							| @@ -9,8 +9,6 @@ on: | ||||
|   pull_request: | ||||
|     types:  | ||||
|       - labeled | ||||
|   schedule: | ||||
|     - cron: '28 21 * * 1-5' | ||||
| jobs: | ||||
|   lighthouse-pr: | ||||
|     if: ${{ github.event.label.name == 'pr:lighthouse' }} | ||||
| @@ -20,10 +18,10 @@ jobs: | ||||
|         uses: actions/checkout@v3 | ||||
|         with: | ||||
|           ref: master #explicitly checkout master for baseline | ||||
|       - name: Install Node 14 | ||||
|       - name: Install Node 16 | ||||
|         uses: actions/setup-node@v3 | ||||
|         with: | ||||
|           node-version: '14' | ||||
|           node-version: '16' | ||||
|       - name: Cache node modules | ||||
|         uses: actions/cache@v2 | ||||
|         env: | ||||
| @@ -54,10 +52,10 @@ jobs: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: actions/checkout@v3 | ||||
|       - name: Install Node 14 | ||||
|       - name: Install Node 16 | ||||
|         uses: actions/setup-node@v3 | ||||
|         with: | ||||
|           node-version: '14' | ||||
|           node-version: '16' | ||||
|       - name: Cache node modules | ||||
|         uses: actions/cache@v2 | ||||
|         env: | ||||
| @@ -83,9 +81,9 @@ jobs: | ||||
|       - name: Install Node 14 | ||||
|         uses: actions/setup-node@v3 | ||||
|         with: | ||||
|           node-version: '14' | ||||
|           node-version: '16' | ||||
|       - name: Cache node modules | ||||
|         uses: actions/cache@v2 | ||||
|         uses: actions/cache@v3 | ||||
|         env: | ||||
|           cache-name: cache-node-modules | ||||
|         with: | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/workflows/pr-platform.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/pr-platform.yml
									
									
									
									
										vendored
									
									
								
							| @@ -16,9 +16,9 @@ jobs: | ||||
|           - macos-latest | ||||
|           - windows-latest | ||||
|         node_version: | ||||
|           - 12 | ||||
|           - 14 | ||||
|           - 16 | ||||
|           - 18 | ||||
|         architecture: | ||||
|           - x64 | ||||
|     name: Node ${{ matrix.node_version }} - ${{ matrix.architecture }} on ${{ matrix.os }} | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/workflows/prcop-config.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/prcop-config.json
									
									
									
									
										vendored
									
									
								
							| @@ -3,7 +3,7 @@ | ||||
|     { | ||||
|       "name": "descriptionRegexp", | ||||
|       "config": { | ||||
|         "regexp": "x] Testing instructions", | ||||
|         "regexp": "[x|X]] Testing instructions", | ||||
|         "errorMessage": ":police_officer: PR Description does not confirm that associated issue(s) contain Testing instructions" | ||||
|       } | ||||
|     }, | ||||
|   | ||||
							
								
								
									
										22
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										22
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -15,8 +15,6 @@ | ||||
| *.idea | ||||
| *.iml | ||||
|  | ||||
| # External dependencies | ||||
|  | ||||
| # Build output | ||||
| target | ||||
| dist | ||||
| @@ -24,30 +22,24 @@ dist | ||||
| # Mac OS X Finder | ||||
| .DS_Store | ||||
|  | ||||
| # Closed source libraries | ||||
| closed-lib | ||||
|  | ||||
| # Node, Bower dependencies | ||||
| node_modules | ||||
| bower_components | ||||
|  | ||||
| # Protractor logs | ||||
| protractor/logs | ||||
|  | ||||
| # npm-debug log | ||||
| npm-debug.log | ||||
|  | ||||
| # karma reports | ||||
| report.*.json | ||||
|  | ||||
| # Lighthouse reports | ||||
| .lighthouseci | ||||
|  | ||||
| # e2e test artifacts | ||||
| test-results | ||||
| allure-results | ||||
| html-test-results | ||||
|  | ||||
| package-lock.json | ||||
|  | ||||
| #codecov artifacts | ||||
| # codecov artifacts | ||||
| .nyc_output | ||||
| coverage | ||||
| codecov | ||||
|  | ||||
| # :( | ||||
| package-lock.json | ||||
|   | ||||
							
								
								
									
										34
									
								
								app.js
									
									
									
									
									
								
							
							
						
						
									
										34
									
								
								app.js
									
									
									
									
									
								
							| @@ -12,6 +12,7 @@ const express = require('express'); | ||||
| const app = express(); | ||||
| const fs = require('fs'); | ||||
| const request = require('request'); | ||||
| const __DEV__ = !process.env.NODE_ENV || process.env.NODE_ENV === 'development'; | ||||
|  | ||||
| // Defaults | ||||
| options.port = options.port || options.p || 8080; | ||||
| @@ -49,14 +50,18 @@ class WatchRunPlugin { | ||||
| } | ||||
|  | ||||
| const webpack = require('webpack'); | ||||
| const webpackConfig = require('./webpack.dev.js'); | ||||
| webpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin()); | ||||
| webpackConfig.plugins.push(new WatchRunPlugin()); | ||||
|  | ||||
| webpackConfig.entry.openmct = [ | ||||
|     'webpack-hot-middleware/client?reload=true', | ||||
|     webpackConfig.entry.openmct | ||||
| ]; | ||||
| let webpackConfig; | ||||
| if (__DEV__) { | ||||
|     webpackConfig = require('./webpack.dev'); | ||||
|     webpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin()); | ||||
|     webpackConfig.entry.openmct = [ | ||||
|         'webpack-hot-middleware/client?reload=true', | ||||
|         webpackConfig.entry.openmct | ||||
|     ]; | ||||
|     webpackConfig.plugins.push(new WatchRunPlugin()); | ||||
| } else { | ||||
|     webpackConfig = require('./webpack.coverage'); | ||||
| } | ||||
|  | ||||
| const compiler = webpack(webpackConfig); | ||||
|  | ||||
| @@ -64,14 +69,16 @@ app.use(require('webpack-dev-middleware')( | ||||
|     compiler, | ||||
|     { | ||||
|         publicPath: '/dist', | ||||
|         logLevel: 'warn' | ||||
|         stats: 'errors-warnings' | ||||
|     } | ||||
| )); | ||||
|  | ||||
| app.use(require('webpack-hot-middleware')( | ||||
|     compiler, | ||||
|     {} | ||||
| )); | ||||
| if (__DEV__) { | ||||
|     app.use(require('webpack-hot-middleware')( | ||||
|         compiler, | ||||
|         {} | ||||
|     )); | ||||
| } | ||||
|  | ||||
| // Expose index.html for development users. | ||||
| app.get('/', function (req, res) { | ||||
| @@ -82,3 +89,4 @@ app.get('/', function (req, res) { | ||||
| app.listen(options.port, options.host, function () { | ||||
|     console.log('Open MCT application running at %s:%s', options.host, options.port); | ||||
| }); | ||||
|  | ||||
|   | ||||
							
								
								
									
										17
									
								
								codecov.yml
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								codecov.yml
									
									
									
									
									
								
							| @@ -13,17 +13,16 @@ coverage: | ||||
|   round: down | ||||
|   range: "66...100" | ||||
|  | ||||
| ignore: | ||||
|  | ||||
| parsers: | ||||
|   gcov: | ||||
|     branch_detection: | ||||
|       conditional: true | ||||
|       loop: true | ||||
|       method: false | ||||
|       macro: false | ||||
| flags: | ||||
|   unit: | ||||
|     carryforward: true  | ||||
|   e2e-ci: | ||||
|     carryforward: true | ||||
|   e2e-full: | ||||
|     carryforward: true     | ||||
|  | ||||
| comment: | ||||
|   layout: "reach,diff,flags,files,footer" | ||||
|   behavior: default | ||||
|   require_changes: false | ||||
|   show_carryforward_flags: true | ||||
| @@ -1,4 +1,12 @@ | ||||
| /* eslint-disable no-undef */ | ||||
| module.exports = { | ||||
|     "extends": ["plugin:playwright/playwright-test"] | ||||
|     "extends": ["plugin:playwright/playwright-test"], | ||||
|     "overrides": [ | ||||
|         { | ||||
|             "files": ["tests/visual/*.spec.js"], | ||||
|             "rules": { | ||||
|                 "playwright/no-wait-for-timeout": "off" | ||||
|             } | ||||
|         } | ||||
|     ] | ||||
| }; | ||||
|   | ||||
							
								
								
									
										122
									
								
								e2e/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								e2e/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,122 @@ | ||||
| # e2e testing | ||||
|  | ||||
| This document captures information specific to the e2e testing of Open MCT. For general information about testing, please see [the Open MCT README](https://github.com/nasa/openmct/blob/master/README.md#tests). | ||||
|  | ||||
| ## Overview | ||||
|  | ||||
| This document is designed to capture on the What, Why, and How's of writing and running e2e tests in Open MCT. | ||||
|  | ||||
| ### About e2e testing | ||||
|  | ||||
| e2e testing is an industry-standard approach to automating the testing of web-based UIs such as Open MCT. Broadly speaking, e2e tests differentiate themselves from unit tests by preferring replication of real user interactions over execution of raw JavaScript functions. | ||||
|  | ||||
| Historically, the abstraction necessary to replicate real user behavior meant that: | ||||
|  | ||||
| - e2e tests were "expensive" due to how much code each test executed. The closer a test replicates the user, the more code is needed run during test execution. Unit tests could run smaller units of code more effeciently. | ||||
| - e2e tests were flaky due to network conditions or the underlying protocols associated with testing a browser. | ||||
| - e2e frameworks relied on a browser communication standard which lacked the observability and controls necessary needed to reach the code paths possible with unit and integration tests. | ||||
| - e2e frameworks provided insufficient debug information on test failure | ||||
|  | ||||
| However, as the web ecosystem has matured to the point where mission-critical UIs can be written for the web (Open MCT), the e2e testing tools have matured as well. There are now fewer "trade-offs" when choosing to write an e2e test over any other type of test. | ||||
|  | ||||
| Modern e2e frameworks: | ||||
|  | ||||
| - Bypass the surface layer of the web-application-under-test and use a raw debugging protocol to observe and control application and browser state. | ||||
| - These new browser-internal protocols enable near-instant, bi-directional communication between test code and the browser, speeding up test execution and making the tests as reliable as the application itself. | ||||
| - Provide test debug tooling which enables developers to pinpoint failure | ||||
|  | ||||
| Furthermore, the abstraction necessary to run e2e tests as a user enables them to be extended to run within a variety of contexts. This matches the extensible design of Open MCT.  | ||||
|  | ||||
| A single e2e test in Open MCT is extended to run: | ||||
|  | ||||
| - Against a matrix of browser versions. | ||||
| - Against a matrix of OS platforms. | ||||
| - Against a local development version of Open MCT. | ||||
| - A version of Open MCT loaded as a dependency (VIPER, VISTA, etc) | ||||
| - Against a variety of data sources or telemetry endpoints. | ||||
|  | ||||
| ### Why Playwright? | ||||
|  | ||||
| [Playwright](https://playwright.dev/) was chosen as our e2e framework because it solves a few VIPER Mission needs: | ||||
| 1. First-class support for Automated Performance Testing | ||||
| 2. Official Chrome, Chrome Canary, and iPad Capabilities | ||||
| 3. Support for Browserless.io | ||||
| 4. Ability to generate code coverage reports | ||||
|  | ||||
| ## Getting Started | ||||
|  | ||||
| ### Getting started with Playwright | ||||
|  | ||||
| ### Getting started with Open MCT's implementation of Playwright | ||||
|  | ||||
| ## Types of Testing | ||||
|  | ||||
| ### (TBD) Visual Testing | ||||
|  | ||||
| - Visual tests leverage [Percy](https://percy.io/). | ||||
| - Visual tests should be written within the `./tests/visual` folder so that they can be ignored during git clones to avoid leaking credentials when executing percy cli | ||||
|  | ||||
| #### (TBD) How to write a good visual test | ||||
|  | ||||
| ### (TBD) Snapshot Testing | ||||
|  | ||||
| <https://playwright.dev/docs/test-snapshots> | ||||
|  | ||||
| ### (TBD) Mobile Testing | ||||
|  | ||||
| ### (TBD) Performance Testing | ||||
|  | ||||
| ### (FUTURE) Component Testing | ||||
|  | ||||
| - Component testing is currrently possible in Playwright but not enabled on this project. For more, please see: <https://playwright.dev/docs/test-components> | ||||
|  | ||||
| ## Architecture, Test Design and Best Practices | ||||
|  | ||||
| ### (TBD) Architecture | ||||
|  | ||||
| #### (TBD)  Continuous Integration | ||||
|  | ||||
| - Test maturation | ||||
| - Difference between full and e2e-ci suites | ||||
| - Platforms | ||||
|  | ||||
| ### (TBD) Multi-browser and Multi-operating system | ||||
|  | ||||
| - Where is it tested | ||||
| - What's supported | ||||
|  | ||||
| ### (TBD) Test Design | ||||
|  | ||||
| - Re-usable tests for VISTA, VIPER, etc. | ||||
|  | ||||
| #### Annotations | ||||
|  | ||||
| - Annotations are a great way of organizing tests outside of a file structure. | ||||
| - Current list of annotations: | ||||
|   - `@ipad` - Mobile execution possible with Playwright's iPad support. | ||||
|   - `@gds` - Executes a GDS Test Case. Used to track in VIPER Mission. | ||||
|   - `@addInit` - Initializes the browser with an injected and artificial state. Useful for non-default plugins. | ||||
|   - `@localStorage` - Captures or generates session storage to manipulate browser state. Useful for excluding in tests which require a persistent backend (i.e. CouchDB). | ||||
|   - `@snapshot` - Uses Playwright's snapshot functionality to record a copy of the DOM for direct comparison. Must be run inside of a container. | ||||
|  | ||||
| ### (TBD) Best Practices | ||||
|  | ||||
| ### (TBD) Reporting | ||||
|  | ||||
| ### (TBD) Code Coverage | ||||
|  | ||||
| Code coverage is collected during test execution and reported with [nyc](https://github.com/istanbuljs/nyc) and [codecov.io](https://about.codecov.io/) | ||||
|  | ||||
| ## Other | ||||
|  | ||||
| ### FAQ | ||||
|  | ||||
| - How does this help NASA missions? | ||||
| - When should I write an e2e test instead of a unit test? | ||||
| - When should I write a functional vs visual test? | ||||
| - How is Open MCT extending default Playwright functionality? | ||||
|  | ||||
| ### Troubleshooting | ||||
|  | ||||
| - Why is my test failing on CI and not locally? | ||||
| - How can I view the failing tests on CI? | ||||
							
								
								
									
										18
									
								
								e2e/commonActions.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								e2e/commonActions.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| /** | ||||
|  * Wait for all animations within the given element and subtrees to finish | ||||
|  * See: https://github.com/microsoft/playwright/issues/15660#issuecomment-1184911658 | ||||
|  * @param {import('@playwright/test').Locator} locator | ||||
|  */ | ||||
| function waitForAnimations(locator) { | ||||
|     return locator | ||||
|         .evaluate((element) => | ||||
|             Promise.all( | ||||
|                 element | ||||
|                     .getAnimations({ subtree: true }) | ||||
|                     .map((animation) => animation.finished))); | ||||
| } | ||||
|  | ||||
| // eslint-disable-next-line no-undef | ||||
| module.exports = { | ||||
|     waitForAnimations | ||||
| }; | ||||
							
								
								
									
										69
									
								
								e2e/fixtures.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								e2e/fixtures.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,69 @@ | ||||
| /* This file extends the base functionality of the playwright test framework to enable | ||||
|  * code coverage instrumentation, console log error detection and working with a 3rd | ||||
|  * party Chrome-as-a-service extension called Browserless. | ||||
|  */ | ||||
|  | ||||
| const base = require('@playwright/test'); | ||||
| const { expect } = require('@playwright/test'); | ||||
| const fs = require('fs'); | ||||
| const path = require('path'); | ||||
| const { v4: uuid } = require('uuid'); | ||||
|  | ||||
| /** | ||||
|  * Takes a `ConsoleMessage` and returns a formatted string | ||||
|  * @param {import('@playwright/test').ConsoleMessage} msg | ||||
|  * @returns {String} formatted string with message type, text, url, and line and column numbers | ||||
|  */ | ||||
| function consoleMessageToString(msg) { | ||||
|     const { url, lineNumber, columnNumber } = msg.location(); | ||||
|  | ||||
|     return `[${msg.type()}] ${msg.text()} | ||||
|     at (${url} ${lineNumber}:${columnNumber})`; | ||||
| } | ||||
|  | ||||
| //The following is based on https://github.com/mxschmitt/playwright-test-coverage | ||||
| // eslint-disable-next-line no-undef | ||||
| const istanbulCLIOutput = path.join(process.cwd(), '.nyc_output'); | ||||
|  | ||||
| // eslint-disable-next-line no-undef | ||||
| exports.test = base.test.extend({ | ||||
|     //The following is based on https://github.com/mxschmitt/playwright-test-coverage | ||||
|     context: async ({ context }, use) => { | ||||
|         await context.addInitScript(() => | ||||
|             window.addEventListener('beforeunload', () => | ||||
|                 (window).collectIstanbulCoverage(JSON.stringify((window).__coverage__)) | ||||
|             ) | ||||
|         ); | ||||
|         await fs.promises.mkdir(istanbulCLIOutput, { recursive: true }); | ||||
|         await context.exposeFunction('collectIstanbulCoverage', (coverageJSON) => { | ||||
|             if (coverageJSON) { | ||||
|                 fs.writeFileSync(path.join(istanbulCLIOutput, `playwright_coverage_${uuid()}.json`), coverageJSON); | ||||
|             } | ||||
|         }); | ||||
|         await use(context); | ||||
|         for (const page of context.pages()) { | ||||
|             await page.evaluate(() => (window).collectIstanbulCoverage(JSON.stringify((window).__coverage__))); | ||||
|         } | ||||
|     }, | ||||
|     page: async ({ baseURL, page }, use) => { | ||||
|         const messages = []; | ||||
|         page.on('console', (msg) => messages.push(msg)); | ||||
|         await use(page); | ||||
|         messages.forEach( | ||||
|             msg => expect.soft(msg.type(), `Console error detected: ${consoleMessageToString(msg)}`).not.toEqual('error') | ||||
|         ); | ||||
|     }, | ||||
|     browser: async ({ playwright, browser }, use, workerInfo) => { | ||||
|         // Use browserless if configured | ||||
|         if (workerInfo.project.name.match(/browserless/)) { | ||||
|             const vBrowser = await playwright.chromium.connectOverCDP({ | ||||
|                 endpointURL: 'ws://localhost:3003' | ||||
|             }); | ||||
|             await use(vBrowser); | ||||
|         } else { | ||||
|             // Use Local Browser for testing. | ||||
|             await use(browser); | ||||
|         } | ||||
|     } | ||||
| }); | ||||
|  | ||||
| @@ -2,38 +2,44 @@ | ||||
| // playwright.config.js | ||||
| // @ts-check | ||||
|  | ||||
| // eslint-disable-next-line no-unused-vars | ||||
| const { devices } = require('@playwright/test'); | ||||
| const MAX_FAILURES = 5; | ||||
| const NUM_WORKERS = 2; | ||||
|  | ||||
| /** @type {import('@playwright/test').PlaywrightTestConfig} */ | ||||
| const config = { | ||||
|     retries: 2, | ||||
|     retries: 3, //Retries 3 times for a total of 4. When running sharded and with maxFailures = 5, this should ensure that flake is managed without failing the full suite | ||||
|     testDir: 'tests', | ||||
|     timeout: 90 * 1000, | ||||
|     testIgnore: '**/*.perf.spec.js', //Ignore performance tests and define in playwright-perfromance.config.js | ||||
|     timeout: 60 * 1000, | ||||
|     webServer: { | ||||
|         command: 'npm run start', | ||||
|         port: 8080, | ||||
|         command: 'cross-env NODE_ENV=test npm run start', | ||||
|         url: 'http://localhost:8080/#', | ||||
|         timeout: 200 * 1000, | ||||
|         reuseExistingServer: !process.env.CI | ||||
|         reuseExistingServer: false | ||||
|     }, | ||||
|     workers: 2, //Limit to 2 for CircleCI Agent | ||||
|     maxFailures: MAX_FAILURES, //Limits failures to 5 to reduce CI Waste | ||||
|     workers: NUM_WORKERS, //Limit to 2 for CircleCI Agent | ||||
|     use: { | ||||
|         baseURL: 'http://localhost:8080/', | ||||
|         headless: true, | ||||
|         ignoreHTTPSErrors: true, | ||||
|         screenshot: 'on', | ||||
|         trace: 'on', | ||||
|         video: 'on' | ||||
|         screenshot: 'only-on-failure', | ||||
|         trace: 'on-first-retry', | ||||
|         video: 'off' | ||||
|     }, | ||||
|     projects: [ | ||||
|         { | ||||
|             name: 'chrome', | ||||
|             use: { | ||||
|                 browserName: 'chromium', | ||||
|                 ...devices['Desktop Chrome'] | ||||
|                 browserName: 'chromium' | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|             name: 'MMOC', | ||||
|             testMatch: '**/*.e2e.spec.js', // only run e2e tests | ||||
|             grepInvert: /@snapshot/, | ||||
|             use: { | ||||
|                 browserName: 'chromium', | ||||
|                 viewport: { | ||||
| @@ -41,19 +47,32 @@ const config = { | ||||
|                     height: 1440 | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         /*{ | ||||
|             name: 'ipad', | ||||
|         }, | ||||
|         { | ||||
|             name: 'firefox', | ||||
|             testMatch: '**/*.e2e.spec.js', // only run e2e tests | ||||
|             grepInvert: /@snapshot/, | ||||
|             use: { | ||||
|                 browserName: 'webkit', | ||||
|                 ...devices['iPad (gen 7) landscape'] // Complete List https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json | ||||
|                 browserName: 'firefox' | ||||
|             } | ||||
|         }*/ | ||||
|         }, | ||||
|         { | ||||
|             name: 'chrome-beta', //Only Chrome Beta is available on ubuntu -- not chrome canary | ||||
|             testMatch: '**/*.e2e.spec.js', // only run e2e tests | ||||
|             grepInvert: /@snapshot/, | ||||
|             use: { | ||||
|                 browserName: 'chromium', | ||||
|                 channel: 'chrome-beta' | ||||
|             } | ||||
|         } | ||||
|     ], | ||||
|     reporter: [ | ||||
|         ['list'], | ||||
|         ['html', { | ||||
|             open: 'never', | ||||
|             outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840 | ||||
|         }], | ||||
|         ['junit', { outputFile: 'test-results/results.xml' }], | ||||
|         ['allure-playwright'], | ||||
|         ['github'] | ||||
|     ] | ||||
| }; | ||||
|   | ||||
| @@ -2,18 +2,20 @@ | ||||
| // playwright.config.js | ||||
| // @ts-check | ||||
|  | ||||
| // eslint-disable-next-line no-unused-vars | ||||
| const { devices } = require('@playwright/test'); | ||||
|  | ||||
| /** @type {import('@playwright/test').PlaywrightTestConfig} */ | ||||
| const config = { | ||||
|     retries: 0, | ||||
|     testDir: 'tests', | ||||
|     testIgnore: '**/*.perf.spec.js', | ||||
|     timeout: 30 * 1000, | ||||
|     webServer: { | ||||
|         command: 'npm run start', | ||||
|         port: 8080, | ||||
|         command: 'cross-env NODE_ENV=test npm run start', | ||||
|         url: 'http://localhost:8080/#', | ||||
|         timeout: 120 * 1000, | ||||
|         reuseExistingServer: !process.env.CI | ||||
|         reuseExistingServer: true | ||||
|     }, | ||||
|     workers: 1, | ||||
|     use: { | ||||
| @@ -21,20 +23,21 @@ const config = { | ||||
|         baseURL: 'http://localhost:8080/', | ||||
|         headless: false, | ||||
|         ignoreHTTPSErrors: true, | ||||
|         screenshot: 'on', | ||||
|         trace: 'on', | ||||
|         video: 'on' | ||||
|         screenshot: 'only-on-failure', | ||||
|         trace: 'retain-on-failure', | ||||
|         video: 'off' | ||||
|     }, | ||||
|     projects: [ | ||||
|         { | ||||
|             name: 'chrome', | ||||
|             use: { | ||||
|                 browserName: 'chromium', | ||||
|                 ...devices['Desktop Chrome'] | ||||
|                 browserName: 'chromium' | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|             name: 'MMOC', | ||||
|             testMatch: '**/*.e2e.spec.js', // only run e2e tests | ||||
|             grepInvert: /@snapshot/, | ||||
|             use: { | ||||
|                 browserName: 'chromium', | ||||
|                 viewport: { | ||||
| @@ -42,18 +45,59 @@ const config = { | ||||
|                     height: 1440 | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         /*{ | ||||
|         }, | ||||
|         { | ||||
|             name: 'safari', | ||||
|             testMatch: '**/*.e2e.spec.js', // only run e2e tests | ||||
|             grep: /@ipad/, // only run ipad tests due to this bug https://github.com/microsoft/playwright/issues/8340 | ||||
|             grepInvert: /@snapshot/, | ||||
|             use: { | ||||
|                 browserName: 'webkit' | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|             name: 'firefox', | ||||
|             testMatch: '**/*.e2e.spec.js', // only run e2e tests | ||||
|             grepInvert: /@snapshot/, | ||||
|             use: { | ||||
|                 browserName: 'firefox' | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|             name: 'canary', | ||||
|             testMatch: '**/*.e2e.spec.js', // only run e2e tests | ||||
|             grepInvert: /@snapshot/, | ||||
|             use: { | ||||
|                 browserName: 'chromium', | ||||
|                 channel: 'chrome-canary' //Note this is not available in ubuntu/CircleCI | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|             name: 'chrome-beta', | ||||
|             testMatch: '**/*.e2e.spec.js', // only run e2e tests | ||||
|             grepInvert: /@snapshot/, | ||||
|             use: { | ||||
|                 browserName: 'chromium', | ||||
|                 channel: 'chrome-beta' | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|             name: 'ipad', | ||||
|             testMatch: '**/*.e2e.spec.js', // only run e2e tests | ||||
|             grep: /@ipad/, | ||||
|             grepInvert: /@snapshot/, | ||||
|             use: { | ||||
|                 browserName: 'webkit', | ||||
|                 ...devices['iPad (gen 7) landscape'] // Complete List https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json | ||||
|             } | ||||
|         }*/ | ||||
|         } | ||||
|     ], | ||||
|     reporter: [ | ||||
|         ['list'], | ||||
|         ['allure-playwright'] | ||||
|         ['html', { | ||||
|             open: 'on-failure', | ||||
|             outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840 | ||||
|         }] | ||||
|     ] | ||||
| }; | ||||
|  | ||||
|   | ||||
							
								
								
									
										43
									
								
								e2e/playwright-performance.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								e2e/playwright-performance.config.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | ||||
| /* eslint-disable no-undef */ | ||||
| // playwright.config.js | ||||
| // @ts-check | ||||
|  | ||||
| const CI = process.env.CI === 'true'; | ||||
|  | ||||
| /** @type {import('@playwright/test').PlaywrightTestConfig} */ | ||||
| const config = { | ||||
|     retries: 1, //Only for debugging purposes because trace is enabled only on first retry | ||||
|     testDir: 'tests/performance/', | ||||
|     timeout: 60 * 1000, | ||||
|     workers: 1, //Only run in serial with 1 worker | ||||
|     webServer: { | ||||
|         command: 'cross-env NODE_ENV=test npm run start', | ||||
|         url: 'http://localhost:8080/#', | ||||
|         timeout: 200 * 1000, | ||||
|         reuseExistingServer: !CI | ||||
|     }, | ||||
|     use: { | ||||
|         browserName: "chromium", | ||||
|         baseURL: 'http://localhost:8080/', | ||||
|         headless: CI, //Only if running locally | ||||
|         ignoreHTTPSErrors: true, | ||||
|         screenshot: 'off', | ||||
|         trace: 'on-first-retry', | ||||
|         video: 'off' | ||||
|     }, | ||||
|     projects: [ | ||||
|         { | ||||
|             name: 'chrome', | ||||
|             use: { | ||||
|                 browserName: 'chromium' | ||||
|             } | ||||
|         } | ||||
|     ], | ||||
|     reporter: [ | ||||
|         ['list'], | ||||
|         ['junit', { outputFile: 'test-results/results.xml' }], | ||||
|         ['json', { outputFile: 'test-results/results.json' }] | ||||
|     ] | ||||
| }; | ||||
|  | ||||
| module.exports = config; | ||||
| @@ -4,29 +4,28 @@ | ||||
|  | ||||
| /** @type {import('@playwright/test').PlaywrightTestConfig} */ | ||||
| const config = { | ||||
|     retries: 0, | ||||
|     testDir: 'tests', | ||||
|     retries: 0, // visual tests should never retry due to snapshot comparison errors | ||||
|     testDir: 'tests/visual', | ||||
|     timeout: 90 * 1000, | ||||
|     workers: 1, | ||||
|     workers: 1, // visual tests should never run in parallel due to test pollution | ||||
|     webServer: { | ||||
|         command: 'npm run start', | ||||
|         port: 8080, | ||||
|         command: 'cross-env NODE_ENV=test npm run start', | ||||
|         url: 'http://localhost:8080/#', | ||||
|         timeout: 200 * 1000, | ||||
|         reuseExistingServer: !process.env.CI | ||||
|     }, | ||||
|     use: { | ||||
|         browserName: "chromium", | ||||
|         baseURL: 'http://localhost:8080/', | ||||
|         headless: true, | ||||
|         headless: true, // this needs to remain headless to avoid visual changes due to GPU | ||||
|         ignoreHTTPSErrors: true, | ||||
|         screenshot: 'on', | ||||
|         trace: 'off', | ||||
|         video: 'on' | ||||
|         video: 'off' | ||||
|     }, | ||||
|     reporter: [ | ||||
|         ['list'], | ||||
|         ['junit', { outputFile: 'test-results/results.xml' }], | ||||
|         ['allure-playwright'] | ||||
|         ['junit', { outputFile: 'test-results/results.xml' }] | ||||
|     ] | ||||
| }; | ||||
|  | ||||
|   | ||||
							
								
								
									
										1
									
								
								e2e/test-data/PerformanceDisplayLayout.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								e2e/test-data/PerformanceDisplayLayout.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| {"openmct":{"b3cee102-86dd-4c0a-8eec-4d5d276f8691":{"identifier":{"key":"b3cee102-86dd-4c0a-8eec-4d5d276f8691","namespace":""},"name":"Performance Display Layout","type":"layout","composition":[{"key":"9666e7b4-be0c-47a5-94b8-99accad7155e","namespace":""}],"configuration":{"items":[{"width":32,"height":18,"x":12,"y":9,"identifier":{"key":"9666e7b4-be0c-47a5-94b8-99accad7155e","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"23ca351d-a67d-46aa-a762-290eb742d2f1"}],"layoutGrid":[10,10]},"modified":1654299875432,"location":"mine","persisted":1654299878751},"9666e7b4-be0c-47a5-94b8-99accad7155e":{"identifier":{"key":"9666e7b4-be0c-47a5-94b8-99accad7155e","namespace":""},"name":"Performance Example Imagery","type":"example.imagery","configuration":{"imageLocation":"","imageLoadDelayInMilliSeconds":20000,"imageSamples":[],"layers":[{"source":"dist/imagery/example-imagery-layer-16x9.png","name":"16:9","visible":false},{"source":"dist/imagery/example-imagery-layer-safe.png","name":"Safe","visible":false},{"source":"dist/imagery/example-imagery-layer-scale.png","name":"Scale","visible":false}]},"telemetry":{"values":[{"name":"Name","key":"name"},{"name":"Time","key":"utc","format":"utc","hints":{"domain":2}},{"name":"Local Time","key":"local","format":"local-format","hints":{"domain":1}},{"name":"Image","key":"url","format":"image","hints":{"image":1},"layers":[{"source":"dist/imagery/example-imagery-layer-16x9.png","name":"16:9"},{"source":"dist/imagery/example-imagery-layer-safe.png","name":"Safe"},{"source":"dist/imagery/example-imagery-layer-scale.png","name":"Scale"}]},{"name":"Image Download Name","key":"imageDownloadName","format":"imageDownloadName","hints":{"imageDownloadName":1}}]},"modified":1654299840077,"location":"b3cee102-86dd-4c0a-8eec-4d5d276f8691","persisted":1654299840078}},"rootId":"b3cee102-86dd-4c0a-8eec-4d5d276f8691"} | ||||
							
								
								
									
										1
									
								
								e2e/test-data/PerformanceNotebook.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								e2e/test-data/PerformanceNotebook.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| {"openmct":{"6d2fa9fd-f2aa-461a-a1e1-164ac44bec9d":{"identifier":{"key":"6d2fa9fd-f2aa-461a-a1e1-164ac44bec9d","namespace":""},"name":"Performance Notebook","type":"notebook","configuration":{"defaultSort":"oldest","entries":{"3e31c412-33ba-4757-8ade-e9821f6ba321":{"8c8f6035-631c-45af-8c24-786c60295335":[{"id":"entry-1652815305457","createdOn":1652815305457,"createdBy":"","text":"Existing Entry 1","embeds":[]},{"id":"entry-1652815313465","createdOn":1652815313465,"createdBy":"","text":"Existing Entry 2","embeds":[]},{"id":"entry-1652815399955","createdOn":1652815399955,"createdBy":"","text":"Existing Entry 3","embeds":[]}]}},"imageMigrationVer":"v1","pageTitle":"Page","sections":[{"id":"3e31c412-33ba-4757-8ade-e9821f6ba321","isDefault":false,"isSelected":false,"name":"Section1","pages":[{"id":"8c8f6035-631c-45af-8c24-786c60295335","isDefault":false,"isSelected":false,"name":"Page1","pageTitle":"Page"},{"id":"36555942-c9aa-439c-bbdb-0aaf50db50f5","isDefault":false,"isSelected":false,"name":"Page2","pageTitle":"Page"}],"sectionTitle":"Section"},{"id":"dab0bd1d-2c5a-405c-987f-107123d6189a","isDefault":false,"isSelected":true,"name":"Section2","pages":[{"id":"f625a86a-cb99-4898-8082-80543c8de534","isDefault":false,"isSelected":false,"name":"Page1","pageTitle":"Page"},{"id":"e77ef810-f785-42a7-942e-07e999b79c59","isDefault":false,"isSelected":true,"name":"Page2","pageTitle":"Page"}],"sectionTitle":"Section"}],"sectionTitle":"Section","type":"General","showTime":"0"},"modified":1652815915219,"location":"mine","persisted":1652815915222}},"rootId":"6d2fa9fd-f2aa-461a-a1e1-164ac44bec9d"} | ||||
							
								
								
									
										22
									
								
								e2e/test-data/VisualTestData_storage.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								e2e/test-data/VisualTestData_storage.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| { | ||||
|   "cookies": [], | ||||
|   "origins": [ | ||||
|     { | ||||
|       "origin": "http://localhost:8080", | ||||
|       "localStorage": [ | ||||
|         { | ||||
|           "name": "tcHistory", | ||||
|           "value": "{\"utc\":[{\"start\":1654548551471,\"end\":1654550351471}]}" | ||||
|         }, | ||||
|         { | ||||
|           "name": "mct", | ||||
|           "value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"527856c0-cced-4b64-bb19-f943432326d0\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1654550352296,\"modified\":1654550352296},\"527856c0-cced-4b64-bb19-f943432326d0\":{\"identifier\":{\"key\":\"527856c0-cced-4b64-bb19-f943432326d0\",\"namespace\":\"\"},\"name\":\"Unnamed Overlay Plot\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"ce88ce37-8bb9-45e1-a85b-bb7e3c8453b9\",\"namespace\":\"\"}],\"configuration\":{\"series\":[{\"identifier\":{\"key\":\"ce88ce37-8bb9-45e1-a85b-bb7e3c8453b9\",\"namespace\":\"\"}}],\"yAxis\":{},\"xAxis\":{}},\"modified\":1654550353356,\"location\":\"mine\",\"persisted\":1654550353357},\"ce88ce37-8bb9-45e1-a85b-bb7e3c8453b9\":{\"name\":\"Unnamed Sine Wave Generator\",\"type\":\"generator\",\"identifier\":{\"key\":\"ce88ce37-8bb9-45e1-a85b-bb7e3c8453b9\",\"namespace\":\"\"},\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":\"5000\"},\"modified\":1654550353350,\"location\":\"527856c0-cced-4b64-bb19-f943432326d0\",\"persisted\":1654550353350}}" | ||||
|         }, | ||||
|         { | ||||
|           "name": "mct-tree-expanded", | ||||
|           "value": "[\"/browse/mine\"]" | ||||
|         } | ||||
|       ] | ||||
|     } | ||||
|   ] | ||||
| } | ||||
							
								
								
									
										22
									
								
								e2e/test-data/recycled_local_storage.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								e2e/test-data/recycled_local_storage.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| { | ||||
|   "cookies": [], | ||||
|   "origins": [ | ||||
|     { | ||||
|       "origin": "http://localhost:8080", | ||||
|       "localStorage": [ | ||||
|         { | ||||
|           "name": "tcHistory", | ||||
|           "value": "{\"utc\":[{\"start\":1654537164464,\"end\":1654538964464},{\"start\":1652301954635,\"end\":1652303754635}]}" | ||||
|         }, | ||||
|         { | ||||
|           "name": "mct", | ||||
|           "value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1654538965703,\"modified\":1654538965703},\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"73f2d9ae-d1f3-4561-b7fc-ecd5df557249\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1652303755999,\"location\":\"mine\",\"persisted\":1652303756002},\"2d02a680-eb7e-4645-bba2-dd298f76efb8\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"4291d80c-303c-4d8d-85e1-10f012b864fb\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1654538965702,\"location\":\"mine\",\"persisted\":1654538965702}}" | ||||
|         }, | ||||
|         { | ||||
|           "name": "mct-tree-expanded", | ||||
|           "value": "[]" | ||||
|         } | ||||
|       ] | ||||
|     } | ||||
|   ] | ||||
| } | ||||
							
								
								
									
										77
									
								
								e2e/tests/api/forms/forms.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								e2e/tests/api/forms/forms.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,77 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* | ||||
| This test suite is dedicated to tests which verify form functionality. | ||||
| */ | ||||
|  | ||||
| const { test, expect } = require('@playwright/test'); | ||||
|  | ||||
| const TEST_FOLDER = 'test folder'; | ||||
|  | ||||
| test.describe('forms set', () => { | ||||
|     test('New folder form has title as required field', async ({ page }) => { | ||||
|         //Go to baseURL | ||||
|         await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|         // Click button:has-text("Create") | ||||
|         await page.click('button:has-text("Create")'); | ||||
|         // Click :nth-match(:text("Folder"), 2) | ||||
|         await page.click(':nth-match(:text("Folder"), 2)'); | ||||
|         // Click text=Properties Title Notes >> input[type="text"] | ||||
|         await page.click('text=Properties Title Notes >> input[type="text"]'); | ||||
|         // Fill text=Properties Title Notes >> input[type="text"] | ||||
|         await page.fill('text=Properties Title Notes >> input[type="text"]', ''); | ||||
|         // Press Tab | ||||
|         await page.press('text=Properties Title Notes >> input[type="text"]', 'Tab'); | ||||
|  | ||||
|         const okButton = page.locator('text=OK'); | ||||
|  | ||||
|         await expect(okButton).toBeDisabled(); | ||||
|         await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(/invalid/); | ||||
|  | ||||
|         // Click text=Properties Title Notes >> input[type="text"] | ||||
|         await page.click('text=Properties Title Notes >> input[type="text"]'); | ||||
|         // Fill text=Properties Title Notes >> input[type="text"] | ||||
|         await page.fill('text=Properties Title Notes >> input[type="text"]', TEST_FOLDER); | ||||
|         // Press Tab | ||||
|         await page.press('text=Properties Title Notes >> input[type="text"]', 'Tab'); | ||||
|  | ||||
|         await expect(page.locator('.c-form-row__state-indicator').first()).not.toHaveClass(/invalid/); | ||||
|  | ||||
|         // Click text=OK | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.click('text=OK') | ||||
|         ]); | ||||
|  | ||||
|         await expect(page.locator('.l-browse-bar__object-name')).toContainText(TEST_FOLDER); | ||||
|     }); | ||||
|     test.fixme('Create all object types and verify correctness', async ({ page }) => { | ||||
|         //Create the following Domain Objects with their unique Object Types | ||||
|         // Sine Wave Generator (number object) | ||||
|         // Timer Object | ||||
|         // Plan View Object | ||||
|         // Clock Object | ||||
|         // Hyperlink | ||||
|     }); | ||||
| }); | ||||
| @@ -24,7 +24,8 @@ | ||||
| This test suite is dedicated to tests which verify branding related components. | ||||
| */ | ||||
|  | ||||
| const { test, expect } = require('@playwright/test'); | ||||
| const { test } = require('../fixtures.js'); | ||||
| const { expect } = require('@playwright/test'); | ||||
|  | ||||
| test.describe('Branding tests', () => { | ||||
|     test('About Modal launches with basic branding properties', async ({ page }) => { | ||||
| @@ -35,7 +36,7 @@ test.describe('Branding tests', () => { | ||||
|         await page.click('.l-shell__app-logo'); | ||||
|  | ||||
|         // Verify that the NASA Logo Appears | ||||
|         await expect(await page.locator('.c-about__image')).toBeVisible(); | ||||
|         await expect(page.locator('.c-about__image')).toBeVisible(); | ||||
|  | ||||
|         // Modify the Build information in 'about' Modal | ||||
|         const versionInformationLocator = page.locator('ul.t-info.l-info.s-info'); | ||||
| @@ -57,6 +58,7 @@ test.describe('Branding tests', () => { | ||||
|             page.waitForEvent('popup'), | ||||
|             page.locator('text=click here for third party licensing information').click() | ||||
|         ]); | ||||
|         expect(page2.waitForURL('**\/licenses**')).toBeTruthy(); | ||||
|         await page2.waitForLoadState('networkidle'); //Avoids timing issues with juggler/firefox | ||||
|         expect(page2.waitForURL('**/licenses**')).toBeTruthy(); | ||||
|     }); | ||||
| }); | ||||
|   | ||||
| @@ -24,7 +24,8 @@ | ||||
| This test suite is dedicated to tests which verify the basic operations surrounding the example event generator. | ||||
| */ | ||||
|  | ||||
| const { test, expect } = require('@playwright/test'); | ||||
| const { test } = require('../../fixtures.js'); | ||||
| const { expect } = require('@playwright/test'); | ||||
|  | ||||
| test.describe('Example Event Generator Operations', () => { | ||||
|     test('Can create example event generator with a name', async ({ page }) => { | ||||
|   | ||||
| @@ -24,10 +24,13 @@ | ||||
| This test suite is dedicated to tests which verify the basic operations surrounding conditionSets. | ||||
| */ | ||||
|  | ||||
| const { test, expect } = require('@playwright/test'); | ||||
| const { test } = require('../../../fixtures.js'); | ||||
| const { expect } = require('@playwright/test'); | ||||
|  | ||||
| test.describe('Sine Wave Generator', () => { | ||||
|     test('Create new Sine Wave Generator Object and validate create Form Logic', async ({ page }) => { | ||||
|     test('Create new Sine Wave Generator Object and validate create Form Logic', async ({ page, browserName }) => { | ||||
|         test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox'); | ||||
|  | ||||
|         //Go to baseURL | ||||
|         await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
| @@ -39,44 +42,45 @@ test.describe('Sine Wave Generator', () => { | ||||
|  | ||||
|         // Verify that the each required field has required indicator | ||||
|         // Title | ||||
|         await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(['c-form-row__state-indicator  req']); | ||||
|         await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(/req/); | ||||
|  | ||||
|         // Verify that the Notes row does not have a required indicator | ||||
|         await expect(page.locator('.c-form__section div:nth-child(3) .form-row .c-form-row__state-indicator')).not.toContain('.req'); | ||||
|         await page.locator('textarea[type="text"]').fill('Optional Note Text'); | ||||
|  | ||||
|         // Period | ||||
|         await expect(page.locator('.c-form__section div:nth-child(4) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator  req']); | ||||
|         await expect(page.locator('div:nth-child(4) .c-form-row__state-indicator')).toHaveClass(/req/); | ||||
|  | ||||
|         // Amplitude | ||||
|         await expect(page.locator('.c-form__section div:nth-child(5) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator  req']); | ||||
|         await expect(page.locator('div:nth-child(5) .c-form-row__state-indicator')).toHaveClass(/req/); | ||||
|  | ||||
|         // Offset | ||||
|         await expect(page.locator('.c-form__section div:nth-child(6) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator  req']); | ||||
|         await expect(page.locator('div:nth-child(6) .c-form-row__state-indicator')).toHaveClass(/req/); | ||||
|  | ||||
|         // Data Rate | ||||
|         await expect(page.locator('.c-form__section div:nth-child(7) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator  req']); | ||||
|         await expect(page.locator('div:nth-child(7) .c-form-row__state-indicator')).toHaveClass(/req/); | ||||
|  | ||||
|         // Phase | ||||
|         await expect(page.locator('.c-form__section div:nth-child(8) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator  req']); | ||||
|         await expect(page.locator('div:nth-child(8) .c-form-row__state-indicator')).toHaveClass(/req/); | ||||
|  | ||||
|         // Randomness | ||||
|         await expect(page.locator('.c-form__section div:nth-child(9) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator  req']); | ||||
|         await expect(page.locator('div:nth-child(9) .c-form-row__state-indicator')).toHaveClass(/req/); | ||||
|  | ||||
|         // Verify that by removing value from required text field shows invalid indicator | ||||
|         await page.locator('text=Properties Title Notes Period Amplitude Offset Data Rate (hz) Phase (radians) Ra >> input[type="text"]').fill(''); | ||||
|         await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(['c-form-row__state-indicator  req invalid']); | ||||
|         await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(/invalid/); | ||||
|  | ||||
|         // Verify that by adding value to empty required text field changes invalid to valid indicator | ||||
|         await page.locator('text=Properties Title Notes Period Amplitude Offset Data Rate (hz) Phase (radians) Ra >> input[type="text"]').fill('non empty'); | ||||
|         await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(['c-form-row__state-indicator  req valid']); | ||||
|         await page.locator('text=Properties Title Notes Period Amplitude Offset Data Rate (hz) Phase (radians) Ra >> input[type="text"]').fill('New Sine Wave Generator'); | ||||
|         await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(/valid/); | ||||
|  | ||||
|         // Verify that by removing value from required number field shows invalid indicator | ||||
|         await page.locator('.field.control.l-input-sm input').first().fill(''); | ||||
|         await expect(page.locator('.c-form__section div:nth-child(4) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator  req invalid']); | ||||
|         await expect(page.locator('div:nth-child(4) .c-form-row__state-indicator')).toHaveClass(/invalid/); | ||||
|  | ||||
|         // Verify that by adding value to empty required number field changes invalid to valid indicator | ||||
|         await page.locator('.field.control.l-input-sm input').first().fill('3'); | ||||
|         await expect(page.locator('.c-form__section div:nth-child(4) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator  req valid']); | ||||
|         await expect(page.locator('div:nth-child(4) .c-form-row__state-indicator')).toHaveClass(/valid/); | ||||
|  | ||||
|         // Verify that can change value of number field by up/down arrows keys | ||||
|         // Click .field.control.l-input-sm input >> nth=0 | ||||
| @@ -89,57 +93,6 @@ test.describe('Sine Wave Generator', () => { | ||||
|         const value = await page.locator('.field.control.l-input-sm input').first().inputValue(); | ||||
|         await expect(value).toBe('6'); | ||||
|  | ||||
|         // Click .c-form-row__state-indicator.grows | ||||
|         await page.locator('.c-form-row__state-indicator.grows').click(); | ||||
|  | ||||
|         // Click text=Properties Title Notes Period Amplitude Offset Data Rate (hz) Phase (radians) Ra >> input[type="text"] | ||||
|         await page.locator('text=Properties Title Notes Period Amplitude Offset Data Rate (hz) Phase (radians) Ra >> input[type="text"]').click(); | ||||
|  | ||||
|         // Click .c-form-row__state-indicator >> nth=0 | ||||
|         await page.locator('.c-form-row__state-indicator').first().click(); | ||||
|  | ||||
|         // Fill text=Properties Title Notes Period Amplitude Offset Data Rate (hz) Phase (radians) Ra >> input[type="text"] | ||||
|         await page.locator('text=Properties Title Notes Period Amplitude Offset Data Rate (hz) Phase (radians) Ra >> input[type="text"]').fill('New Sine Wave Generator'); | ||||
|  | ||||
|         // Double click div:nth-child(4) .form-row .c-form-row__controls | ||||
|         await page.locator('div:nth-child(4) .form-row .c-form-row__controls').dblclick(); | ||||
|  | ||||
|         // Click .field.control.l-input-sm input >> nth=0 | ||||
|         await page.locator('.field.control.l-input-sm input').first().click(); | ||||
|  | ||||
|         // Click div:nth-child(4) .form-row .c-form-row__state-indicator | ||||
|         await page.locator('div:nth-child(4) .form-row .c-form-row__state-indicator').click(); | ||||
|  | ||||
|         // Click .field.control.l-input-sm input >> nth=0 | ||||
|         await page.locator('.field.control.l-input-sm input').first().click(); | ||||
|  | ||||
|         // Click .field.control.l-input-sm input >> nth=0 | ||||
|         await page.locator('.field.control.l-input-sm input').first().click(); | ||||
|  | ||||
|         // Click div:nth-child(5) .form-row .c-form-row__controls .form-control .field input | ||||
|         await page.locator('div:nth-child(5) .form-row .c-form-row__controls .form-control .field input').click(); | ||||
|  | ||||
|         // Click div:nth-child(5) .form-row .c-form-row__controls .form-control .field input | ||||
|         await page.locator('div:nth-child(5) .form-row .c-form-row__controls .form-control .field input').click(); | ||||
|  | ||||
|         // Click div:nth-child(5) .form-row .c-form-row__controls .form-control .field input | ||||
|         await page.locator('div:nth-child(5) .form-row .c-form-row__controls .form-control .field input').click(); | ||||
|  | ||||
|         // Click div:nth-child(6) .form-row .c-form-row__controls .form-control .field input | ||||
|         await page.locator('div:nth-child(6) .form-row .c-form-row__controls .form-control .field input').click(); | ||||
|  | ||||
|         // Double click div:nth-child(7) .form-row .c-form-row__controls .form-control .field input | ||||
|         await page.locator('div:nth-child(7) .form-row .c-form-row__controls .form-control .field input').dblclick(); | ||||
|  | ||||
|         // Click div:nth-child(7) .form-row .c-form-row__state-indicator | ||||
|         await page.locator('div:nth-child(7) .form-row .c-form-row__state-indicator').click(); | ||||
|  | ||||
|         // Click div:nth-child(7) .form-row .c-form-row__controls .form-control .field input | ||||
|         await page.locator('div:nth-child(7) .form-row .c-form-row__controls .form-control .field input').click(); | ||||
|  | ||||
|         // Fill div:nth-child(7) .form-row .c-form-row__controls .form-control .field input | ||||
|         await page.locator('div:nth-child(7) .form-row .c-form-row__controls .form-control .field input').fill('3'); | ||||
|  | ||||
|         //Click text=OK | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
| @@ -150,12 +103,17 @@ test.describe('Sine Wave Generator', () => { | ||||
|         // Verify object properties | ||||
|         await expect(page.locator('.l-browse-bar__object-name')).toContainText('New Sine Wave Generator'); | ||||
|  | ||||
|         // Verify canvas rendered | ||||
|         // Verify canvas rendered and can be interacted with | ||||
|         await page.locator('canvas').nth(1).click({ | ||||
|             position: { | ||||
|                 x: 341, | ||||
|                 y: 28 | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         // Verify that where we click on canvas shows the number we clicked on | ||||
|         // Note that any number will do, we just care that a number exists | ||||
|         await expect(page.locator('.value-to-display-nearestValue')).toContainText(/[+-]?([0-9]*[.])?[0-9]+/); | ||||
|  | ||||
|     }); | ||||
| }); | ||||
|   | ||||
							
								
								
									
										55
									
								
								e2e/tests/framework.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								e2e/tests/framework.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* | ||||
| This test suite is dedicated to testing our use of the playwright framework as it | ||||
| relates to how we've extended it (i.e. ./e2e/fixtures.js) and assumptions made in our dev environment | ||||
| (app.js and ./e2e/webpack-dev-middleware.js) | ||||
| */ | ||||
|  | ||||
| const { test } = require('../fixtures.js'); | ||||
|  | ||||
| test.describe('fixtures.js tests', () => { | ||||
|     test('Verify that tests fail if console.error is thrown', async ({ page }) => { | ||||
|         test.fail(); | ||||
|         //Go to baseURL | ||||
|         await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|         //Verify that ../fixtures.js detects console log errors | ||||
|         await Promise.all([ | ||||
|             page.evaluate(() => console.error('This should result in a failure')), | ||||
|             page.waitForEvent('console') // always wait for the event to happen while triggering it! | ||||
|         ]); | ||||
|  | ||||
|     }); | ||||
|     test('Verify that tests pass if console.warn is thrown', async ({ page }) => { | ||||
|         //Go to baseURL | ||||
|         await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|         //Verify that ../fixtures.js detects console log errors | ||||
|         await Promise.all([ | ||||
|             page.evaluate(() => console.warn('This should result in a pass')), | ||||
|             page.waitForEvent('console') // always wait for the event to happen while triggering it! | ||||
|         ]); | ||||
|  | ||||
|     }); | ||||
| }); | ||||
| @@ -24,19 +24,114 @@ | ||||
| This test suite is dedicated to tests which verify the basic operations surrounding moving objects. | ||||
| */ | ||||
|  | ||||
| const { test, expect } = require('@playwright/test'); | ||||
| const { test } = require('../fixtures.js'); | ||||
| const { expect } = require('@playwright/test'); | ||||
|  | ||||
| test.describe('Move item tests', () => { | ||||
|     test.fixme('Create a basic object and verify that it can be moved to another Folder', async ({ page }) => { | ||||
|         //Create and save Folder | ||||
|         //Create and save Domain Object | ||||
|         //Verify that the newly created domain object can be moved to Folder from Step 1. | ||||
|         //Verify that newly moved object appears in the correct point in Tree | ||||
|         //Verify that newly moved object appears correctly in Inspector panel | ||||
|     test('Create a basic object and verify that it can be moved to another folder', async ({ page }) => { | ||||
|         // Go to Open MCT | ||||
|         await page.goto('/'); | ||||
|  | ||||
|         // Create a new folder in the root my items folder | ||||
|         let folder1 = "Folder1"; | ||||
|         await page.locator('button:has-text("Create")').click(); | ||||
|         await page.locator('li.icon-folder').click(); | ||||
|  | ||||
|         await page.locator('text=Properties Title Notes >> input[type="text"]').click(); | ||||
|         await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder1); | ||||
|  | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('text=OK').click(), | ||||
|             page.waitForSelector('.c-message-banner__message') | ||||
|         ]); | ||||
|         //Wait until Save Banner is gone | ||||
|         await page.locator('.c-message-banner__close-button').click(); | ||||
|         await page.waitForSelector('.c-message-banner__message', { state: 'detached'}); | ||||
|  | ||||
|         // Create another folder with a new name at default location, which is currently inside Folder 1 | ||||
|         let folder2 = "Folder2"; | ||||
|         await page.locator('button:has-text("Create")').click(); | ||||
|         await page.locator('li.icon-folder').click(); | ||||
|         await page.locator('text=Properties Title Notes >> input[type="text"]').click(); | ||||
|         await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder2); | ||||
|  | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('text=OK').click(), | ||||
|             page.waitForSelector('.c-message-banner__message') | ||||
|         ]); | ||||
|         //Wait until Save Banner is gone | ||||
|         await page.locator('.c-message-banner__close-button').click(); | ||||
|         await page.waitForSelector('.c-message-banner__message', { state: 'detached'}); | ||||
|  | ||||
|         // Move Folder 2 from Folder 1 to My Items | ||||
|         await page.locator('text=Open MCT My Items >> span').nth(3).click(); | ||||
|         await page.locator('.c-tree__scrollable div div:nth-child(2) .c-tree__item .c-tree__item__view-control').click(); | ||||
|  | ||||
|         await page.locator(`a:has-text("${folder2}")`).click({ | ||||
|             button: 'right' | ||||
|         }); | ||||
|         await page.locator('li.icon-move').click(); | ||||
|         await page.locator('form[name="mctForm"] >> text=My Items').click(); | ||||
|  | ||||
|         await page.locator('text=OK').click(); | ||||
|  | ||||
|         // Expect that Folder 2 is in My Items, the root folder | ||||
|         expect(page.locator(`text=My Items >> nth=0:has(text=${folder2})`)).toBeTruthy(); | ||||
|     }); | ||||
|     test.fixme('Create a basic object and verify that it cannot be moved to object without Composition Provider', async ({ page }) => { | ||||
|         //Create and save Telemetry Object | ||||
|         //Create and save Domain Object | ||||
|         //Verify that the newly created domain object cannot be moved to Telemetry Object from step 1. | ||||
|     test('Create a basic object and verify that it cannot be moved to telemetry object without Composition Provider', async ({ page }) => { | ||||
|         // Go to Open MCT | ||||
|         await page.goto('/'); | ||||
|  | ||||
|         // Create Telemetry Table | ||||
|         let telemetryTable = 'Test Telemetry Table'; | ||||
|         await page.locator('button:has-text("Create")').click(); | ||||
|         await page.locator('li:has-text("Telemetry Table")').click(); | ||||
|         await page.locator('text=Properties Title Notes >> input[type="text"]').click(); | ||||
|         await page.locator('text=Properties Title Notes >> input[type="text"]').fill(telemetryTable); | ||||
|  | ||||
|         await page.locator('text=OK').click(); | ||||
|  | ||||
|         // Finish editing and save Telemetry Table | ||||
|         await page.locator('.c-button--menu.c-button--major.icon-save').click(); | ||||
|         await page.locator('text=Save and Finish Editing').click(); | ||||
|  | ||||
|         // Create New Folder Basic Domain Object | ||||
|         let folder = 'Test Folder'; | ||||
|         await page.locator('button:has-text("Create")').click(); | ||||
|         await page.locator('li:has-text("Folder")').click(); | ||||
|         await page.locator('text=Properties Title Notes >> input[type="text"]').click(); | ||||
|         await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder); | ||||
|  | ||||
|         // See if it's possible to put the folder in the Telemetry object during creation (Soft Assert) | ||||
|         await page.locator(`form[name="mctForm"] >> text=${telemetryTable}`).click(); | ||||
|         let okButton = await page.locator('button.c-button.c-button--major:has-text("OK")'); | ||||
|         let okButtonStateDisabled = await okButton.isDisabled(); | ||||
|         expect.soft(okButtonStateDisabled).toBeTruthy(); | ||||
|  | ||||
|         // Continue test regardless of assertion and create it in My Items | ||||
|         await page.locator('form[name="mctForm"] >> text=My Items').click(); | ||||
|         await page.locator('text=OK').click(); | ||||
|  | ||||
|         // Open My Items | ||||
|         await page.locator('text=Open MCT My Items >> span').nth(3).click(); | ||||
|  | ||||
|         // Select Folder Object and select Move from context menu | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator(`a:has-text("${folder}")`).click() | ||||
|         ]); | ||||
|         await page.locator('.c-tree__item.is-navigated-object .c-tree__item__label .c-tree__item__type-icon').click({ | ||||
|             button: 'right' | ||||
|         }); | ||||
|         await page.locator('li.icon-move').click(); | ||||
|  | ||||
|         // See if it's possible to put the folder in the Telemetry object after creation | ||||
|         await page.locator('text=Location Open MCT My Items >> span').nth(3).click(); | ||||
|         await page.locator(`form[name="mctForm"] >> text=${telemetryTable}`).click(); | ||||
|         let okButton2 = await page.locator('button.c-button.c-button--major:has-text("OK")'); | ||||
|         let okButtonStateDisabled2 = await okButton2.isDisabled(); | ||||
|         expect(okButtonStateDisabled2).toBeTruthy(); | ||||
|     }); | ||||
| }); | ||||
|   | ||||
							
								
								
									
										177
									
								
								e2e/tests/performance/imagery.perf.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										177
									
								
								e2e/tests/performance/imagery.perf.spec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,177 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* | ||||
| This test suite is dedicated to performance tests to ensure that testability of performance | ||||
| is not broken upstream on Open MCT. Any assumptions made downstream will be tested here | ||||
|  | ||||
| TODO: | ||||
|  - Update resolution of performance config | ||||
|  - Add Performance Observer on init to push all performance marks | ||||
|  - Move client CDP connection to before or to a fixture | ||||
|  - | ||||
|  | ||||
| */ | ||||
|  | ||||
| const { test, expect } = require('@playwright/test'); | ||||
|  | ||||
| const filePath = 'e2e/test-data/PerformanceDisplayLayout.json'; | ||||
|  | ||||
| test.describe('Performance tests', () => { | ||||
|     test.beforeEach(async ({ page, browser }, testInfo) => { | ||||
|         // Go to baseURL | ||||
|         await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|         // Click a:has-text("My Items") | ||||
|         await page.locator('a:has-text("My Items")').click({ | ||||
|             button: 'right' | ||||
|         }); | ||||
|  | ||||
|         // Click text=Import from JSON | ||||
|         await page.locator('text=Import from JSON').click(); | ||||
|  | ||||
|         // Upload Performance Display Layout.json | ||||
|         await page.setInputFiles('#fileElem', filePath); | ||||
|  | ||||
|         // Click text=OK | ||||
|         await page.locator('text=OK').click(); | ||||
|  | ||||
|         await expect(page.locator('a:has-text("Performance Display Layout Display Layout")')).toBeVisible(); | ||||
|  | ||||
|         //Create a Chrome Performance Timeline trace to store as a test artifact | ||||
|         console.log("\n==== Devtools: startTracing ====\n"); | ||||
|         await browser.startTracing(page, { | ||||
|             path: `${testInfo.outputPath()}-trace.json`, | ||||
|             screenshots: true | ||||
|         }); | ||||
|     }); | ||||
|     test.afterEach(async ({ page, browser}) => { | ||||
|         console.log("\n==== Devtools: stopTracing ====\n"); | ||||
|         await browser.stopTracing(); | ||||
|  | ||||
|         /* Measurement Section | ||||
|         / The following section includes a block of performance measurements. | ||||
|         */ | ||||
|         //Get time difference between viewlarge actionability and evaluate time | ||||
|         await page.evaluate(() => (window.performance.measure("machine-time-difference", "viewlarge.start", "viewLarge.start.test"))); | ||||
|  | ||||
|         //Get StartTime | ||||
|         const startTime = await page.evaluate(() => window.performance.timing.navigationStart); | ||||
|         console.log('window.performance.timing.navigationStart', startTime); | ||||
|  | ||||
|         //Get All Performance Marks | ||||
|         const getAllMarksJson = await page.evaluate(() => JSON.stringify(window.performance.getEntriesByType("mark"))); | ||||
|         const getAllMarks = JSON.parse(getAllMarksJson); | ||||
|         console.log('window.performance.getEntriesByType("mark")', getAllMarks); | ||||
|  | ||||
|         //Get All Performance Measures | ||||
|         const getAllMeasuresJson = await page.evaluate(() => JSON.stringify(window.performance.getEntriesByType("measure"))); | ||||
|         const getAllMeasures = JSON.parse(getAllMeasuresJson); | ||||
|         console.log('window.performance.getEntriesByType("measure")', getAllMeasures); | ||||
|  | ||||
|     }); | ||||
|     /* The following test will navigate to a previously created Performance Display Layout and measure the | ||||
|     /  following metrics: | ||||
|     /  - ElementResourceTiming | ||||
|     /  - Interaction Timing | ||||
|     */ | ||||
|     test('Embedded View Large for Imagery is performant in Fixed Time', async ({ page, browser }) => { | ||||
|         const client = await page.context().newCDPSession(page); | ||||
|         // Tell the DevTools session to record performance metrics | ||||
|         // https://chromedevtools.github.io/devtools-protocol/tot/Performance/#method-getMetrics | ||||
|         await client.send('Performance.enable'); | ||||
|         // Go to baseURL | ||||
|         await page.goto('/'); | ||||
|  | ||||
|         // Search Available after Launch | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); | ||||
|         await page.evaluate(() => window.performance.mark("search-available")); | ||||
|         // Fill Search input | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Performance Display Layout'); | ||||
|         await page.evaluate(() => window.performance.mark("search-entered")); | ||||
|         //Search Result Appears and is clicked | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('a:has-text("Performance Display Layout")').first().click(), | ||||
|             page.evaluate(() => window.performance.mark("click-search-result")) | ||||
|         ]); | ||||
|  | ||||
|         //Time to Example Imagery Frame loads within Display Layout | ||||
|         await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible'}); | ||||
|         //Time to Example Imagery object loads | ||||
|         await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible'}); | ||||
|  | ||||
|         //Get background-image url from background-image css prop | ||||
|         const backgroundImage = await page.locator('.c-imagery__main-image__background-image'); | ||||
|         let backgroundImageUrl = await backgroundImage.evaluate((el) => { | ||||
|             return window.getComputedStyle(el).getPropertyValue('background-image').match(/url\(([^)]+)\)/)[1]; | ||||
|         }); | ||||
|         backgroundImageUrl = backgroundImageUrl.slice(1, -1); //forgive me, padre | ||||
|         console.log('backgroundImageurl ' + backgroundImageUrl); | ||||
|  | ||||
|         //Get ResourceTiming of background-image jpg | ||||
|         const resourceTimingJson = await page.evaluate((bgImageUrl) => | ||||
|             JSON.stringify(window.performance.getEntriesByName(bgImageUrl).pop()), | ||||
|         backgroundImageUrl | ||||
|         ); | ||||
|         console.log('resourceTimingJson ' + resourceTimingJson); | ||||
|  | ||||
|         //Open Large view | ||||
|         await page.locator('button:has-text("Large View")').click(); //This action includes the performance.mark named 'viewLarge.start' | ||||
|         await page.evaluate(() => window.performance.mark("viewLarge.start.test")); //This is a mark only to compare evaluate timing | ||||
|  | ||||
|         //Time to Imagery Rendered in Large Frame | ||||
|         await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible'}); | ||||
|         await page.evaluate(() => window.performance.mark("background-image-frame")); | ||||
|  | ||||
|         //Time to Example Imagery object loads | ||||
|         await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible'}); | ||||
|         await page.evaluate(() => window.performance.mark("background-image-visible")); | ||||
|  | ||||
|         // Get Current number of images in thumbstrip | ||||
|         await page.waitForSelector('.c-imagery__thumb'); | ||||
|         const thumbCount = await page.locator('.c-imagery__thumb').count(); | ||||
|         console.log('number of thumbs rendered ' + thumbCount); | ||||
|         await page.locator('.c-imagery__thumb').last().click(); | ||||
|  | ||||
|         //Get ResourceTiming of all jpg resources | ||||
|         const resourceTimingJson2 = await page.evaluate(() => | ||||
|             JSON.stringify(window.performance.getEntriesByType('resource')) | ||||
|         ); | ||||
|         const resourceTiming = JSON.parse(resourceTimingJson2); | ||||
|         const jpgResourceTiming = resourceTiming.find((element) => | ||||
|             element.name.includes('.jpg') | ||||
|         ); | ||||
|         console.log('jpgResourceTiming ' + JSON.stringify(jpgResourceTiming)); | ||||
|  | ||||
|         // Click Close Icon | ||||
|         await page.locator('[aria-label="Close"]').click(); | ||||
|         await page.evaluate(() => window.performance.mark("view-large-close-button")); | ||||
|  | ||||
|         //await client.send('HeapProfiler.enable'); | ||||
|         await client.send('HeapProfiler.collectGarbage'); | ||||
|  | ||||
|         let performanceMetrics = await client.send('Performance.getMetrics'); | ||||
|         console.log(performanceMetrics.metrics); | ||||
|  | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										119
									
								
								e2e/tests/performance/memleak-imagery.perf.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								e2e/tests/performance/memleak-imagery.perf.spec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,119 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* | ||||
| This test suite is an initial example for memory leak testing using performance. This configuration and execution must | ||||
| be kept separate from the traditional performance measurements to avoid any "observer" effects associated with tracing | ||||
| or profiling playwright and/or the browser. | ||||
|  | ||||
| Based on a pattern identified in https://github.com/trentmwillis/devtools-protocol-demos/blob/master/testing-demos/memory-leak-by-heap.js | ||||
| and https://github.com/paulirish/automated-chrome-profiling/issues/3 | ||||
|  | ||||
| Best path forward: https://github.com/cowchimp/headless-devtools/blob/master/src/Memory/example.js | ||||
|  | ||||
| */ | ||||
|  | ||||
| const { test, expect } = require('@playwright/test'); | ||||
|  | ||||
| const filePath = 'e2e/test-data/PerformanceDisplayLayout.json'; | ||||
|  | ||||
| // eslint-disable-next-line playwright/no-skipped-test | ||||
| test.describe.skip('Memory Performance tests', () => { | ||||
|     test.beforeEach(async ({ page, browser }, testInfo) => { | ||||
|         // Go to baseURL | ||||
|         await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|         // Click a:has-text("My Items") | ||||
|         await page.locator('a:has-text("My Items")').click({ | ||||
|             button: 'right' | ||||
|         }); | ||||
|  | ||||
|         // Click text=Import from JSON | ||||
|         await page.locator('text=Import from JSON').click(); | ||||
|  | ||||
|         // Upload Performance Display Layout.json | ||||
|         await page.setInputFiles('#fileElem', filePath); | ||||
|  | ||||
|         // Click text=OK | ||||
|         await page.locator('text=OK').click(); | ||||
|  | ||||
|         await expect(page.locator('a:has-text("Performance Display Layout Display Layout")')).toBeVisible(); | ||||
|     }); | ||||
|  | ||||
|     test('Embedded View Large for Imagery is performant in Fixed Time', async ({ page, browser }) => { | ||||
|  | ||||
|         await page.goto('/', {waitUntil: 'networkidle'}); | ||||
|  | ||||
|         // To to Search Available after Launch | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); | ||||
|         // Fill Search input | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Performance Display Layout'); | ||||
|         //Search Result Appears and is clicked | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('a:has-text("Performance Display Layout")').first().click() | ||||
|         ]); | ||||
|  | ||||
|         //Time to Example Imagery Frame loads within Display Layout | ||||
|         await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible'}); | ||||
|         //Time to Example Imagery object loads | ||||
|         await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible'}); | ||||
|  | ||||
|         const client = await page.context().newCDPSession(page); | ||||
|         await client.send('HeapProfiler.enable'); | ||||
|         await client.send('HeapProfiler.startSampling'); | ||||
|         // await client.send('HeapProfiler.collectGarbage'); | ||||
|         await client.send('Performance.enable'); | ||||
|  | ||||
|         let performanceMetricsBefore = await client.send('Performance.getMetrics'); | ||||
|         console.log(performanceMetricsBefore.metrics); | ||||
|  | ||||
|         //await client.send('Performance.disable'); | ||||
|  | ||||
|         //Open Large view | ||||
|         await page.locator('button:has-text("Large View")').click(); | ||||
|         await client.send('HeapProfiler.takeHeapSnapshot'); | ||||
|  | ||||
|         //Time to Imagery Rendered in Large Frame | ||||
|         await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible'}); | ||||
|  | ||||
|         //Time to Example Imagery object loads | ||||
|         await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible'}); | ||||
|  | ||||
|         // Click Close Icon | ||||
|         await page.locator('.c-click-icon').click(); | ||||
|  | ||||
|         //Time to Example Imagery Frame loads within Display Layout | ||||
|         await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible'}); | ||||
|         //Time to Example Imagery object loads | ||||
|         await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible'}); | ||||
|  | ||||
|         await client.send('HeapProfiler.collectGarbage'); | ||||
|         //await client.send('Performance.enable'); | ||||
|  | ||||
|         let performanceMetricsAfter = await client.send('Performance.getMetrics'); | ||||
|         console.log(performanceMetricsAfter.metrics); | ||||
|  | ||||
|         //await client.send('Performance.disable'); | ||||
|  | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										158
									
								
								e2e/tests/performance/notebook.perf.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										158
									
								
								e2e/tests/performance/notebook.perf.spec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,158 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* | ||||
| This test suite is dedicated to performance tests to ensure that testability of performance | ||||
| is not broken upstream on Open MCT. Any assumptions made downstream will be tested here. | ||||
|  | ||||
| TODO: | ||||
|  - Update resolution of performance config | ||||
|  - Add Performance Observer on init to push all performance marks | ||||
|  - Move client CDP connection to before or to a fixture | ||||
|  | ||||
| */ | ||||
|  | ||||
| const { test, expect } = require('@playwright/test'); | ||||
|  | ||||
| const notebookFilePath = 'e2e/test-data/PerformanceNotebook.json'; | ||||
|  | ||||
| test.describe('Performance tests', () => { | ||||
|     test.beforeEach(async ({ page, browser }, testInfo) => { | ||||
|         // Go to baseURL | ||||
|         await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|         // Click a:has-text("My Items") | ||||
|         await page.locator('a:has-text("My Items")').click({ | ||||
|             button: 'right' | ||||
|         }); | ||||
|  | ||||
|         // Click text=Import from JSON | ||||
|         await page.locator('text=Import from JSON').click(); | ||||
|  | ||||
|         // Upload Performance Display Layout.json | ||||
|         await page.setInputFiles('#fileElem', notebookFilePath); | ||||
|  | ||||
|         // TODO Fix this | ||||
|         await page.locator('text=OK >> nth=1').click(); | ||||
|  | ||||
|         await expect(page.locator('a:has-text("Performance Notebook")')).toBeVisible(); | ||||
|  | ||||
|         //Create a Chrome Performance Timeline trace to store as a test artifact | ||||
|         console.log("\n==== Devtools: startTracing ====\n"); | ||||
|         await browser.startTracing(page, { | ||||
|             path: `${testInfo.outputPath()}-trace.json`, | ||||
|             screenshots: true | ||||
|         }); | ||||
|     }); | ||||
|     test.afterEach(async ({ page, browser}) => { | ||||
|         console.log("\n==== Devtools: stopTracing ====\n"); | ||||
|         await browser.stopTracing(); | ||||
|  | ||||
|         /* Measurement Section | ||||
|         / The following section includes a block of performance measurements. | ||||
|         */ | ||||
|         const startTime = await page.evaluate(() => window.performance.timing.navigationStart); | ||||
|         console.log('window.performance.timing.navigationStart', startTime); | ||||
|  | ||||
|         //Get All Performance Marks | ||||
|         const getAllMarksJson = await page.evaluate(() => JSON.stringify(window.performance.getEntriesByType("mark"))); | ||||
|         const getAllMarks = JSON.parse(getAllMarksJson); | ||||
|         console.log('window.performance.getEntriesByType("mark")', getAllMarks); | ||||
|  | ||||
|         //Get All Performance Measures | ||||
|         const getAllMeasuresJson = await page.evaluate(() => JSON.stringify(window.performance.getEntriesByType("measure"))); | ||||
|         const getAllMeasures = JSON.parse(getAllMeasuresJson); | ||||
|         console.log('window.performance.getEntriesByType("measure")', getAllMeasures); | ||||
|  | ||||
|     }); | ||||
|     /* The following test will navigate to a previously created Performance Display Layout and measure the | ||||
|     /  following metrics: | ||||
|     /  - ElementResourceTiming | ||||
|     /  - Interaction Timing | ||||
|     */ | ||||
|     test('Notebook Search, Add Entry, Update Entry are performant', async ({ page, browser }) => { | ||||
|         const client = await page.context().newCDPSession(page); | ||||
|         // Tell the DevTools session to record performance metrics | ||||
|         // https://chromedevtools.github.io/devtools-protocol/tot/Performance/#method-getMetrics | ||||
|         await client.send('Performance.enable'); | ||||
|         // Go to baseURL | ||||
|         await page.goto('/'); | ||||
|  | ||||
|         // To to Search Available after Launch | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); | ||||
|         await page.evaluate(() => window.performance.mark("search-available")); | ||||
|         // Fill Search input | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Performance Notebook'); | ||||
|         await page.evaluate(() => window.performance.mark("search-entered")); | ||||
|         //Search Result Appears and is clicked | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('a:has-text("Performance Notebook")').first().click(), | ||||
|             page.evaluate(() => window.performance.mark("click-search-result")) | ||||
|         ]); | ||||
|  | ||||
|         await page.waitForSelector('.c-tree__item c-tree-and-search__loading loading', {state: 'hidden'}); | ||||
|         await page.evaluate(() => window.performance.mark("search-spinner-gone")); | ||||
|  | ||||
|         await page.waitForSelector('.l-browse-bar__object-name', { state: 'visible'}); | ||||
|         await page.evaluate(() => window.performance.mark("object-title-appears")); | ||||
|  | ||||
|         await page.waitForSelector('.c-notebook__entry >> nth=0', { state: 'visible'}); | ||||
|         await page.evaluate(() => window.performance.mark("notebook-entry-appears")); | ||||
|  | ||||
|         // Click Add new Notebook Entry | ||||
|         await page.locator('.c-notebook__drag-area').click(); | ||||
|         await page.evaluate(() => window.performance.mark("new-notebook-entry-created")); | ||||
|  | ||||
|         // Enter Notebook Entry text | ||||
|         await page.locator('div.c-ne__text').last().fill('New Entry'); | ||||
|         await page.keyboard.press('Enter'); | ||||
|         await page.evaluate(() => window.performance.mark("new-notebook-entry-filled")); | ||||
|  | ||||
|         //Individual Notebook Entry Search | ||||
|         await page.evaluate(() => window.performance.mark("notebook-search-start")); | ||||
|         await page.locator('.c-notebook__search >> input').fill('Existing Entry'); | ||||
|         await page.evaluate(() => window.performance.mark("notebook-search-filled")); | ||||
|         await page.waitForSelector('text=Search Results (3)', { state: 'visible'}); | ||||
|         await page.evaluate(() => window.performance.mark("notebook-search-processed")); | ||||
|         await page.waitForSelector('.c-notebook__entry >> nth=2', { state: 'visible'}); | ||||
|         await page.evaluate(() => window.performance.mark("notebook-search-processed")); | ||||
|  | ||||
|         //Clear Search | ||||
|         await page.locator('.c-search.c-notebook__search .c-search__clear-input').click(); | ||||
|         await page.evaluate(() => window.performance.mark("notebook-search-processed")); | ||||
|  | ||||
|         // Hover on Last | ||||
|         await page.evaluate(() => window.performance.mark("new-notebook-entry-delete")); | ||||
|         await page.locator('div.c-ne__time-and-content').last().hover(); | ||||
|         await page.locator('button[title="Delete this entry"]').last().click(); | ||||
|         await page.locator('button:has-text("Ok")').click(); | ||||
|         await page.waitForSelector('.c-notebook__entry >> nth=3', { state: 'detached'}); | ||||
|         await page.evaluate(() => window.performance.mark("new-notebook-entry-deleted")); | ||||
|  | ||||
|         //await client.send('HeapProfiler.enable'); | ||||
|         await client.send('HeapProfiler.collectGarbage'); | ||||
|  | ||||
|         let performanceMetrics = await client.send('Performance.getMetrics'); | ||||
|         console.log(performanceMetrics.metrics); | ||||
|     }); | ||||
| }); | ||||
| @@ -24,12 +24,11 @@ | ||||
| This test suite is dedicated to tests which verify the basic operations surrounding conditionSets. | ||||
| */ | ||||
|  | ||||
| const { test, expect } = require('@playwright/test'); | ||||
| const { test } = require('../../fixtures.js'); | ||||
| const { expect } = require('@playwright/test'); | ||||
| const path = require('path'); | ||||
|  | ||||
| // https://github.com/nasa/openmct/issues/4323#issuecomment-1067282651 | ||||
|  | ||||
| test.describe('Persistence operations', () => { | ||||
| test.describe('Persistence operations @addInit', () => { | ||||
|     // add non persistable root item | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         // eslint-disable-next-line no-undef | ||||
| @@ -37,6 +36,10 @@ test.describe('Persistence operations', () => { | ||||
|     }); | ||||
|  | ||||
|     test('Persistability should be respected in the create form location field', async ({ page }) => { | ||||
|         test.info().annotations.push({ | ||||
|             type: 'issue', | ||||
|             description: 'https://github.com/nasa/openmct/issues/4323' | ||||
|         }); | ||||
|         // Go to baseURL | ||||
|         await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|   | ||||
| @@ -24,7 +24,10 @@ | ||||
| This test suite is dedicated to tests which verify the basic operations surrounding exportAsJSON. | ||||
| */ | ||||
|  | ||||
| const { test, expect } = require('@playwright/test'); | ||||
| const { test } = require('../../../fixtures.js'); | ||||
| // FIXME: Remove this eslint exception once tests are implemented | ||||
| // eslint-disable-next-line no-unused-vars | ||||
| const { expect } = require('@playwright/test'); | ||||
|  | ||||
| test.describe('ExportAsJSON', () => { | ||||
|     test.fixme('Create a basic object and verify that it can be exported as JSON from Tree', async ({ page }) => { | ||||
|   | ||||
| @@ -24,7 +24,10 @@ | ||||
| This test suite is dedicated to tests which verify the basic operations surrounding importAsJSON. | ||||
| */ | ||||
|  | ||||
| const { test, expect } = require('@playwright/test'); | ||||
| const { test } = require('../../../fixtures.js'); | ||||
| // FIXME: Remove this eslint exception once tests are implemented | ||||
| // eslint-disable-next-line no-unused-vars | ||||
| const { expect } = require('@playwright/test'); | ||||
|  | ||||
| test.describe('ExportAsJSON', () => { | ||||
|     test.fixme('Verify that domain object can be importAsJSON from Tree', async ({ page }) => { | ||||
|   | ||||
| @@ -24,7 +24,8 @@ | ||||
| This test suite is dedicated to tests which verify the basic operations surrounding Clock. | ||||
| */ | ||||
| 
 | ||||
| const { test, expect } = require('@playwright/test'); | ||||
| const { test } = require('../../../fixtures.js'); | ||||
| const { expect } = require('@playwright/test'); | ||||
| 
 | ||||
| test.describe('Clock Generator', () => { | ||||
| 
 | ||||
| @@ -45,22 +46,22 @@ test.describe('Clock Generator', () => { | ||||
|         // Click .icon-arrow-down
 | ||||
|         await page.locator('.icon-arrow-down').click(); | ||||
|         //verify if the autocomplete dropdown is visible
 | ||||
|         await expect(page.locator(".optionPreSelected")).toBeVisible(); | ||||
|         await expect(page.locator(".c-input--autocomplete__options")).toBeVisible(); | ||||
|         // Click .icon-arrow-down
 | ||||
|         await page.locator('.icon-arrow-down').click(); | ||||
| 
 | ||||
|         // Verify clicking on the autocomplete arrow collapses the dropdown
 | ||||
|         await expect(page.locator(".optionPreSelected")).not.toBeVisible(); | ||||
|         await expect(page.locator(".c-input--autocomplete__options")).not.toBeVisible(); | ||||
| 
 | ||||
|         // Click timezone input to open dropdown
 | ||||
|         await page.locator('.autocompleteInput').click(); | ||||
|         await page.locator('.c-input--autocomplete__input').click(); | ||||
|         //verify if the autocomplete dropdown is visible
 | ||||
|         await expect(page.locator(".optionPreSelected")).toBeVisible(); | ||||
|         await expect(page.locator(".c-input--autocomplete__options")).toBeVisible(); | ||||
| 
 | ||||
|         // Verify clicking outside the autocomplete dropdown collapses it
 | ||||
|         await page.locator('text=Timezone').click(); | ||||
|         // Verify clicking on the autocomplete arrow collapses the dropdown
 | ||||
|         await expect(page.locator(".optionPreSelected")).not.toBeVisible(); | ||||
|         await expect(page.locator(".c-input--autocomplete__options")).not.toBeVisible(); | ||||
| 
 | ||||
|     }); | ||||
| }); | ||||
| @@ -21,13 +21,21 @@ | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* | ||||
| This test suite is dedicated to tests which verify the basic operations surrounding conditionSets. | ||||
| This test suite is dedicated to tests which verify the basic operations surrounding conditionSets. Note: this | ||||
| suite is sharing state between tests which is considered an anti-pattern. Implimenting in this way to | ||||
| demonstrate some playwright for test developers. This pattern should not be re-used in other CRUD suites. | ||||
| */ | ||||
|  | ||||
| const { test, expect } = require('@playwright/test'); | ||||
| const { test } = require('../../../fixtures.js'); | ||||
| const { expect } = require('@playwright/test'); | ||||
|  | ||||
| test.describe('Condition Set Operations', () => { | ||||
|     test('Create new button `condition set` creates new condition object', async ({ page }) => { | ||||
| let conditionSetUrl; | ||||
| let getConditionSetIdentifierFromUrl; | ||||
|  | ||||
| test.describe.serial('Condition Set CRUD Operations on @localStorage', () => { | ||||
|     test.beforeAll(async ({ browser}) => { | ||||
|         const context = await browser.newContext(); | ||||
|         const page = await context.newPage(); | ||||
|         //Go to baseURL | ||||
|         await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
| @@ -35,31 +43,140 @@ test.describe('Condition Set Operations', () => { | ||||
|         await page.click('button:has-text("Create")'); | ||||
|  | ||||
|         // Click text=Condition Set | ||||
|         await page.click('text=Condition Set'); | ||||
|         await page.locator('li:has-text("Condition Set")').click(); | ||||
|  | ||||
|         // Click text=OK | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine/dab945d4-5a84-480e-8180-222b4aa730fa?tc.mode=fixed&tc.startBound=1639696164435&tc.endBound=1639697964435&tc.timeSystem=utc&view=conditionSet.view' }*/), | ||||
|             page.waitForNavigation(), | ||||
|             page.click('text=OK') | ||||
|         ]); | ||||
|  | ||||
|         await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set'); | ||||
|         //Save localStorage for future test execution | ||||
|         await context.storageState({ path: './e2e/test-data/recycled_local_storage.json' }); | ||||
|  | ||||
|         //Set object identifier from url | ||||
|         conditionSetUrl = page.url(); | ||||
|         console.log('conditionSetUrl ' + conditionSetUrl); | ||||
|  | ||||
|         getConditionSetIdentifierFromUrl = conditionSetUrl.split('/').pop().split('?')[0]; | ||||
|         console.debug('getConditionSetIdentifierFromUrl ' + getConditionSetIdentifierFromUrl); | ||||
|         await page.close(); | ||||
|     }); | ||||
|     test.fixme('condition set object properties exist', async ({ page }) => { | ||||
|         //Go to object created in step one | ||||
|         //Verify the Condition Set properties persist on Save | ||||
|         //Verify the Condition Set properties persist on page.reload() | ||||
|  | ||||
|     //Load localStorage for subsequent tests | ||||
|     test.use({ storageState: './e2e/test-data/recycled_local_storage.json' }); | ||||
|     //Begin suite of tests again localStorage | ||||
|     test('Condition set object properties persist in main view and inspector @localStorage', async ({ page }) => { | ||||
|         //Navigate to baseURL with injected localStorage | ||||
|         await page.goto(conditionSetUrl, { waitUntil: 'networkidle' }); | ||||
|  | ||||
|         //Assertions on loaded Condition Set in main view. This is a stateful transition step after page.goto() | ||||
|         await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set'); | ||||
|  | ||||
|         //Assertions on loaded Condition Set in Inspector | ||||
|         expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy(); | ||||
|  | ||||
|         //Reload Page | ||||
|         await Promise.all([ | ||||
|             page.reload(), | ||||
|             page.waitForLoadState('networkidle') | ||||
|         ]); | ||||
|  | ||||
|         //Re-verify after reload | ||||
|         await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set'); | ||||
|         //Assertions on loaded Condition Set in Inspector | ||||
|         expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy(); | ||||
|  | ||||
|     }); | ||||
|     test.fixme('condition set object can be modified', async ({ page }) => { | ||||
|         //Go to object created in step one | ||||
|     test('condition set object can be modified on @localStorage', async ({ page }) => { | ||||
|         await page.goto(conditionSetUrl, { waitUntil: 'networkidle' }); | ||||
|  | ||||
|         //Assertions on loaded Condition Set in main view. This is a stateful transition step after page.goto() | ||||
|         await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set'); | ||||
|  | ||||
|         //Update the Condition Set properties | ||||
|         //Verify the Condition Set properties persist on Save | ||||
|         //Verify the Condition Set properties persist on page.reload() | ||||
|         // Click Edit Button | ||||
|         await page.locator('text=Conditions View Snapshot >> button').nth(3).click(); | ||||
|  | ||||
|         //Edit Condition Set Name from main view | ||||
|         await page.locator('text=Unnamed Condition Set').first().fill('Renamed Condition Set'); | ||||
|         await page.locator('text=Renamed Condition Set').first().press('Enter'); | ||||
|         // Click Save Button | ||||
|         await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click(); | ||||
|         // Click Save and Finish Editing Option | ||||
|         await page.locator('text=Save and Finish Editing').click(); | ||||
|  | ||||
|         //Verify Main section reflects updated Name Property | ||||
|         await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Renamed Condition Set'); | ||||
|  | ||||
|         // Verify Inspector properties | ||||
|         // Verify Inspector has updated Name property | ||||
|         expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy(); | ||||
|         // Verify Inspector Details has updated Name property | ||||
|         expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy(); | ||||
|  | ||||
|         // Verify Tree reflects updated Name proprety | ||||
|         // Expand Tree | ||||
|         await page.locator('text=Open MCT My Items >> span >> nth=3').click(); | ||||
|         // Verify Condition Set Object is renamed in Tree | ||||
|         expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); | ||||
|         // Verify Search Tree reflects renamed Name property | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Renamed'); | ||||
|         expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); | ||||
|  | ||||
|         //Reload Page | ||||
|         await Promise.all([ | ||||
|             page.reload(), | ||||
|             page.waitForLoadState('networkidle') | ||||
|         ]); | ||||
|  | ||||
|         //Verify Main section reflects updated Name Property | ||||
|         await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Renamed Condition Set'); | ||||
|  | ||||
|         // Verify Inspector properties | ||||
|         // Verify Inspector has updated Name property | ||||
|         expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy(); | ||||
|         // Verify Inspector Details has updated Name property | ||||
|         expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy(); | ||||
|  | ||||
|         // Verify Tree reflects updated Name proprety | ||||
|         // Expand Tree | ||||
|         await page.locator('text=Open MCT My Items >> span >> nth=3').click(); | ||||
|         // Verify Condition Set Object is renamed in Tree | ||||
|         expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); | ||||
|         // Verify Search Tree reflects renamed Name property | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Renamed'); | ||||
|         expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); | ||||
|     }); | ||||
|     test.fixme('condition set object can be deleted', async ({ page }) => { | ||||
|         //Go to object created in step one | ||||
|         //Verify that Condition Set object can be deleted | ||||
|         //Verify the Condition Set object does not exist in Tree | ||||
|         //Verify the Condition Set object does not exist with direct navigation to object's URL | ||||
|     test('condition set object can be deleted by Search Tree Actions menu on @localStorage', async ({ page }) => { | ||||
|         //Navigate to baseURL | ||||
|         await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|         //Assertions on loaded Condition Set in main view. This is a stateful transition step after page.goto() | ||||
|         await expect(page.locator('a:has-text("Unnamed Condition Set Condition Set") >> nth=0')).toBeVisible(); | ||||
|  | ||||
|         const numberOfConditionSetsToStart = await page.locator('a:has-text("Unnamed Condition Set Condition Set")').count(); | ||||
|  | ||||
|         // Search for Unnamed Condition Set | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Unnamed Condition Set'); | ||||
|         // Click Search Result | ||||
|         await page.locator('[aria-label="OpenMCT Search"] >> text=Unnamed Condition Set').first().click(); | ||||
|         // Click hamburger button | ||||
|         await page.locator('[title="More options"]').click(); | ||||
|  | ||||
|         // Click text=Remove | ||||
|         await page.locator('text=Remove').click(); | ||||
|         await page.locator('text=OK').click(); | ||||
|  | ||||
|         //Expect Unnamed Condition Set to be removed in Main View | ||||
|         const numberOfConditionSetsAtEnd = await page.locator('a:has-text("Unnamed Condition Set Condition Set")').count(); | ||||
|  | ||||
|         expect(numberOfConditionSetsAtEnd).toEqual(numberOfConditionSetsToStart - 1); | ||||
|  | ||||
|         //Feature? | ||||
|         //Domain Object is still available by direct URL after delete | ||||
|         await page.goto(conditionSetUrl, { waitUntil: 'networkidle' }); | ||||
|         await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set'); | ||||
|  | ||||
|     }); | ||||
| }); | ||||
|   | ||||
| @@ -24,13 +24,18 @@ | ||||
| This test suite is dedicated to tests which verify the basic operations surrounding imagery, | ||||
| but only assume that example imagery is present. | ||||
| */ | ||||
| /* globals process */ | ||||
|  | ||||
| const { test, expect } = require('@playwright/test'); | ||||
| const { test } = require('../../../fixtures.js'); | ||||
| const { expect } = require('@playwright/test'); | ||||
| const { waitForAnimations } = require('../../../commonActions.js'); | ||||
|  | ||||
| test.describe('Example Imagery', () => { | ||||
| const backgroundImageSelector = '.c-imagery__main-image__background-image'; | ||||
|  | ||||
| //The following block of tests verifies the basic functionality of example imagery and serves as a template for Imagery objects embedded in other objects. | ||||
| test.describe('Example Imagery Object', () => { | ||||
|  | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         page.on('console', msg => console.log(msg.text())) | ||||
|         //Go to baseURL | ||||
|         await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
| @@ -42,48 +47,63 @@ test.describe('Example Imagery', () => { | ||||
|  | ||||
|         // Click text=OK | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine/dab945d4-5a84-480e-8180-222b4aa730fa?tc.mode=fixed&tc.startBound=1639696164435&tc.endBound=1639697964435&tc.timeSystem=utc&view=conditionSet.view' }*/), | ||||
|             page.click('text=OK') | ||||
|             page.waitForNavigation({waitUntil: 'networkidle'}), | ||||
|             page.click('text=OK'), | ||||
|             //Wait for Save Banner to appear | ||||
|             page.waitForSelector('.c-message-banner__message') | ||||
|         ]); | ||||
|         // Close Banner | ||||
|         await page.locator('.c-message-banner__close-button').click(); | ||||
|  | ||||
|         //Wait until Save Banner is gone | ||||
|         await page.waitForSelector('.c-message-banner__message', { state: 'detached'}); | ||||
|         await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery'); | ||||
|         await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|     }); | ||||
|  | ||||
|     const backgroundImageSelector = '.c-imagery__main-image__background-image'; | ||||
|     test('Can use Mouse Wheel to zoom in and out of latest image', async ({ page }) => { | ||||
|         const bgImageLocator = await page.locator(backgroundImageSelector); | ||||
|         const deltaYStep = 100; //equivalent to 1x zoom | ||||
|         await bgImageLocator.hover(); | ||||
|         await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|         const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|         // zoom in | ||||
|         await bgImageLocator.hover(); | ||||
|         await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|         await page.mouse.wheel(0, deltaYStep * 2); | ||||
|         // wait for zoom animation to finish | ||||
|         await bgImageLocator.hover(); | ||||
|         await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|         const imageMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|         // zoom out | ||||
|         await bgImageLocator.hover(); | ||||
|         await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|         await page.mouse.wheel(0, -deltaYStep); | ||||
|         // wait for zoom animation to finish | ||||
|         await bgImageLocator.hover(); | ||||
|         await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|         const imageMouseZoomedOut = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|  | ||||
|         expect(imageMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height); | ||||
|         expect(imageMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width); | ||||
|         expect(imageMouseZoomedOut.height).toBeLessThan(imageMouseZoomedIn.height); | ||||
|         expect(imageMouseZoomedOut.width).toBeLessThan(imageMouseZoomedIn.width); | ||||
|     }); | ||||
|  | ||||
|     test('Can adjust image brightness/contrast by dragging the sliders', async ({ page, browserName }) => { | ||||
|         test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox'); | ||||
|         // Open the image filter menu | ||||
|         await page.locator('[role=toolbar] button[title="Brightness and contrast"]').click(); | ||||
|  | ||||
|         // Drag the brightness and contrast sliders around and assert filter values | ||||
|         await dragBrightnessSliderAndAssertFilterValues(page); | ||||
|         await dragContrastSliderAndAssertFilterValues(page); | ||||
|     }); | ||||
|  | ||||
|     test('Can use alt+drag to move around image once zoomed in', async ({ page }) => { | ||||
|         const deltaYStep = 100; //equivalent to 1x zoom | ||||
|         const panHotkey = process.platform === 'linux' ? ['Control', 'Alt'] : ['Alt']; | ||||
|  | ||||
|         await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|  | ||||
|         const bgImageLocator = await page.locator(backgroundImageSelector); | ||||
|         await bgImageLocator.hover(); | ||||
|         // zoom in | ||||
|         await page.mouse.wheel(0, deltaYStep * 2); | ||||
|         await bgImageLocator.hover(); | ||||
|         const zoomedBoundingBox = await bgImageLocator.boundingBox(); | ||||
|         await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|         const zoomedBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|         const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2; | ||||
|         const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2; | ||||
|         // move to the right | ||||
| @@ -91,127 +111,707 @@ test.describe('Example Imagery', () => { | ||||
|         // center the mouse pointer | ||||
|         await page.mouse.move(imageCenterX, imageCenterY); | ||||
|  | ||||
|         //Get Diagnostic info about process environment | ||||
|         console.log('process.platform is ' + process.platform); | ||||
|         const getUA = await page.evaluate(() => navigator.userAgent); | ||||
|         console.log('navigator.userAgent ' + getUA); | ||||
|         // Pan Imagery Hints | ||||
|         const expectedAltText = process.platform === 'linux' ? 'Ctrl+Alt drag to pan' : 'Alt drag to pan'; | ||||
|         const imageryHintsText = await page.locator('.c-imagery__hints').innerText(); | ||||
|         expect(expectedAltText).toEqual(imageryHintsText); | ||||
|  | ||||
|         // pan right | ||||
|         await page.keyboard.down('Alt'); | ||||
|         await Promise.all(panHotkey.map(x => page.keyboard.down(x))); | ||||
|         await page.mouse.down(); | ||||
|         await page.mouse.move(imageCenterX - 200, imageCenterY, 10); | ||||
|         await page.mouse.up(); | ||||
|         await page.keyboard.up('Alt'); | ||||
|         const afterRightPanBoundingBox = await bgImageLocator.boundingBox(); | ||||
|         await Promise.all(panHotkey.map(x => page.keyboard.up(x))); | ||||
|         const afterRightPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|         expect(zoomedBoundingBox.x).toBeGreaterThan(afterRightPanBoundingBox.x); | ||||
|  | ||||
|         // pan left | ||||
|         await page.keyboard.down('Alt'); | ||||
|         await Promise.all(panHotkey.map(x => page.keyboard.down(x))); | ||||
|         await page.mouse.down(); | ||||
|         await page.mouse.move(imageCenterX, imageCenterY, 10); | ||||
|         await page.mouse.up(); | ||||
|         await page.keyboard.up('Alt'); | ||||
|         const afterLeftPanBoundingBox = await bgImageLocator.boundingBox(); | ||||
|         await Promise.all(panHotkey.map(x => page.keyboard.up(x))); | ||||
|         const afterLeftPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|         expect(afterRightPanBoundingBox.x).toBeLessThan(afterLeftPanBoundingBox.x); | ||||
|  | ||||
|         // pan up | ||||
|         await page.mouse.move(imageCenterX, imageCenterY); | ||||
|         await page.keyboard.down('Alt'); | ||||
|         await Promise.all(panHotkey.map(x => page.keyboard.down(x))); | ||||
|         await page.mouse.down(); | ||||
|         await page.mouse.move(imageCenterX, imageCenterY + 200, 10); | ||||
|         await page.mouse.up(); | ||||
|         await page.keyboard.up('Alt'); | ||||
|         const afterUpPanBoundingBox = await bgImageLocator.boundingBox(); | ||||
|         await Promise.all(panHotkey.map(x => page.keyboard.up(x))); | ||||
|         const afterUpPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|         expect(afterUpPanBoundingBox.y).toBeGreaterThan(afterLeftPanBoundingBox.y); | ||||
|  | ||||
|         // pan down | ||||
|         await page.keyboard.down('Alt'); | ||||
|         await Promise.all(panHotkey.map(x => page.keyboard.down(x))); | ||||
|         await page.mouse.down(); | ||||
|         await page.mouse.move(imageCenterX, imageCenterY - 200, 10); | ||||
|         await page.mouse.up(); | ||||
|         await page.keyboard.up('Alt'); | ||||
|         const afterDownPanBoundingBox = await bgImageLocator.boundingBox(); | ||||
|         await Promise.all(panHotkey.map(x => page.keyboard.up(x))); | ||||
|         const afterDownPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|         expect(afterDownPanBoundingBox.y).toBeLessThan(afterUpPanBoundingBox.y); | ||||
|  | ||||
|     }); | ||||
|  | ||||
|     test('Can use + - buttons to zoom on the image', async ({ page }) => { | ||||
|         const bgImageLocator = await page.locator(backgroundImageSelector); | ||||
|         await bgImageLocator.hover(); | ||||
|         const zoomInBtn = await page.locator('.t-btn-zoom-in'); | ||||
|         const zoomOutBtn = await page.locator('.t-btn-zoom-out'); | ||||
|         const initialBoundingBox = await bgImageLocator.boundingBox(); | ||||
|         // Get initial image dimensions | ||||
|         const initialBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|  | ||||
|         await zoomInBtn.click(); | ||||
|         await zoomInBtn.click(); | ||||
|         // wait for zoom animation to finish | ||||
|         await bgImageLocator.hover(); | ||||
|         const zoomedInBoundingBox = await bgImageLocator.boundingBox(); | ||||
|         // Zoom in twice via button | ||||
|         await zoomIntoImageryByButton(page); | ||||
|         await zoomIntoImageryByButton(page); | ||||
|  | ||||
|         // Get and assert zoomed in image dimensions | ||||
|         const zoomedInBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|         expect(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height); | ||||
|         expect(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width); | ||||
|  | ||||
|         await zoomOutBtn.click(); | ||||
|         // wait for zoom animation to finish | ||||
|         await bgImageLocator.hover(); | ||||
|         const zoomedOutBoundingBox = await bgImageLocator.boundingBox(); | ||||
|         // Zoom out once via button | ||||
|         await zoomOutOfImageryByButton(page); | ||||
|  | ||||
|         // Get and assert zoomed out image dimensions | ||||
|         const zoomedOutBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|         expect(zoomedOutBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height); | ||||
|         expect(zoomedOutBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width); | ||||
|  | ||||
|         // Zoom out again via button, assert against the initial image dimensions | ||||
|         await zoomOutOfImageryByButton(page); | ||||
|         const finalBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|         expect(finalBoundingBox).toEqual(initialBoundingBox); | ||||
|     }); | ||||
|  | ||||
|     test('Can use the reset button to reset the image', async ({ page }) => { | ||||
|         const bgImageLocator = await page.locator(backgroundImageSelector); | ||||
|         await bgImageLocator.hover(); | ||||
|         const zoomInBtn = await page.locator('.t-btn-zoom-in'); | ||||
|         const zoomResetBtn = await page.locator('.t-btn-zoom-reset'); | ||||
|         const initialBoundingBox = await bgImageLocator.boundingBox(); | ||||
|     test('Can use the reset button to reset the image', async ({ page }, testInfo) => { | ||||
|         test.slow(testInfo.project === 'chrome-beta', "This test is slow in chrome-beta"); | ||||
|         // Get initial image dimensions | ||||
|         const initialBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|  | ||||
|         await zoomInBtn.click(); | ||||
|         await zoomInBtn.click(); | ||||
|         // wait for zoom animation to finish | ||||
|         await bgImageLocator.hover(); | ||||
|         const zoomedInBoundingBox = await bgImageLocator.boundingBox(); | ||||
|         expect(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height); | ||||
|         expect(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width); | ||||
|         // Zoom in twice via button | ||||
|         await zoomIntoImageryByButton(page); | ||||
|         await zoomIntoImageryByButton(page); | ||||
|  | ||||
|         await zoomResetBtn.click(); | ||||
|         await bgImageLocator.hover(); | ||||
|         // Get and assert zoomed in image dimensions | ||||
|         const zoomedInBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|         expect.soft(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height); | ||||
|         expect.soft(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width); | ||||
|  | ||||
|         const resetBoundingBox = await bgImageLocator.boundingBox(); | ||||
|         expect(resetBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height); | ||||
|         expect(resetBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width); | ||||
|  | ||||
|         expect(resetBoundingBox.height).toEqual(initialBoundingBox.height); | ||||
|         expect(resetBoundingBox.width).toEqual(initialBoundingBox.width); | ||||
|         // Reset pan and zoom and assert against initial image dimensions | ||||
|         await resetImageryPanAndZoom(page); | ||||
|         const finalBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|         expect(finalBoundingBox).toEqual(initialBoundingBox); | ||||
|     }); | ||||
|  | ||||
|     test('Using the zoom features does not pause telemetry', async ({ page }) => { | ||||
|         const pausePlayButton = page.locator('.c-button.pause-play'); | ||||
|  | ||||
|         // open the time conductor drop down | ||||
|         await page.locator('button:has-text("Fixed Timespan")').click(); | ||||
|  | ||||
|         // Click local clock | ||||
|         await page.locator('[data-testid="conductor-modeOption-realtime"]').click(); | ||||
|         await expect.soft(pausePlayButton).not.toHaveClass(/is-paused/); | ||||
|  | ||||
|         // Zoom in via button | ||||
|         await zoomIntoImageryByButton(page); | ||||
|         await expect(pausePlayButton).not.toHaveClass(/is-paused/); | ||||
|     }); | ||||
|  | ||||
|     //test('Can use Mouse Wheel to zoom in and out of previous image'); | ||||
|     //test('Can zoom into the latest image and the real-time/fixed-time imagery will pause'); | ||||
|     //test.skip('Can zoom into a previous image from thumbstrip in real-time or fixed-time'); | ||||
|     //test.skip('Clicking on the left arrow should pause the imagery and go to previous image'); | ||||
|     //test.skip('If the imagery view is in pause mode, it should not be updated when new images come in'); | ||||
|     //test.skip('If the imagery view is not in pause mode, it should be updated when new images come in'); | ||||
| }); | ||||
|  | ||||
| test.describe('Example Imagery in Display layout', () => { | ||||
|     test.skip('Can use Mouse Wheel to zoom in and out of previous image'); | ||||
|     test.skip('Can use alt+drag to move around image once zoomed in'); | ||||
|     test.skip('Can zoom into the latest image and the real-time/fixed-time imagery will pause'); | ||||
|     test.skip('Clicking on the left arrow should pause the imagery and go to previous image'); | ||||
|     test.skip('If the imagery view is in pause mode, it should not be updated when new images come in'); | ||||
|     test.skip('If the imagery view is not in pause mode, it should be updated when new images come in'); | ||||
| // The following test case will cover these scenarios | ||||
| // ('Can use Mouse Wheel to zoom in and out of previous image'); | ||||
| // ('Can use alt+drag to move around image once zoomed in'); | ||||
| // ('Clicking on the left arrow should pause the imagery and go to previous image'); | ||||
| // ('If the imagery view is in pause mode, it should not be updated when new images come in'); | ||||
| // ('If the imagery view is not in pause mode, it should be updated when new images come in'); | ||||
| test('Example Imagery in Display layout', async ({ page, browserName }) => { | ||||
|     test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox'); | ||||
|     test.info().annotations.push({ | ||||
|         type: 'issue', | ||||
|         description: 'https://github.com/nasa/openmct/issues/5265' | ||||
|     }); | ||||
|  | ||||
|     // Go to baseURL | ||||
|     await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|     // Click the Create button | ||||
|     await page.click('button:has-text("Create")'); | ||||
|  | ||||
|     // Click text=Example Imagery | ||||
|     await page.click('text=Example Imagery'); | ||||
|  | ||||
|     // Clear and set Image load delay to minimum value | ||||
|     await page.locator('input[type="number"]').fill(''); | ||||
|     await page.locator('input[type="number"]').fill('5000'); | ||||
|  | ||||
|     // Click text=OK | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation({waitUntil: 'networkidle'}), | ||||
|         page.click('text=OK'), | ||||
|         //Wait for Save Banner to appear | ||||
|         page.waitForSelector('.c-message-banner__message') | ||||
|     ]); | ||||
|  | ||||
|     // Wait until Save Banner is gone | ||||
|     await page.waitForSelector('.c-message-banner__message', { state: 'detached'}); | ||||
|     await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery'); | ||||
|     await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|  | ||||
|     // Click previous image button | ||||
|     const previousImageButton = page.locator('.c-nav--prev'); | ||||
|     await previousImageButton.click(); | ||||
|  | ||||
|     // Verify previous image | ||||
|     const selectedImage = page.locator('.selected'); | ||||
|     await expect(selectedImage).toBeVisible(); | ||||
|  | ||||
|     // Zoom in | ||||
|     const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|     await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|     const deltaYStep = 100; // equivalent to 1x zoom | ||||
|     await page.mouse.wheel(0, deltaYStep * 2); | ||||
|     const zoomedBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|     const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2; | ||||
|     const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2; | ||||
|  | ||||
|     // Wait for zoom animation to finish | ||||
|     await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|     const imageMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|     expect(imageMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height); | ||||
|     expect(imageMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width); | ||||
|  | ||||
|     // Center the mouse pointer | ||||
|     await page.mouse.move(imageCenterX, imageCenterY); | ||||
|  | ||||
|     // Pan Imagery Hints | ||||
|     const expectedAltText = process.platform === 'linux' ? 'Ctrl+Alt drag to pan' : 'Alt drag to pan'; | ||||
|     const imageryHintsText = await page.locator('.c-imagery__hints').innerText(); | ||||
|     expect(expectedAltText).toEqual(imageryHintsText); | ||||
|  | ||||
|     // Click next image button | ||||
|     const nextImageButton = page.locator('.c-nav--next'); | ||||
|     await nextImageButton.click(); | ||||
|  | ||||
|     // Click time conductor mode button | ||||
|     await page.locator('.c-mode-button').click(); | ||||
|  | ||||
|     // Select local clock mode | ||||
|     await page.locator('[data-testid=conductor-modeOption-realtime]').click(); | ||||
|  | ||||
|     // Zoom in on next image | ||||
|     await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|     await page.mouse.wheel(0, deltaYStep * 2); | ||||
|  | ||||
|     // Wait for zoom animation to finish | ||||
|     await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|     const imageNextMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|     expect(imageNextMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height); | ||||
|     expect(imageNextMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width); | ||||
|  | ||||
|     // Click previous image button | ||||
|     await previousImageButton.click(); | ||||
|  | ||||
|     // Verify previous image | ||||
|     await expect(selectedImage).toBeVisible(); | ||||
|  | ||||
|     const imageCount = await page.locator('.c-imagery__thumb').count(); | ||||
|     await expect.poll(async () => { | ||||
|         const newImageCount = await page.locator('.c-imagery__thumb').count(); | ||||
|  | ||||
|         return newImageCount; | ||||
|     }, { | ||||
|         message: "verify that old images are discarded", | ||||
|         timeout: 6 * 1000 | ||||
|     }).toBe(imageCount); | ||||
|  | ||||
|     // Verify selected image is still displayed | ||||
|     await expect(selectedImage).toBeVisible(); | ||||
|  | ||||
|     // Unpause imagery | ||||
|     await page.locator('.pause-play').click(); | ||||
|  | ||||
|     //Get background-image url from background-image css prop | ||||
|     await assertBackgroundImageUrlFromBackgroundCss(page); | ||||
|  | ||||
|     // Open the image filter menu | ||||
|     await page.locator('[role=toolbar] button[title="Brightness and contrast"]').click(); | ||||
|  | ||||
|     // Drag the brightness and contrast sliders around and assert filter values | ||||
|     await dragBrightnessSliderAndAssertFilterValues(page); | ||||
|     await dragContrastSliderAndAssertFilterValues(page); | ||||
| }); | ||||
|  | ||||
| test.describe('Example imagery thumbnails resize in display layouts', () => { | ||||
|     test('Resizing the layout changes thumbnail visibility and size', async ({ page }) => { | ||||
|         await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|         const thumbsWrapperLocator = page.locator('.c-imagery__thumbs-wrapper'); | ||||
|         // Click button:has-text("Create") | ||||
|         await page.locator('button:has-text("Create")').click(); | ||||
|  | ||||
|         // Click li:has-text("Display Layout") | ||||
|         await page.locator('li:has-text("Display Layout")').click(); | ||||
|         const displayLayoutTitleField = page.locator('text=Properties Title Notes Horizontal grid (px) Vertical grid (px) Horizontal size ( >> input[type="text"]'); | ||||
|         await displayLayoutTitleField.click(); | ||||
|  | ||||
|         await displayLayoutTitleField.fill('Thumbnail Display Layout'); | ||||
|  | ||||
|         // Click text=OK | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('text=OK').click() | ||||
|         ]); | ||||
|  | ||||
|         // Click text=Snapshot Save and Finish Editing Save and Continue Editing >> button >> nth=1 | ||||
|         await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click(); | ||||
|  | ||||
|         // Click text=Save and Finish Editing | ||||
|         await page.locator('text=Save and Finish Editing').click(); | ||||
|  | ||||
|         // Click button:has-text("Create") | ||||
|         await page.locator('button:has-text("Create")').click(); | ||||
|  | ||||
|         // Click li:has-text("Example Imagery") | ||||
|         await page.locator('li:has-text("Example Imagery")').click(); | ||||
|  | ||||
|         const imageryTitleField = page.locator('text=Properties Title Notes Images url list (comma separated) Image load delay (milli >> input[type="text"]'); | ||||
|         // Click text=Properties Title Notes Images url list (comma separated) Image load delay (milli >> input[type="text"] | ||||
|         await imageryTitleField.click(); | ||||
|  | ||||
|         // Fill text=Properties Title Notes Images url list (comma separated) Image load delay (milli >> input[type="text"] | ||||
|         await imageryTitleField.fill('Thumbnail Example Imagery'); | ||||
|  | ||||
|         // Click text=OK | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('text=OK').click() | ||||
|         ]); | ||||
|  | ||||
|         // Click text=Thumbnail Example Imagery Imagery Layout Snapshot >> button >> nth=0 | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('text=Thumbnail Example Imagery Imagery Layout Snapshot >> button').first().click() | ||||
|         ]); | ||||
|  | ||||
|         // Edit mode | ||||
|         await page.locator('text=Thumbnail Display Layout Snapshot >> button').nth(3).click(); | ||||
|  | ||||
|         // Click on example imagery to expose toolbar | ||||
|         await page.locator('text=Thumbnail Example Imagery Snapshot Large View').click(); | ||||
|  | ||||
|         // expect thumbnails not be visible when first added | ||||
|         expect.soft(thumbsWrapperLocator.isHidden()).toBeTruthy(); | ||||
|  | ||||
|         // Resize the example imagery vertically to change the thumbnail visibility | ||||
|         /* | ||||
|         The following arbitrary values are added to observe the separate visual | ||||
|         conditions of the thumbnails (hidden, small thumbnails, regular thumbnails). | ||||
|         Specifically, height is set to 50px for small thumbs and 100px for regular | ||||
|         */ | ||||
|         // Click #mct-input-id-103 | ||||
|         await page.locator('#mct-input-id-103').click(); | ||||
|  | ||||
|         // Fill #mct-input-id-103 | ||||
|         await page.locator('#mct-input-id-103').fill('50'); | ||||
|  | ||||
|         expect(thumbsWrapperLocator.isVisible()).toBeTruthy(); | ||||
|         await expect(thumbsWrapperLocator).toHaveClass(/is-small-thumbs/); | ||||
|  | ||||
|         // Resize the example imagery vertically to change the thumbnail visibility | ||||
|         // Click #mct-input-id-103 | ||||
|         await page.locator('#mct-input-id-103').click(); | ||||
|  | ||||
|         // Fill #mct-input-id-103 | ||||
|         await page.locator('#mct-input-id-103').fill('100'); | ||||
|  | ||||
|         expect(thumbsWrapperLocator.isVisible()).toBeTruthy(); | ||||
|         await expect(thumbsWrapperLocator).not.toHaveClass(/is-small-thumbs/); | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| // test.fixme('Can use Mouse Wheel to zoom in and out of previous image'); | ||||
| // test.fixme('Can use alt+drag to move around image once zoomed in'); | ||||
| // test.fixme('Clicking on the left arrow should pause the imagery and go to previous image'); | ||||
| // test.fixme('If the imagery view is in pause mode, images still come in'); | ||||
| // test.fixme('If the imagery view is not in pause mode, it should be updated when new images come in'); | ||||
| test.describe('Example Imagery in Flexible layout', () => { | ||||
|     test.skip('Can use Mouse Wheel to zoom in and out of previous image'); | ||||
|     test.skip('Can use alt+drag to move around image once zoomed in'); | ||||
|     test.skip('Can zoom into the latest image and the real-time/fixed-time imagery will pause'); | ||||
|     test.skip('Clicking on the left arrow should pause the imagery and go to previous image'); | ||||
|     test.skip('If the imagery view is in pause mode, it should not be updated when new images come in'); | ||||
|     test.skip('If the imagery view is not in pause mode, it should be updated when new images come in'); | ||||
|     test('Example Imagery in Flexible layout', async ({ page, browserName }) => { | ||||
|         test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox'); | ||||
|         test.info().annotations.push({ | ||||
|             type: 'issue', | ||||
|             description: 'https://github.com/nasa/openmct/issues/5326' | ||||
|         }); | ||||
|  | ||||
|         // Go to baseURL | ||||
|         await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|         // Click the Create button | ||||
|         await page.click('button:has-text("Create")'); | ||||
|  | ||||
|         // Click text=Example Imagery | ||||
|         await page.click('text=Example Imagery'); | ||||
|  | ||||
|         // Clear and set Image load delay (milliseconds) | ||||
|         await page.click('input[type="number"]', {clickCount: 3}); | ||||
|         await page.type('input[type="number"]', "20"); | ||||
|  | ||||
|         // Click text=OK | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation({waitUntil: 'networkidle'}), | ||||
|             page.click('text=OK'), | ||||
|             //Wait for Save Banner to appear | ||||
|             page.waitForSelector('.c-message-banner__message') | ||||
|         ]); | ||||
|         // Wait until Save Banner is gone | ||||
|         await page.waitForSelector('.c-message-banner__message', { state: 'detached'}); | ||||
|         await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery'); | ||||
|         await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|  | ||||
|         // Click the Create button | ||||
|         await page.click('button:has-text("Create")'); | ||||
|  | ||||
|         // Click text=Flexible Layout | ||||
|         await page.click('text=Flexible Layout'); | ||||
|  | ||||
|         // Assert Flexable layout | ||||
|         await expect(page.locator('.js-form-title')).toHaveText('Create a New Flexible Layout'); | ||||
|  | ||||
|         await page.locator('form[name="mctForm"] >> text=My Items').click(); | ||||
|  | ||||
|         // Click My Items | ||||
|         await Promise.all([ | ||||
|             page.locator('text=OK').click(), | ||||
|             page.waitForNavigation({waitUntil: 'networkidle'}) | ||||
|         ]); | ||||
|  | ||||
|         // Click My Items | ||||
|         await page.locator('.c-disclosure-triangle').click(); | ||||
|  | ||||
|         // Right click example imagery | ||||
|         await page.click(('text=Unnamed Example Imagery'), { button: 'right' }); | ||||
|  | ||||
|         // Click move | ||||
|         await page.locator('.icon-move').click(); | ||||
|  | ||||
|         // Click triangle to open sub menu | ||||
|         await page.locator('.c-form__section .c-disclosure-triangle').click(); | ||||
|  | ||||
|         // Click Flexable Layout | ||||
|         await page.click('.c-overlay__outer >> text=Unnamed Flexible Layout'); | ||||
|  | ||||
|         // Click text=OK | ||||
|         await page.locator('text=OK').click(); | ||||
|  | ||||
|         // Save template | ||||
|         await saveTemplate(page); | ||||
|  | ||||
|         // Zoom in | ||||
|         await mouseZoomIn(page); | ||||
|  | ||||
|         // Center the mouse pointer | ||||
|         const zoomedBoundingBox = await await page.locator(backgroundImageSelector).boundingBox(); | ||||
|         const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2; | ||||
|         const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2; | ||||
|         await page.mouse.move(imageCenterX, imageCenterY); | ||||
|  | ||||
|         // Pan zoom | ||||
|         await panZoomAndAssertImageProperties(page); | ||||
|  | ||||
|         // Click previous image button | ||||
|         const previousImageButton = page.locator('.c-nav--prev'); | ||||
|         await previousImageButton.click(); | ||||
|  | ||||
|         // Verify previous image | ||||
|         const selectedImage = page.locator('.selected'); | ||||
|         await expect(selectedImage).toBeVisible(); | ||||
|  | ||||
|         // Click time conductor mode button | ||||
|         await page.locator('.c-mode-button').click(); | ||||
|  | ||||
|         // Select local clock mode | ||||
|         await page.locator('[data-testid=conductor-modeOption-realtime]').click(); | ||||
|  | ||||
|         // Zoom in on next image | ||||
|         await mouseZoomIn(page); | ||||
|  | ||||
|         // Click previous image button | ||||
|         await previousImageButton.click(); | ||||
|  | ||||
|         // Verify previous image | ||||
|         await expect(selectedImage).toBeVisible(); | ||||
|  | ||||
|         const imageCount = await page.locator('.c-imagery__thumb').count(); | ||||
|         await expect.poll(async () => { | ||||
|             const newImageCount = await page.locator('.c-imagery__thumb').count(); | ||||
|  | ||||
|             return newImageCount; | ||||
|         }, { | ||||
|             message: "verify that old images are discarded", | ||||
|             timeout: 6 * 1000 | ||||
|         }).toBe(imageCount); | ||||
|  | ||||
|         // Verify selected image is still displayed | ||||
|         await expect(selectedImage).toBeVisible(); | ||||
|  | ||||
|         // Unpause imagery | ||||
|         await page.locator('.pause-play').click(); | ||||
|  | ||||
|         //Get background-image url from background-image css prop | ||||
|         await assertBackgroundImageUrlFromBackgroundCss(page); | ||||
|  | ||||
|         // Open the image filter menu | ||||
|         await page.locator('[role=toolbar] button[title="Brightness and contrast"]').click(); | ||||
|  | ||||
|         // Drag the brightness and contrast sliders around and assert filter values | ||||
|         await dragBrightnessSliderAndAssertFilterValues(page); | ||||
|         await dragContrastSliderAndAssertFilterValues(page); | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| test.describe('Example Imagery in Tabs view', () => { | ||||
|     test.skip('Can use Mouse Wheel to zoom in and out of previous image'); | ||||
|     test.skip('Can use alt+drag to move around image once zoomed in'); | ||||
|     test.skip('Can zoom into the latest image and the real-time/fixed-time imagery will pause'); | ||||
|     test.skip('Can zoom into a previous image from thumbstrip in real-time or fixed-time'); | ||||
|     test.skip('Clicking on the left arrow should pause the imagery and go to previous image'); | ||||
|     test.skip('If the imagery view is in pause mode, it should not be updated when new images come in'); | ||||
|     test.skip('If the imagery view is not in pause mode, it should be updated when new images come in'); | ||||
|     test.fixme('Can use Mouse Wheel to zoom in and out of previous image'); | ||||
|     test.fixme('Can use alt+drag to move around image once zoomed in'); | ||||
|     test.fixme('Can zoom into the latest image and the real-time/fixed-time imagery will pause'); | ||||
|     test.fixme('Can zoom into a previous image from thumbstrip in real-time or fixed-time'); | ||||
|     test.fixme('Clicking on the left arrow should pause the imagery and go to previous image'); | ||||
|     test.fixme('If the imagery view is in pause mode, it should not be updated when new images come in'); | ||||
|     test.fixme('If the imagery view is not in pause mode, it should be updated when new images come in'); | ||||
| }); | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function saveTemplate(page) { | ||||
|     await page.locator('.c-button--menu.c-button--major.icon-save').click(); | ||||
|     await page.locator('text=Save and Finish Editing').click(); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Drag the brightness slider to max, min, and midpoint and assert the filter values | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function dragBrightnessSliderAndAssertFilterValues(page) { | ||||
|     const brightnessSlider = 'div.c-image-controls__slider-wrapper.icon-brightness > input'; | ||||
|     const brightnessBoundingBox = await page.locator(brightnessSlider).boundingBox(); | ||||
|     const brightnessMidX = brightnessBoundingBox.x + brightnessBoundingBox.width / 2; | ||||
|     const brightnessMidY = brightnessBoundingBox.y + brightnessBoundingBox.height / 2; | ||||
|  | ||||
|     await page.locator(brightnessSlider).hover({trial: true}); | ||||
|     await page.mouse.down(); | ||||
|     await page.mouse.move(brightnessBoundingBox.x + brightnessBoundingBox.width, brightnessMidY); | ||||
|     await assertBackgroundImageBrightness(page, '500'); | ||||
|     await page.mouse.move(brightnessBoundingBox.x, brightnessMidY); | ||||
|     await assertBackgroundImageBrightness(page, '0'); | ||||
|     await page.mouse.move(brightnessMidX, brightnessMidY); | ||||
|     await assertBackgroundImageBrightness(page, '250'); | ||||
|     await page.mouse.up(); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Drag the contrast slider to max, min, and midpoint and assert the filter values | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function dragContrastSliderAndAssertFilterValues(page) { | ||||
|     const contrastSlider = 'div.c-image-controls__slider-wrapper.icon-contrast > input'; | ||||
|     const contrastBoundingBox = await page.locator(contrastSlider).boundingBox(); | ||||
|     const contrastMidX = contrastBoundingBox.x + contrastBoundingBox.width / 2; | ||||
|     const contrastMidY = contrastBoundingBox.y + contrastBoundingBox.height / 2; | ||||
|  | ||||
|     await page.locator(contrastSlider).hover({trial: true}); | ||||
|     await page.mouse.down(); | ||||
|     await page.mouse.move(contrastBoundingBox.x + contrastBoundingBox.width, contrastMidY); | ||||
|     await assertBackgroundImageContrast(page, '500'); | ||||
|     await page.mouse.move(contrastBoundingBox.x, contrastMidY); | ||||
|     await assertBackgroundImageContrast(page, '0'); | ||||
|     await page.mouse.move(contrastMidX, contrastMidY); | ||||
|     await assertBackgroundImageContrast(page, '250'); | ||||
|     await page.mouse.up(); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Gets the filter:brightness value of the current background-image and | ||||
|  * asserts against an expected value | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  * @param {String} expected The expected brightness value | ||||
|  */ | ||||
| async function assertBackgroundImageBrightness(page, expected) { | ||||
|     const backgroundImage = page.locator('.c-imagery__main-image__background-image'); | ||||
|  | ||||
|     // Get the brightness filter value (i.e: filter: brightness(500%) => "500") | ||||
|     const actual = await backgroundImage.evaluate((el) => { | ||||
|         return el.style.filter.match(/brightness\((\d{1,3})%\)/)[1]; | ||||
|     }); | ||||
|     expect(actual).toBe(expected); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function assertBackgroundImageUrlFromBackgroundCss(page) { | ||||
|     const backgroundImage = page.locator('.c-imagery__main-image__background-image'); | ||||
|     let backgroundImageUrl = await backgroundImage.evaluate((el) => { | ||||
|         return window.getComputedStyle(el).getPropertyValue('background-image').match(/url\(([^)]+)\)/)[1]; | ||||
|     }); | ||||
|     let backgroundImageUrl1 = backgroundImageUrl.slice(1, -1); //forgive me, padre | ||||
|     console.log('backgroundImageUrl1 ' + backgroundImageUrl1); | ||||
|  | ||||
|     let backgroundImageUrl2; | ||||
|     await expect.poll(async () => { | ||||
|         // Verify next image has updated | ||||
|         let backgroundImageUrlNext = await backgroundImage.evaluate((el) => { | ||||
|             return window.getComputedStyle(el).getPropertyValue('background-image').match(/url\(([^)]+)\)/)[1]; | ||||
|         }); | ||||
|         backgroundImageUrl2 = backgroundImageUrlNext.slice(1, -1); //forgive me, padre | ||||
|  | ||||
|         return backgroundImageUrl2; | ||||
|     }, { | ||||
|         message: "verify next image has updated", | ||||
|         timeout: 6 * 1000 | ||||
|     }).not.toBe(backgroundImageUrl1); | ||||
|     console.log('backgroundImageUrl2 ' + backgroundImageUrl2); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function panZoomAndAssertImageProperties(page) { | ||||
|     const panHotkey = process.platform === 'linux' ? ['Control', 'Alt'] : ['Alt']; | ||||
|     const expectedAltText = process.platform === 'linux' ? 'Ctrl+Alt drag to pan' : 'Alt drag to pan'; | ||||
|     const imageryHintsText = await page.locator('.c-imagery__hints').innerText(); | ||||
|     expect(expectedAltText).toEqual(imageryHintsText); | ||||
|     const zoomedBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|     const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2; | ||||
|     const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2; | ||||
|  | ||||
|     // Pan right | ||||
|     await Promise.all(panHotkey.map(x => page.keyboard.down(x))); | ||||
|     await page.mouse.down(); | ||||
|     await page.mouse.move(imageCenterX - 200, imageCenterY, 10); | ||||
|     await page.mouse.up(); | ||||
|     await Promise.all(panHotkey.map(x => page.keyboard.up(x))); | ||||
|     const afterRightPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|     expect(zoomedBoundingBox.x).toBeGreaterThan(afterRightPanBoundingBox.x); | ||||
|  | ||||
|     // Pan left | ||||
|     await Promise.all(panHotkey.map(x => page.keyboard.down(x))); | ||||
|     await page.mouse.down(); | ||||
|     await page.mouse.move(imageCenterX, imageCenterY, 10); | ||||
|     await page.mouse.up(); | ||||
|     await Promise.all(panHotkey.map(x => page.keyboard.up(x))); | ||||
|     const afterLeftPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|     expect(afterRightPanBoundingBox.x).toBeLessThan(afterLeftPanBoundingBox.x); | ||||
|  | ||||
|     // Pan up | ||||
|     await page.mouse.move(imageCenterX, imageCenterY); | ||||
|     await Promise.all(panHotkey.map(x => page.keyboard.down(x))); | ||||
|     await page.mouse.down(); | ||||
|     await page.mouse.move(imageCenterX, imageCenterY + 200, 10); | ||||
|     await page.mouse.up(); | ||||
|     await Promise.all(panHotkey.map(x => page.keyboard.up(x))); | ||||
|     const afterUpPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|     expect(afterUpPanBoundingBox.y).toBeGreaterThanOrEqual(afterLeftPanBoundingBox.y); | ||||
|  | ||||
|     // Pan down | ||||
|     await Promise.all(panHotkey.map(x => page.keyboard.down(x))); | ||||
|     await page.mouse.down(); | ||||
|     await page.mouse.move(imageCenterX, imageCenterY - 200, 10); | ||||
|     await page.mouse.up(); | ||||
|     await Promise.all(panHotkey.map(x => page.keyboard.up(x))); | ||||
|     const afterDownPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|     expect(afterDownPanBoundingBox.y).toBeLessThanOrEqual(afterUpPanBoundingBox.y); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
| */ | ||||
| async function mouseZoomIn(page) { | ||||
|     // Zoom in | ||||
|     const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|     await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|     const deltaYStep = 100; // equivalent to 1x zoom | ||||
|     await page.mouse.wheel(0, deltaYStep * 2); | ||||
|     const zoomedBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|     const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2; | ||||
|     const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2; | ||||
|  | ||||
|     // center the mouse pointer | ||||
|     await page.mouse.move(imageCenterX, imageCenterY); | ||||
|  | ||||
|     // Wait for zoom animation to finish | ||||
|     await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|     const imageMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|     expect(imageMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height); | ||||
|     expect(imageMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Gets the filter:contrast value of the current background-image and | ||||
|  * asserts against an expected value | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  * @param {String} expected The expected contrast value | ||||
|  */ | ||||
| async function assertBackgroundImageContrast(page, expected) { | ||||
|     const backgroundImage = page.locator('.c-imagery__main-image__background-image'); | ||||
|  | ||||
|     // Get the contrast filter value (i.e: filter: contrast(500%) => "500") | ||||
|     const actual = await backgroundImage.evaluate((el) => { | ||||
|         return el.style.filter.match(/contrast\((\d{1,3})%\)/)[1]; | ||||
|     }); | ||||
|     expect(actual).toBe(expected); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Use the '+' button to zoom in. Hovers first if the toolbar is not visible | ||||
|  * and waits for the zoom animation to finish afterwards. | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function zoomIntoImageryByButton(page) { | ||||
|     // FIXME: There should only be one set of imagery buttons, but there are two? | ||||
|     const zoomInBtn = page.locator("[role='toolbar'][aria-label='Image controls'] .t-btn-zoom-in").nth(0); | ||||
|     const backgroundImage = page.locator(backgroundImageSelector); | ||||
|     if (!(await zoomInBtn.isVisible())) { | ||||
|         await backgroundImage.hover({trial: true}); | ||||
|     } | ||||
|  | ||||
|     await zoomInBtn.click(); | ||||
|     await waitForAnimations(backgroundImage); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Use the '-' button to zoom out. Hovers first if the toolbar is not visible | ||||
|  * and waits for the zoom animation to finish afterwards. | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function zoomOutOfImageryByButton(page) { | ||||
|     // FIXME: There should only be one set of imagery buttons, but there are two? | ||||
|     const zoomOutBtn = page.locator("[role='toolbar'][aria-label='Image controls'] .t-btn-zoom-out").nth(0); | ||||
|     const backgroundImage = page.locator(backgroundImageSelector); | ||||
|     if (!(await zoomOutBtn.isVisible())) { | ||||
|         await backgroundImage.hover({trial: true}); | ||||
|     } | ||||
|  | ||||
|     await zoomOutBtn.click(); | ||||
|     await waitForAnimations(backgroundImage); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Use the reset button to reset image pan and zoom. Hovers first if the toolbar is not visible | ||||
|  * and waits for the zoom animation to finish afterwards. | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function resetImageryPanAndZoom(page) { | ||||
|     // FIXME: There should only be one set of imagery buttons, but there are two? | ||||
|     const panZoomResetBtn = page.locator("[role='toolbar'][aria-label='Image controls'] .t-btn-zoom-reset").nth(0); | ||||
|     const backgroundImage = page.locator(backgroundImageSelector); | ||||
|     if (!(await panZoomResetBtn.isVisible())) { | ||||
|         await backgroundImage.hover({trial: true}); | ||||
|     } | ||||
|  | ||||
|     await panZoomResetBtn.click(); | ||||
|     await waitForAnimations(backgroundImage); | ||||
| } | ||||
|   | ||||
							
								
								
									
										30
									
								
								e2e/tests/plugins/notebook/addInitRestrictedNotebook.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								e2e/tests/plugins/notebook/addInitRestrictedNotebook.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| // this will be called from the test suite with | ||||
| // await page.addInitScript({ path: path.join(__dirname, 'addInitRestrictedNotebook.js') }); | ||||
| // it will install the RestrictedNotebook since it is not installed by default | ||||
|  | ||||
| document.addEventListener('DOMContentLoaded', () => { | ||||
|     const openmct = window.openmct; | ||||
|     openmct.install(openmct.plugins.RestrictedNotebook('CUSTOM_NAME')); | ||||
| }); | ||||
							
								
								
									
										198
									
								
								e2e/tests/plugins/notebook/notebook.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										198
									
								
								e2e/tests/plugins/notebook/notebook.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,198 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* | ||||
| This test suite is dedicated to tests which verify the basic operations surrounding Notebooks. | ||||
| */ | ||||
|  | ||||
| const { test } = require('../../../fixtures'); | ||||
|  | ||||
| test.describe('Notebook CRUD Operations', () => { | ||||
|     test.fixme('Can create a Notebook Object', async ({ page }) => { | ||||
|         //Create domain object | ||||
|         //Newly created notebook should have one Section and one page, 'Unnamed Section'/'Unnamed Page' | ||||
|     }); | ||||
|     test.fixme('Can update a Notebook Object', async ({ page }) => {}); | ||||
|     test.fixme('Can view a perviously created Notebook Object', async ({ page }) => {}); | ||||
|     test.fixme('Can Delete a Notebook Object', async ({ page }) => { | ||||
|         // Other than non-persistible objects | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| test.describe('Default Notebook', () => { | ||||
|     // General Default Notebook statements | ||||
|     // ## Useful commands: | ||||
|     // 1.  - To check default notebook: | ||||
|     //     `JSON.parse(localStorage.getItem('notebook-storage'));` | ||||
|     // 1.  - Clear default notebook: | ||||
|     //     `localStorage.setItem('notebook-storage', null);` | ||||
|     test.fixme('A newly created Notebook is automatically set as the default notebook if no other notebooks exist', async ({ page }) => { | ||||
|         //Create new notebook | ||||
|         //Verify Default Notebook Characteristics | ||||
|     }); | ||||
|     test.fixme('A newly created Notebook is automatically set as the default notebook if at least one other notebook exists', async ({ page }) => { | ||||
|         //Create new notebook A | ||||
|         //Create second notebook B | ||||
|         //Verify Non-Default Notebook A Characteristics | ||||
|         //Verify Default Notebook B Characteristics | ||||
|     }); | ||||
|     test.fixme('If a default notebook is deleted, the second most recent notebook becomes the default', async ({ page }) => { | ||||
|         //Create new notebook A | ||||
|         //Create second notebook B | ||||
|         //Delete Notebook B | ||||
|         //Verify Default Notebook A Characteristics | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| test.describe('Notebook section tests', () => { | ||||
|     //The following test cases are associated with Notebook Sections | ||||
|     test.fixme('New sections are automatically named Unnamed Section with Unnamed Page', async ({ page }) => { | ||||
|         //Create new notebook A | ||||
|         //Add section | ||||
|         //Verify new section and new page details | ||||
|     }); | ||||
|     test.fixme('Section selection operations and associated behavior', async ({ page }) => { | ||||
|         //Create new notebook A | ||||
|         //Add Sections until 6 total with no default section/page | ||||
|         //Select 3rd section | ||||
|         //Delete 4th section | ||||
|         //3rd section is still selected | ||||
|         //Delete 3rd section | ||||
|         //1st section is selected | ||||
|         //Set 3rd section as default | ||||
|         //Delete 2nd section | ||||
|         //3rd section is still default | ||||
|         //Delete 3rd section | ||||
|         //1st is selected and there is no default notebook | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| test.describe('Notebook page tests', () => { | ||||
|     //The following test cases are associated with Notebook Pages | ||||
|     test.fixme('Page selection operations and associated behavior', async ({ page }) => { | ||||
|         //Create new notebook A | ||||
|         //Delete existing Page | ||||
|         //New 'Unnamed Page' automatically created | ||||
|         //Create 6 total Pages without a default page | ||||
|         //Select 3rd | ||||
|         //Delete 3rd | ||||
|         //First is now selected | ||||
|         //Set 3rd as default | ||||
|         //Select 2nd page | ||||
|         //Delete 2nd page | ||||
|         //3rd (default) is now selected | ||||
|         //Set 3rd as default page | ||||
|         //Select 3rd (default) page | ||||
|         //Delete 3rd page | ||||
|         //First is now selected and there is no default notebook | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| test.describe('Notebook search tests', () => { | ||||
|     test.fixme('Can search for a single result', async ({ page }) => {}); | ||||
|     test.fixme('Can search for many results', async ({ page }) => {}); | ||||
|     test.fixme('Can search for new and recently modified entries', async ({ page }) => {}); | ||||
|     test.fixme('Can search for section text', async ({ page }) => {}); | ||||
|     test.fixme('Can search for page text', async ({ page }) => {}); | ||||
|     test.fixme('Can search for entry text', async ({ page }) => {}); | ||||
| }); | ||||
|  | ||||
| test.describe('Notebook entry tests', () => { | ||||
|     test.fixme('When a new entry is created, it should be focused', async ({ page }) => {}); | ||||
|     test.fixme('When a telemetry object is dropped into a notebook, a new entry is created and it should be focused', async ({ page }) => { | ||||
|         // Drag and drop any telmetry object on 'drop object' | ||||
|         // new entry gets created with telemtry object | ||||
|     }); | ||||
|     test.fixme('When a telemetry object is dropped into a notebooks existing entry, it should be focused', async ({ page }) => { | ||||
|         // Drag and drop any telemetry object onto existing entry | ||||
|         // Entry updated with object and snapshot | ||||
|     }); | ||||
|     test.fixme('new entries persist through navigation events without save', async ({ page }) => {}); | ||||
|     test.fixme('previous and new entries can be deleted', async ({ page }) => {}); | ||||
| }); | ||||
|  | ||||
| test.describe('Snapshot Menu tests', () => { | ||||
|     test.fixme('When no default notebook is selected, Snapshot Menu dropdown should only have a single option', async ({ page }) => { | ||||
|         // There should be no default notebook | ||||
|         // Clear default notebook if exists using `localStorage.setItem('notebook-storage', null);` | ||||
|         // refresh page | ||||
|         // Click on 'Notebook Snaphot Menu' | ||||
|         // 'save to Notebook Snapshots' should be only option there | ||||
|     }); | ||||
|     test.fixme('When default notebook is updated selected, Snapshot Menu dropdown should list it as the newest option', async ({ page }) => { | ||||
|         // Create 2a notebooks | ||||
|         // Set Notebook A as Default | ||||
|         // Open Snapshot Menu and note that Notebook A is listed | ||||
|         // Close Snapshot Menu | ||||
|         // Set Default Notebook to Notebook B | ||||
|         // Open Snapshot Notebook and note that Notebook B is listed | ||||
|         // Select Default Notebook Option and verify that Snapshot is added to Notebook B | ||||
|     }); | ||||
|     test.fixme('Can add Snapshots via Snapshot Menu and details are correct', async ({ page }) => { | ||||
|         //Note this should be a visual test, too | ||||
|         // Create Telemetry object | ||||
|         // Create A notebook with many pages and sections. | ||||
|         // Set page and section defaults to be between first and last of many. i.e. 3 of 5 | ||||
|         // Navigate to Telemetry object | ||||
|         // Select Default Notebook Option and verify that Snapshot is added to Notebook A | ||||
|         // Verify Snapshot Details appear correctly | ||||
|     }); | ||||
|     test.fixme('Snapshots adjust time conductor', async ({ page }) => { | ||||
|         // Create Telemetry object | ||||
|         // Set Telemetry object's timeconductor to Fixed time with Start and Endtimes are recorded | ||||
|         // Embed Telemetry object into notebook | ||||
|         // Set Time Conductor to Local clock | ||||
|         // Click into embedded telemetry object and verify object appears with same fixed time from record | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| test.describe('Snapshot Container tests', () => { | ||||
|     test.fixme('5 Snapshots can be added to a container', async ({ page }) => {}); | ||||
|     test.fixme('5 Snapshots can be added to a container and Deleted with Delete All action', async ({ page }) => {}); | ||||
|     test.fixme('A snapshot can be Deleted from Container', async ({ page }) => {}); | ||||
|     test.fixme('A snapshot can be Previewed from Container', async ({ page }) => {}); | ||||
|     test.fixme('A snapshot Container can be open and closed', async ({ page }) => {}); | ||||
|     test.fixme('Can add object to Snapshot container and pull into notebook and create a new entry', async ({ page }) => { | ||||
|         //Create Notebook | ||||
|         //Create Telemetry Object | ||||
|         //From Telemetry Object, use 'save to Notebook Snapshots' | ||||
|         //Snapshots indicator should blink, click on it to view snapshots | ||||
|         //Navigate to Notebook | ||||
|         //Drag and Drop onto droppable area for new entry | ||||
|         //New Entry created with given snapshot added | ||||
|         //Snapshot removed from container? | ||||
|     }); | ||||
|     test.fixme('Can add object to Snapshot container and pull into notebook and existing entry', async ({ page }) => { | ||||
|         //Create Notebook | ||||
|         //Create Telemetry Object | ||||
|         //From Telemetry Object, use 'save to Notebook Snapshots' | ||||
|         //Snapshots indicator should blink, click on it to view snapshots | ||||
|         //Navigate to Notebook | ||||
|         //Drag and Drop into exiting entry | ||||
|         //Existing Entry updated with given snapshot | ||||
|         //Snapshot removed from container? | ||||
|     }); | ||||
|     test.fixme('Verify Embedded options for PNG, JPG, and Annotate work correctly', async ({ page }) => { | ||||
|         //Add snapshot to container | ||||
|         //Verify PNG, JPG, and Annotate buttons work correctly | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										255
									
								
								e2e/tests/plugins/notebook/restrictedNotebook.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										255
									
								
								e2e/tests/plugins/notebook/restrictedNotebook.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,255 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| const { test } = require('../../../fixtures'); | ||||
| const { expect } = require('@playwright/test'); | ||||
| const path = require('path'); | ||||
|  | ||||
| const TEST_TEXT = 'Testing text for entries.'; | ||||
| const TEST_TEXT_NAME = 'Test Page'; | ||||
| const CUSTOM_NAME = 'CUSTOM_NAME'; | ||||
| const NOTEBOOK_DROP_AREA = '.c-notebook__drag-area'; | ||||
|  | ||||
| test.describe('Restricted Notebook', () => { | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         await startAndAddRestrictedNotebookObject(page); | ||||
|     }); | ||||
|  | ||||
|     test('Can be renamed @addInit', async ({ page }) => { | ||||
|         await expect(page.locator('.l-browse-bar__object-name')).toContainText(`Unnamed ${CUSTOM_NAME}`); | ||||
|     }); | ||||
|  | ||||
|     test('Can be deleted if there are no locked pages @addInit', async ({ page }) => { | ||||
|         await openContextMenuRestrictedNotebook(page); | ||||
|  | ||||
|         const menuOptions = page.locator('.c-menu ul'); | ||||
|         await expect.soft(menuOptions).toContainText('Remove'); | ||||
|  | ||||
|         const restrictedNotebookTreeObject = page.locator(`a:has-text("Unnamed ${CUSTOM_NAME}")`); | ||||
|  | ||||
|         // notbook tree object exists | ||||
|         expect.soft(await restrictedNotebookTreeObject.count()).toEqual(1); | ||||
|  | ||||
|         // Click Remove Text | ||||
|         await page.locator('text=Remove').click(); | ||||
|  | ||||
|         // Click 'OK' on confirmation window and wait for save banner to appear | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('text=OK').click(), | ||||
|             page.waitForSelector('.c-message-banner__message') | ||||
|         ]); | ||||
|  | ||||
|         // has been deleted | ||||
|         expect(await restrictedNotebookTreeObject.count()).toEqual(0); | ||||
|     }); | ||||
|  | ||||
|     test('Can be locked if at least one page has one entry @addInit', async ({ page }) => { | ||||
|  | ||||
|         await enterTextEntry(page); | ||||
|  | ||||
|         const commitButton = page.locator('button:has-text("Commit Entries")'); | ||||
|         expect(await commitButton.count()).toEqual(1); | ||||
|     }); | ||||
|  | ||||
| }); | ||||
|  | ||||
| test.describe('Restricted Notebook with at least one entry and with the page locked @addInit', () => { | ||||
|  | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         await startAndAddRestrictedNotebookObject(page); | ||||
|         await enterTextEntry(page); | ||||
|         await lockPage(page); | ||||
|  | ||||
|         // FIXME: Give ample time for the mutation to happen | ||||
|         // https://github.com/nasa/openmct/issues/5409 | ||||
|         // eslint-disable-next-line playwright/no-wait-for-timeout | ||||
|         await page.waitForTimeout(1 * 1000); | ||||
|  | ||||
|         // open sidebar | ||||
|         await page.locator('button.c-notebook__toggle-nav-button').click(); | ||||
|     }); | ||||
|  | ||||
|     test('Locked page should now be in a locked state @addInit', async ({ page }, testInfo) => { | ||||
|         test.fixme(testInfo.project === 'chrome-beta', "Test is unreliable on chrome-beta"); | ||||
|         // main lock message on page | ||||
|         const lockMessage = page.locator('text=This page has been committed and cannot be modified or removed'); | ||||
|         expect.soft(await lockMessage.count()).toEqual(1); | ||||
|  | ||||
|         // lock icon on page in sidebar | ||||
|         const pageLockIcon = page.locator('ul.c-notebook__pages li div.icon-lock'); | ||||
|         expect.soft(await pageLockIcon.count()).toEqual(1); | ||||
|  | ||||
|         // no way to remove a restricted notebook with a locked page | ||||
|         await openContextMenuRestrictedNotebook(page); | ||||
|         const menuOptions = page.locator('.c-menu ul'); | ||||
|  | ||||
|         await expect(menuOptions).not.toContainText('Remove'); | ||||
|     }); | ||||
|  | ||||
|     test('Can still: add page, rename, add entry, delete unlocked pages @addInit', async ({ page }) => { | ||||
|         // Click text=Page Add >> button | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('text=Page Add >> button').click() | ||||
|         ]); | ||||
|         // Click text=Unnamed Page >> nth=1 | ||||
|         await page.locator('text=Unnamed Page').nth(1).click(); | ||||
|         // Press a with modifiers | ||||
|         await page.locator('text=Unnamed Page').nth(1).fill(TEST_TEXT_NAME); | ||||
|  | ||||
|         // expect to be able to rename unlocked pages | ||||
|         const newPageElement = page.locator(`text=${TEST_TEXT_NAME}`); | ||||
|         const newPageCount = await newPageElement.count(); | ||||
|         await newPageElement.press('Enter'); // exit contenteditable state | ||||
|         expect.soft(newPageCount).toEqual(1); | ||||
|  | ||||
|         // enter test text | ||||
|         await enterTextEntry(page); | ||||
|  | ||||
|         // expect new page to be lockable | ||||
|         const commitButton = page.locator('BUTTON:HAS-TEXT("COMMIT ENTRIES")'); | ||||
|         expect.soft(await commitButton.count()).toEqual(1); | ||||
|  | ||||
|         // Click text=Unnamed PageTest Page >> button | ||||
|         await page.locator('text=Unnamed PageTest Page >> button').click(); | ||||
|         // Click text=Delete Page | ||||
|         await page.locator('text=Delete Page').click(); | ||||
|         // Click text=Ok | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('text=Ok').click() | ||||
|         ]); | ||||
|  | ||||
|         // deleted page, should no longer exist | ||||
|         const deletedPageElement = page.locator(`text=${TEST_TEXT_NAME}`); | ||||
|         expect(await deletedPageElement.count()).toEqual(0); | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| test.describe('Restricted Notebook with a page locked and with an embed @addInit', () => { | ||||
|  | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         await startAndAddRestrictedNotebookObject(page); | ||||
|         await dragAndDropEmbed(page); | ||||
|     }); | ||||
|  | ||||
|     test('Allows embeds to be deleted if page unlocked @addInit', async ({ page }) => { | ||||
|         // Click .c-ne__embed__name .c-popup-menu-button | ||||
|         await page.locator('.c-ne__embed__name .c-popup-menu-button').click(); // embed popup menu | ||||
|  | ||||
|         const embedMenu = page.locator('body >> .c-menu'); | ||||
|         await expect(embedMenu).toContainText('Remove This Embed'); | ||||
|     }); | ||||
|  | ||||
|     test('Disallows embeds to be deleted if page locked @addInit', async ({ page }) => { | ||||
|         await lockPage(page); | ||||
|         // Click .c-ne__embed__name .c-popup-menu-button | ||||
|         await page.locator('.c-ne__embed__name .c-popup-menu-button').click(); // embed popup menu | ||||
|  | ||||
|         const embedMenu = page.locator('body >> .c-menu'); | ||||
|         await expect(embedMenu).not.toContainText('Remove This Embed'); | ||||
|     }); | ||||
|  | ||||
| }); | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function startAndAddRestrictedNotebookObject(page) { | ||||
|     // eslint-disable-next-line no-undef | ||||
|     await page.addInitScript({ path: path.join(__dirname, 'addInitRestrictedNotebook.js') }); | ||||
|     //Go to baseURL | ||||
|     await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|     //Click the Create button | ||||
|     await page.click('button:has-text("Create")'); | ||||
|     // Click text=CUSTOME_NAME | ||||
|     await page.click(`text=${CUSTOM_NAME}`); // secondarily tests renamability also | ||||
|     // Click text=OK | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation({waitUntil: 'networkidle'}), | ||||
|         page.click('text=OK') | ||||
|     ]); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function enterTextEntry(page) { | ||||
|     // Click .c-notebook__drag-area | ||||
|     await page.locator(NOTEBOOK_DROP_AREA).click(); | ||||
|  | ||||
|     // enter text | ||||
|     await page.locator('div.c-ne__text').click(); | ||||
|     await page.locator('div.c-ne__text').fill(TEST_TEXT); | ||||
|     await page.locator('div.c-ne__text').press('Enter'); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function dragAndDropEmbed(page) { | ||||
|     // Click button:has-text("Create") | ||||
|     await page.locator('button:has-text("Create")').click(); | ||||
|     // Click li:has-text("Sine Wave Generator") | ||||
|     await page.locator('li:has-text("Sine Wave Generator")').click(); | ||||
|     // Click form[name="mctForm"] >> text=My Items | ||||
|     await page.locator('form[name="mctForm"] >> text=My Items').click(); | ||||
|     // Click text=OK | ||||
|     await page.locator('text=OK').click(); | ||||
|     // Click text=Open MCT My Items >> span >> nth=3 | ||||
|     await page.locator('text=Open MCT My Items >> span').nth(3).click(); | ||||
|     // Click text=Unnamed CUSTOM_NAME | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('text=Unnamed CUSTOM_NAME').click() | ||||
|     ]); | ||||
|  | ||||
|     await page.dragAndDrop('text=UNNAMED SINE WAVE GENERATOR', NOTEBOOK_DROP_AREA); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function lockPage(page) { | ||||
|     const commitButton = page.locator('button:has-text("Commit Entries")'); | ||||
|     await commitButton.click(); | ||||
|  | ||||
|     //Wait until Lock Banner is visible | ||||
|     await page.locator('text=Lock Page').click(); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function openContextMenuRestrictedNotebook(page) { | ||||
|     const myItemsFolder = page.locator('text=Open MCT My Items >> span').nth(3); | ||||
|     const className = await myItemsFolder.getAttribute('class'); | ||||
|     if (!className.includes('c-disclosure-triangle--expanded')) { | ||||
|         await myItemsFolder.click(); | ||||
|     } | ||||
|  | ||||
|     // Click a:has-text("Unnamed CUSTOM_NAME") | ||||
|     await page.locator(`a:has-text("Unnamed ${CUSTOM_NAME}")`).click({ | ||||
|         button: 'right' | ||||
|     }); | ||||
| } | ||||
							
								
								
									
										205
									
								
								e2e/tests/plugins/notebook/tags.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										205
									
								
								e2e/tests/plugins/notebook/tags.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,205 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* | ||||
| This test suite is dedicated to tests which verify form functionality. | ||||
| */ | ||||
|  | ||||
| const { expect } = require('@playwright/test'); | ||||
| const { test } = require('../../../fixtures'); | ||||
|  | ||||
| /** | ||||
|   * Creates a notebook object and adds an entry. | ||||
|   * @param {import('@playwright/test').Page} - page to load | ||||
|   * @param {number} [iterations = 1] - the number of entries to create | ||||
|   */ | ||||
| async function createNotebookAndEntry(page, iterations = 1) { | ||||
|     //Go to baseURL | ||||
|     await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|     // Click button:has-text("Create") | ||||
|     await page.locator('button:has-text("Create")').click(); | ||||
|     await page.locator('[title="Create and save timestamped notes with embedded object snapshots."]').click(); | ||||
|     // Click button:has-text("OK") | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('[name="mctForm"] >> text=My Items').click(), | ||||
|         page.locator('button:has-text("OK")').click() | ||||
|     ]); | ||||
|  | ||||
|     for (let iteration = 0; iteration < iterations; iteration++) { | ||||
|         // Click text=To start a new entry, click here or drag and drop any object | ||||
|         await page.locator('text=To start a new entry, click here or drag and drop any object').click(); | ||||
|         const entryLocator = `[aria-label="Notebook Entry Input"] >> nth = ${iteration}`; | ||||
|         await page.locator(entryLocator).click(); | ||||
|         await page.locator(entryLocator).fill(`Entry ${iteration}`); | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| /** | ||||
|   * Creates a notebook object, adds an entry, and adds a tag. | ||||
|   * @param {import('@playwright/test').Page} page | ||||
|   * @param {number} [iterations = 1] - the number of entries (and tags) to create | ||||
|   */ | ||||
| async function createNotebookEntryAndTags(page, iterations = 1) { | ||||
|     await createNotebookAndEntry(page, iterations); | ||||
|  | ||||
|     for (let iteration = 0; iteration < iterations; iteration++) { | ||||
|         // Click text=To start a new entry, click here or drag and drop any object | ||||
|         await page.locator(`button:has-text("Add Tag") >> nth = ${iteration}`).click(); | ||||
|  | ||||
|         // Click [placeholder="Type to select tag"] | ||||
|         await page.locator('[placeholder="Type to select tag"]').click(); | ||||
|         // Click text=Driving | ||||
|         await page.locator('[aria-label="Autocomplete Options"] >> text=Driving').click(); | ||||
|  | ||||
|         // Click button:has-text("Add Tag") | ||||
|         await page.locator(`button:has-text("Add Tag") >> nth = ${iteration}`).click(); | ||||
|         // Click [placeholder="Type to select tag"] | ||||
|         await page.locator('[placeholder="Type to select tag"]').click(); | ||||
|         // Click text=Science | ||||
|         await page.locator('[aria-label="Autocomplete Options"] >> text=Science').click(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| test.describe('Tagging in Notebooks', () => { | ||||
|     test('Can load tags', async ({ page }) => { | ||||
|         await createNotebookAndEntry(page); | ||||
|         // Click text=To start a new entry, click here or drag and drop any object | ||||
|         await page.locator('button:has-text("Add Tag")').click(); | ||||
|  | ||||
|         // Click [placeholder="Type to select tag"] | ||||
|         await page.locator('[placeholder="Type to select tag"]').click(); | ||||
|  | ||||
|         await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Science"); | ||||
|         await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Drilling"); | ||||
|         await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Driving"); | ||||
|     }); | ||||
|     test('Can add tags', async ({ page }) => { | ||||
|         await createNotebookEntryAndTags(page); | ||||
|  | ||||
|         await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Science"); | ||||
|         await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Driving"); | ||||
|  | ||||
|         // Click button:has-text("Add Tag") | ||||
|         await page.locator('button:has-text("Add Tag")').click(); | ||||
|         // Click [placeholder="Type to select tag"] | ||||
|         await page.locator('[placeholder="Type to select tag"]').click(); | ||||
|  | ||||
|         await expect(page.locator('[aria-label="Autocomplete Options"]')).not.toContainText("Science"); | ||||
|         await expect(page.locator('[aria-label="Autocomplete Options"]')).not.toContainText("Driving"); | ||||
|         await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Drilling"); | ||||
|     }); | ||||
|     test('Can search for tags', async ({ page }) => { | ||||
|         await createNotebookEntryAndTags(page); | ||||
|         // Click [aria-label="OpenMCT Search"] input[type="search"] | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); | ||||
|         // Fill [aria-label="OpenMCT Search"] input[type="search"] | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc'); | ||||
|         await expect(page.locator('[aria-label="Search Result"]')).toContainText("Science"); | ||||
|         await expect(page.locator('[aria-label="Search Result"]')).toContainText("Driving"); | ||||
|  | ||||
|         // Click [aria-label="OpenMCT Search"] input[type="search"] | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); | ||||
|         // Fill [aria-label="OpenMCT Search"] input[type="search"] | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Sc'); | ||||
|         await expect(page.locator('[aria-label="Search Result"]')).toContainText("Science"); | ||||
|         await expect(page.locator('[aria-label="Search Result"]')).toContainText("Driving"); | ||||
|  | ||||
|         // Click [aria-label="OpenMCT Search"] input[type="search"] | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); | ||||
|         // Fill [aria-label="OpenMCT Search"] input[type="search"] | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Xq'); | ||||
|         await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible(); | ||||
|         await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible(); | ||||
|     }); | ||||
|  | ||||
|     test('Can delete tags', async ({ page }) => { | ||||
|         await createNotebookEntryAndTags(page); | ||||
|         await page.locator('[aria-label="Notebook Entries"]').click(); | ||||
|         // Delete Driving | ||||
|         await page.locator('text=Science Driving Add Tag >> button').nth(1).click(); | ||||
|  | ||||
|         await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Science"); | ||||
|         await expect(page.locator('[aria-label="Notebook Entry"]')).not.toContainText("Driving"); | ||||
|  | ||||
|         // Fill [aria-label="OpenMCT Search"] input[type="search"] | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc'); | ||||
|         await expect(page.locator('[aria-label="Search Result"]')).not.toContainText("Driving"); | ||||
|     }); | ||||
|     test('Tags persist across reload', async ({ page }) => { | ||||
|         //Go to baseURL | ||||
|         await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|         // Create a clock object we can navigate to | ||||
|         await page.click('button:has-text("Create")'); | ||||
|  | ||||
|         // Click Clock | ||||
|         await page.click('text=Clock'); | ||||
|         // Click button:has-text("OK") | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('[name="mctForm"] >> text=My Items').click(), | ||||
|             page.locator('button:has-text("OK")').click() | ||||
|         ]); | ||||
|  | ||||
|         await page.click('.c-disclosure-triangle'); | ||||
|  | ||||
|         const ITERATIONS = 4; | ||||
|         await createNotebookEntryAndTags(page, ITERATIONS); | ||||
|  | ||||
|         for (let iteration = 0; iteration < ITERATIONS; iteration++) { | ||||
|             const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`; | ||||
|             await expect(page.locator(entryLocator)).toContainText("Science"); | ||||
|             await expect(page.locator(entryLocator)).toContainText("Driving"); | ||||
|         } | ||||
|  | ||||
|         // Click Unnamed Clock | ||||
|         await page.click('text="Unnamed Clock"'); | ||||
|  | ||||
|         // Click Unnamed Notebook | ||||
|         await page.click('text="Unnamed Notebook"'); | ||||
|  | ||||
|         for (let iteration = 0; iteration < ITERATIONS; iteration++) { | ||||
|             const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`; | ||||
|             await expect(page.locator(entryLocator)).toContainText("Science"); | ||||
|             await expect(page.locator(entryLocator)).toContainText("Driving"); | ||||
|         } | ||||
|  | ||||
|         //Reload Page | ||||
|         await Promise.all([ | ||||
|             page.reload(), | ||||
|             page.waitForLoadState('networkidle') | ||||
|         ]); | ||||
|  | ||||
|         // Click Unnamed Notebook | ||||
|         await page.click('text="Unnamed Notebook"'); | ||||
|  | ||||
|         for (let iteration = 0; iteration < ITERATIONS; iteration++) { | ||||
|             const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`; | ||||
|             await expect(page.locator(entryLocator)).toContainText("Science"); | ||||
|             await expect(page.locator(entryLocator)).toContainText("Driving"); | ||||
|         } | ||||
|  | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										185
									
								
								e2e/tests/plugins/plot/autoscale.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										185
									
								
								e2e/tests/plugins/plot/autoscale.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,185 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* | ||||
| Testsuite for plot autoscale. | ||||
| */ | ||||
|  | ||||
| const { test } = require('../../../fixtures.js'); | ||||
| const { expect } = require('@playwright/test'); | ||||
|  | ||||
| test.use({ | ||||
|     viewport: { | ||||
|         width: 1280, | ||||
|         height: 720 | ||||
|     } | ||||
| }); | ||||
|  | ||||
| test.describe('ExportAsJSON', () => { | ||||
|     test('User can set autoscale with a valid range @snapshot', async ({ page }) => { | ||||
|         //This is necessary due to the size of the test suite. | ||||
|         test.slow(); | ||||
|  | ||||
|         await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|         await setTimeRange(page); | ||||
|  | ||||
|         await createSinewaveOverlayPlot(page); | ||||
|  | ||||
|         await testYTicks(page, ['-1.00', '-0.50', '0.00', '0.50', '1.00']); | ||||
|  | ||||
|         await turnOffAutoscale(page); | ||||
|  | ||||
|         // Make sure that after turning off autoscale, the user selected range values start at the same values the plot had prior. | ||||
|         await testYTicks(page, ['-1.00', '-0.50', '0.00', '0.50', '1.00']); | ||||
|  | ||||
|         const canvas = page.locator('canvas').nth(1); | ||||
|  | ||||
|         await canvas.hover({trial: true}); | ||||
|  | ||||
|         expect(await canvas.screenshot()).toMatchSnapshot('autoscale-canvas-prepan'); | ||||
|  | ||||
|         //Alt Drag Start | ||||
|         await page.keyboard.down('Alt'); | ||||
|  | ||||
|         await canvas.dragTo(canvas, { | ||||
|             sourcePosition: { | ||||
|                 x: 200, | ||||
|                 y: 200 | ||||
|             }, | ||||
|             targetPosition: { | ||||
|                 x: 400, | ||||
|                 y: 400 | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         //Alt Drag End | ||||
|         await page.keyboard.up('Alt'); | ||||
|  | ||||
|         // Ensure the drag worked. | ||||
|         await testYTicks(page, ['0.00', '0.50', '1.00', '1.50', '2.00']); | ||||
|  | ||||
|         await canvas.hover({trial: true}); | ||||
|  | ||||
|         expect(await canvas.screenshot()).toMatchSnapshot('autoscale-canvas-panned'); | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  * @param {string} start | ||||
|  * @param {string} end | ||||
|  */ | ||||
| async function setTimeRange(page, start = '2022-03-29 22:00:00.000Z', end = '2022-03-29 22:00:30.000Z') { | ||||
|     // Set a specific time range for consistency, otherwise it will change | ||||
|     // on every test to a range based on the current time. | ||||
|  | ||||
|     const timeInputs = page.locator('input.c-input--datetime'); | ||||
|     await timeInputs.first().click(); | ||||
|     await timeInputs.first().fill(start); | ||||
|  | ||||
|     await timeInputs.nth(1).click(); | ||||
|     await timeInputs.nth(1).fill(end); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function createSinewaveOverlayPlot(page) { | ||||
|     // click create button | ||||
|     await page.locator('button:has-text("Create")').click(); | ||||
|  | ||||
|     // add overlay plot with defaults | ||||
|     await page.locator('li:has-text("Overlay Plot")').click(); | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('text=OK').click(), | ||||
|         //Wait for Save Banner to appear1 | ||||
|         page.waitForSelector('.c-message-banner__message') | ||||
|     ]); | ||||
|     //Wait until Save Banner is gone | ||||
|     await page.locator('.c-message-banner__close-button').click(); | ||||
|     await page.waitForSelector('.c-message-banner__message', { state: 'detached'}); | ||||
|  | ||||
|     // save (exit edit mode) | ||||
|     await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click(); | ||||
|     await page.locator('text=Save and Finish Editing').click(); | ||||
|  | ||||
|     // click create button | ||||
|     await page.locator('button:has-text("Create")').click(); | ||||
|  | ||||
|     // add sine wave generator with defaults | ||||
|     await page.locator('li:has-text("Sine Wave Generator")').click(); | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('text=OK').click(), | ||||
|         //Wait for Save Banner to appear1 | ||||
|         page.waitForSelector('.c-message-banner__message') | ||||
|     ]); | ||||
|     //Wait until Save Banner is gone | ||||
|     await page.locator('.c-message-banner__close-button').click(); | ||||
|     await page.waitForSelector('.c-message-banner__message', { state: 'detached'}); | ||||
|  | ||||
|     // focus the overlay plot | ||||
|     await page.locator('text=Open MCT My Items >> span').nth(3).click(); | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('text=Unnamed Overlay Plot').first().click() | ||||
|     ]); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function turnOffAutoscale(page) { | ||||
|     // enter edit mode | ||||
|     await page.locator('text=Unnamed Overlay Plot Snapshot >> button').nth(3).click(); | ||||
|  | ||||
|     // uncheck autoscale | ||||
|     await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"] >> nth=1').uncheck(); | ||||
|  | ||||
|     // save | ||||
|     await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click(); | ||||
|     await Promise.all([ | ||||
|         page.locator('text=Save and Finish Editing').click(), | ||||
|         //Wait for Save Banner to appear | ||||
|         page.waitForSelector('.c-message-banner__message') | ||||
|     ]); | ||||
|     //Wait until Save Banner is gone | ||||
|     await page.locator('.c-message-banner__close-button').click(); | ||||
|     await page.waitForSelector('.c-message-banner__message', { state: 'detached'}); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function testYTicks(page, values) { | ||||
|     const yTicks = page.locator('.gl-plot-y-tick-label'); | ||||
|     await page.locator('canvas >> nth=1').hover(); | ||||
|     let promises = [yTicks.count().then(c => expect(c).toBe(values.length))]; | ||||
|  | ||||
|     for (let i = 0, l = values.length; i < l; i += 1) { | ||||
|         promises.push(expect(yTicks.nth(i)).toHaveText(values[i])); // eslint-disable-line | ||||
|     } | ||||
|  | ||||
|     await Promise.all(promises); | ||||
| } | ||||
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 18 KiB | 
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 18 KiB | 
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 21 KiB | 
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 21 KiB | 
							
								
								
									
										298
									
								
								e2e/tests/plugins/plot/logPlot.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										298
									
								
								e2e/tests/plugins/plot/logPlot.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,298 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* | ||||
| Tests to verify log plot functionality. Note this test suite if very much under active development and should not | ||||
| necessarily be used for reference when writing new tests in this area. | ||||
| */ | ||||
|  | ||||
| const { test } = require('../../../fixtures.js'); | ||||
| const { expect } = require('@playwright/test'); | ||||
|  | ||||
| test.describe('Log plot tests', () => { | ||||
|     test('Log Plot ticks are functionally correct in regular and log mode and after refresh', async ({ page }) => { | ||||
|         //Test.slow decorator is currently broken. Needs to be fixed in https://github.com/nasa/openmct/issues/5374 | ||||
|         test.slow(); | ||||
|  | ||||
|         await makeOverlayPlot(page); | ||||
|         await testRegularTicks(page); | ||||
|         await enableEditMode(page); | ||||
|         await enableLogMode(page); | ||||
|         await testLogTicks(page); | ||||
|         await disableLogMode(page); | ||||
|         await testRegularTicks(page); | ||||
|         await enableLogMode(page); | ||||
|         await testLogTicks(page); | ||||
|         await saveOverlayPlot(page); | ||||
|         await testLogTicks(page); | ||||
|     }); | ||||
|  | ||||
|     // Leaving test as 'TODO' for now. | ||||
|     // NOTE: Not eligible for community contributions. | ||||
|     test.fixme('Verify that log mode option is reflected in import/export JSON', async ({ page }) => { | ||||
|         await makeOverlayPlot(page); | ||||
|         await enableEditMode(page); | ||||
|         await enableLogMode(page); | ||||
|         await saveOverlayPlot(page); | ||||
|  | ||||
|         // TODO ...export, delete the overlay, then import it... | ||||
|  | ||||
|         //await testLogTicks(page); | ||||
|  | ||||
|         // TODO, the plot is slightly at different position that in the other test, so this fails. | ||||
|         // ...We can fix it by copying all steps from the first test... | ||||
|         // await testLogPlotPixels(page); | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| /** | ||||
|  * Makes an overlay plot with a sine wave generator and clicks on the overlay plot in the sidebar so it is the active thing displayed. | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function makeOverlayPlot(page) { | ||||
|     // fresh page with time range from 2022-03-29 22:00:00.000Z to 2022-03-29 22:00:30.000Z | ||||
|     await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|     // Set a specific time range for consistency, otherwise it will change | ||||
|     // on every test to a range based on the current time. | ||||
|  | ||||
|     const timeInputs = page.locator('input.c-input--datetime'); | ||||
|     await timeInputs.first().click(); | ||||
|     await timeInputs.first().fill('2022-03-29 22:00:00.000Z'); | ||||
|  | ||||
|     await timeInputs.nth(1).click(); | ||||
|     await timeInputs.nth(1).fill('2022-03-29 22:00:30.000Z'); | ||||
|  | ||||
|     // create overlay plot | ||||
|  | ||||
|     await page.locator('button.c-create-button').click(); | ||||
|     await page.locator('li:has-text("Overlay Plot")').click(); | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation({ waitUntil: 'networkidle'}), | ||||
|         page.locator('text=OK').click(), | ||||
|         //Wait for Save Banner to appear | ||||
|         page.waitForSelector('.c-message-banner__message') | ||||
|     ]); | ||||
|     //Wait until Save Banner is gone | ||||
|     await page.locator('.c-message-banner__close-button').click(); | ||||
|     await page.waitForSelector('.c-message-banner__message', { state: 'detached'}); | ||||
|  | ||||
|     // save the overlay plot | ||||
|  | ||||
|     await saveOverlayPlot(page); | ||||
|  | ||||
|     // create a sinewave generator | ||||
|  | ||||
|     await page.locator('button.c-create-button').click(); | ||||
|     await page.locator('li:has-text("Sine Wave Generator")').click(); | ||||
|  | ||||
|     // set amplitude to 6, offset 4, period 2 | ||||
|  | ||||
|     await page.locator('div:nth-child(5) .c-form-row__controls .form-control .field input').click(); | ||||
|     await page.locator('div:nth-child(5) .c-form-row__controls .form-control .field input').fill('6'); | ||||
|  | ||||
|     await page.locator('div:nth-child(6) .c-form-row__controls .form-control .field input').click(); | ||||
|     await page.locator('div:nth-child(6) .c-form-row__controls .form-control .field input').fill('4'); | ||||
|  | ||||
|     await page.locator('div:nth-child(7) .c-form-row__controls .form-control .field input').click(); | ||||
|     await page.locator('div:nth-child(7) .c-form-row__controls .form-control .field input').fill('2'); | ||||
|  | ||||
|     // Click OK to make generator | ||||
|  | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation({ waitUntil: 'networkidle'}), | ||||
|         page.locator('text=OK').click(), | ||||
|         //Wait for Save Banner to appear | ||||
|         page.waitForSelector('.c-message-banner__message') | ||||
|     ]); | ||||
|     //Wait until Save Banner is gone | ||||
|     await page.locator('.c-message-banner__close-button').click(); | ||||
|     await page.waitForSelector('.c-message-banner__message', { state: 'detached'}); | ||||
|  | ||||
|     // click on overlay plot | ||||
|  | ||||
|     await page.locator('text=Open MCT My Items >> span').nth(3).click(); | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('text=Unnamed Overlay Plot').first().click() | ||||
|     ]); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function testRegularTicks(page) { | ||||
|     const yTicks = await page.locator('.gl-plot-y-tick-label'); | ||||
|     expect(await yTicks.count()).toBe(7); | ||||
|     await expect(yTicks.nth(0)).toHaveText('-2'); | ||||
|     await expect(yTicks.nth(1)).toHaveText('0'); | ||||
|     await expect(yTicks.nth(2)).toHaveText('2'); | ||||
|     await expect(yTicks.nth(3)).toHaveText('4'); | ||||
|     await expect(yTicks.nth(4)).toHaveText('6'); | ||||
|     await expect(yTicks.nth(5)).toHaveText('8'); | ||||
|     await expect(yTicks.nth(6)).toHaveText('10'); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function testLogTicks(page) { | ||||
|     const yTicks = await page.locator('.gl-plot-y-tick-label'); | ||||
|     expect(await yTicks.count()).toBe(28); | ||||
|     await expect(yTicks.nth(0)).toHaveText('-2.98'); | ||||
|     await expect(yTicks.nth(1)).toHaveText('-2.50'); | ||||
|     await expect(yTicks.nth(2)).toHaveText('-2.00'); | ||||
|     await expect(yTicks.nth(3)).toHaveText('-1.51'); | ||||
|     await expect(yTicks.nth(4)).toHaveText('-1.20'); | ||||
|     await expect(yTicks.nth(5)).toHaveText('-1.00'); | ||||
|     await expect(yTicks.nth(6)).toHaveText('-0.80'); | ||||
|     await expect(yTicks.nth(7)).toHaveText('-0.58'); | ||||
|     await expect(yTicks.nth(8)).toHaveText('-0.40'); | ||||
|     await expect(yTicks.nth(9)).toHaveText('-0.20'); | ||||
|     await expect(yTicks.nth(10)).toHaveText('-0.00'); | ||||
|     await expect(yTicks.nth(11)).toHaveText('0.20'); | ||||
|     await expect(yTicks.nth(12)).toHaveText('0.40'); | ||||
|     await expect(yTicks.nth(13)).toHaveText('0.58'); | ||||
|     await expect(yTicks.nth(14)).toHaveText('0.80'); | ||||
|     await expect(yTicks.nth(15)).toHaveText('1.00'); | ||||
|     await expect(yTicks.nth(16)).toHaveText('1.20'); | ||||
|     await expect(yTicks.nth(17)).toHaveText('1.51'); | ||||
|     await expect(yTicks.nth(18)).toHaveText('2.00'); | ||||
|     await expect(yTicks.nth(19)).toHaveText('2.50'); | ||||
|     await expect(yTicks.nth(20)).toHaveText('2.98'); | ||||
|     await expect(yTicks.nth(21)).toHaveText('3.50'); | ||||
|     await expect(yTicks.nth(22)).toHaveText('4.00'); | ||||
|     await expect(yTicks.nth(23)).toHaveText('4.50'); | ||||
|     await expect(yTicks.nth(24)).toHaveText('5.31'); | ||||
|     await expect(yTicks.nth(25)).toHaveText('7.00'); | ||||
|     await expect(yTicks.nth(26)).toHaveText('8.00'); | ||||
|     await expect(yTicks.nth(27)).toHaveText('9.00'); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function enableEditMode(page) { | ||||
|     // turn on edit mode | ||||
|     await page.locator('text=Unnamed Overlay Plot Snapshot >> button').nth(3).click(); | ||||
|     await expect(await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1)).toBeVisible(); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function enableLogMode(page) { | ||||
|     // turn on log mode | ||||
|     await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"]').first().check(); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function disableLogMode(page) { | ||||
|     // turn off log mode | ||||
|     await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"]').first().uncheck(); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function saveOverlayPlot(page) { | ||||
|     // save overlay plot | ||||
|     await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click(); | ||||
|  | ||||
|     await Promise.all([ | ||||
|         page.locator('text=Save and Finish Editing').click(), | ||||
|         //Wait for Save Banner to appear | ||||
|         page.waitForSelector('.c-message-banner__message') | ||||
|     ]); | ||||
|     //Wait until Save Banner is gone | ||||
|     await page.locator('.c-message-banner__close-button').click(); | ||||
|     await page.waitForSelector('.c-message-banner__message', { state: 'detached' }); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| // FIXME: Remove this eslint exception once implemented | ||||
| // eslint-disable-next-line no-unused-vars | ||||
| async function testLogPlotPixels(page) { | ||||
|     const pixelsMatch = await page.evaluate(async () => { | ||||
|         // TODO get canvas pixels at a few locations to make sure they're the correct color, to test that the plot comes out as expected. | ||||
|  | ||||
|         await new Promise((r) => setTimeout(r, 5 * 1000)); | ||||
|  | ||||
|         // These are some pixels that should be blue points in the log plot. | ||||
|         // If the plot changes shape to an unexpected shape, this will | ||||
|         // likely fail, which is what we want. | ||||
|         // | ||||
|         // I found these pixels by pausing playwright in debug mode at this | ||||
|         // point, and using similar code as below to output the pixel data, then | ||||
|         // I logged those pixels here. | ||||
|         const expectedBluePixels = [ | ||||
|             // TODO these pixel sets only work with the first test, but not the second test. | ||||
|  | ||||
|             // [60, 35], | ||||
|             // [121, 125], | ||||
|             // [156, 377], | ||||
|             // [264, 73], | ||||
|             // [372, 186], | ||||
|             // [576, 73], | ||||
|             // [659, 439], | ||||
|             // [675, 423] | ||||
|  | ||||
|             [60, 35], | ||||
|             [120, 125], | ||||
|             [156, 375], | ||||
|             [264, 73], | ||||
|             [372, 185], | ||||
|             [575, 72], | ||||
|             [659, 437], | ||||
|             [675, 421] | ||||
|         ]; | ||||
|  | ||||
|         // The first canvas in the DOM is the one that has the plot point | ||||
|         // icons (canvas 2d), which is the one we are testing. The second | ||||
|         // one in the DOM is the WebGL canvas with the line. (Why aren't | ||||
|         // they both WebGL?) | ||||
|         const canvas = document.querySelector('canvas'); | ||||
|  | ||||
|         const ctx = canvas.getContext('2d'); | ||||
|  | ||||
|         for (const pixel of expectedBluePixels) { | ||||
|             // XXX Possible optimization: call getImageData only once with | ||||
|             // area including all pixels to be tested. | ||||
|             const data = ctx.getImageData(pixel[0], pixel[1], 1, 1).data; | ||||
|  | ||||
|             // #43b0ffff <-- openmct cyanish-blue with 100% opacity | ||||
|             // if (data[0] !== 0x43 || data[1] !== 0xb0 || data[2] !== 0xff || data[3] !== 0xff) { | ||||
|             if (data[0] === 0 && data[1] === 0 && data[2] === 0 && data[3] === 0) { | ||||
|                 // If any pixel is empty, it means we didn't hit a plot point. | ||||
|                 return false; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return true; | ||||
|     }); | ||||
|  | ||||
|     expect(pixelsMatch).toBe(true); | ||||
| } | ||||
							
								
								
									
										155
									
								
								e2e/tests/plugins/plot/missingPlotObj.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								e2e/tests/plugins/plot/missingPlotObj.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,155 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* | ||||
| Tests to verify log plot functionality when objects are missing | ||||
| */ | ||||
|  | ||||
| const { test } = require('../../../fixtures.js'); | ||||
| const { expect } = require('@playwright/test'); | ||||
|  | ||||
| test.describe('Handle missing object for plots', () => { | ||||
|     test('Displays empty div for missing stacked plot item', async ({ page, browserName }) => { | ||||
|         test.fixme(browserName === 'firefox', 'Firefox failing due to console events being missed'); | ||||
|         const errorLogs = []; | ||||
|  | ||||
|         page.on("console", (message) => { | ||||
|             if (message.type() === 'warning' && message.text().includes('Missing domain object')) { | ||||
|                 errorLogs.push(message.text()); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         //Make stacked plot | ||||
|         await makeStackedPlot(page); | ||||
|  | ||||
|         //Gets local storage and deletes the last sine wave generator in the stacked plot | ||||
|         const localStorage = await page.evaluate(() => window.localStorage); | ||||
|         const parsedData = JSON.parse(localStorage.mct); | ||||
|         const keys = Object.keys(parsedData); | ||||
|         const lastKey = keys[keys.length - 1]; | ||||
|  | ||||
|         delete parsedData[lastKey]; | ||||
|  | ||||
|         //Sets local storage with missing object | ||||
|         await page.evaluate( | ||||
|             `window.localStorage.setItem('mct', '${JSON.stringify(parsedData)}')` | ||||
|         ); | ||||
|  | ||||
|         //Reloads page and clicks on stacked plot | ||||
|         await Promise.all([ | ||||
|             page.reload(), | ||||
|             page.waitForLoadState('networkidle') | ||||
|         ]); | ||||
|  | ||||
|         //Verify Main section is there on load | ||||
|         await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Stacked Plot'); | ||||
|  | ||||
|         await page.locator('text=Open MCT My Items >> span').nth(3).click(); | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('text=Unnamed Stacked Plot').first().click() | ||||
|         ]); | ||||
|  | ||||
|         //Check that there is only one stacked item plot with a plot, the missing one will be empty | ||||
|         await expect(page.locator(".c-plot--stacked-container:has(.gl-plot)")).toHaveCount(1); | ||||
|         //Verify that console.warn is thrown | ||||
|         expect(errorLogs).toHaveLength(1); | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| /** | ||||
|  * This is used the create a stacked plot object | ||||
|  * @private | ||||
|  */ | ||||
| async function makeStackedPlot(page) { | ||||
|     // fresh page with time range from 2022-03-29 22:00:00.000Z to 2022-03-29 22:00:30.000Z | ||||
|     await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|     // create stacked plot | ||||
|     await page.locator('button.c-create-button').click(); | ||||
|     await page.locator('li:has-text("Stacked Plot")').click(); | ||||
|  | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation({ waitUntil: 'networkidle'}), | ||||
|         page.locator('text=OK').click(), | ||||
|         //Wait for Save Banner to appear | ||||
|         page.waitForSelector('.c-message-banner__message') | ||||
|     ]); | ||||
|  | ||||
|     // save the stacked plot | ||||
|     await saveStackedPlot(page); | ||||
|  | ||||
|     // create a sinewave generator | ||||
|     await createSineWaveGenerator(page); | ||||
|  | ||||
|     // click on stacked plot | ||||
|     await page.locator('text=Open MCT My Items >> span').nth(3).click(); | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('text=Unnamed Stacked Plot').first().click() | ||||
|     ]); | ||||
|  | ||||
|     // create a second sinewave generator | ||||
|     await createSineWaveGenerator(page); | ||||
|  | ||||
|     // click on stacked plot | ||||
|     await page.locator('text=Open MCT My Items >> span').nth(3).click(); | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('text=Unnamed Stacked Plot').first().click() | ||||
|     ]); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * This is used to save a stacked plot object | ||||
|  * @private | ||||
|  */ | ||||
| async function saveStackedPlot(page) { | ||||
|     // save stacked plot | ||||
|     await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click(); | ||||
|  | ||||
|     await Promise.all([ | ||||
|         page.locator('text=Save and Finish Editing').click(), | ||||
|         //Wait for Save Banner to appear | ||||
|         page.waitForSelector('.c-message-banner__message') | ||||
|     ]); | ||||
|     //Wait until Save Banner is gone | ||||
|     await page.locator('.c-message-banner__close-button').click(); | ||||
|     await page.waitForSelector('.c-message-banner__message', { state: 'detached' }); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * This is used to create a sine wave generator object | ||||
|  * @private | ||||
|  */ | ||||
| async function createSineWaveGenerator(page) { | ||||
|     //Create sine wave generator | ||||
|     await page.locator('button.c-create-button').click(); | ||||
|     await page.locator('li:has-text("Sine Wave Generator")').click(); | ||||
|  | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation({ waitUntil: 'networkidle'}), | ||||
|         page.locator('text=OK').click(), | ||||
|         //Wait for Save Banner to appear | ||||
|         page.waitForSelector('.c-message-banner__message') | ||||
|     ]); | ||||
| } | ||||
							
								
								
									
										41
									
								
								e2e/tests/plugins/remoteClock/remoteClock.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								e2e/tests/plugins/remoteClock/remoteClock.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| const { test } = require('../../../fixtures.js'); | ||||
| // eslint-disable-next-line no-unused-vars | ||||
| const { expect } = require('@playwright/test'); | ||||
|  | ||||
| test.describe('Remote Clock', () => { | ||||
|     // eslint-disable-next-line require-await | ||||
|     test.fixme('blocks historical requests until first tick is received', async ({ page }) => { | ||||
|         test.info().annotations.push({ | ||||
|             type: 'issue', | ||||
|             description: 'https://github.com/nasa/openmct/issues/5221' | ||||
|         }); | ||||
|         // addInitScript to with remote clock | ||||
|         // Switch time conductor mode to 'remote clock' | ||||
|         // Navigate to telemetry | ||||
|         // Verify that the plot renders historical data within the correct bounds | ||||
|         // Refresh the page | ||||
|         // Verify again that the plot renders historical data within the correct bounds | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										104
									
								
								e2e/tests/plugins/telemetryTable/telemetryTable.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								e2e/tests/plugins/telemetryTable/telemetryTable.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,104 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| const { test } = require('../../../fixtures'); | ||||
| const { expect } = require('@playwright/test'); | ||||
|  | ||||
| test.describe('Telemetry Table', () => { | ||||
|     test('unpauses and filters data when paused by button and user changes bounds', async ({ page }) => { | ||||
|         test.info().annotations.push({ | ||||
|             type: 'issue', | ||||
|             description: 'https://github.com/nasa/openmct/issues/5113' | ||||
|         }); | ||||
|  | ||||
|         const bannerMessage = '.c-message-banner__message'; | ||||
|         const createButton = 'button:has-text("Create")'; | ||||
|  | ||||
|         await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|         // Click create button | ||||
|         await page.locator(createButton).click(); | ||||
|         await page.locator('li:has-text("Telemetry Table")').click(); | ||||
|  | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('text=OK').click(), | ||||
|             // Wait for Save Banner to appear | ||||
|             page.waitForSelector(bannerMessage) | ||||
|         ]); | ||||
|  | ||||
|         // Save (exit edit mode) | ||||
|         await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(3).click(); | ||||
|         await page.locator('text=Save and Finish Editing').click(); | ||||
|  | ||||
|         // Click create button | ||||
|         await page.locator(createButton).click(); | ||||
|  | ||||
|         // add Sine Wave Generator with defaults | ||||
|         await page.locator('li:has-text("Sine Wave Generator")').click(); | ||||
|  | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('text=OK').click(), | ||||
|             // Wait for Save Banner to appear | ||||
|             page.waitForSelector(bannerMessage) | ||||
|         ]); | ||||
|  | ||||
|         // focus the Telemetry Table | ||||
|         await page.locator('text=Open MCT My Items >> span').nth(3).click(); | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('text=Unnamed Telemetry Table').first().click() | ||||
|         ]); | ||||
|  | ||||
|         // Click pause button | ||||
|         const pauseButton = page.locator('button.c-button.icon-pause'); | ||||
|         await pauseButton.click(); | ||||
|  | ||||
|         const tableWrapper = page.locator('div.c-table-wrapper'); | ||||
|         await expect(tableWrapper).toHaveClass(/is-paused/); | ||||
|  | ||||
|         // Subtract 5 minutes from the current end bound datetime and set it | ||||
|         const endTimeInput = page.locator('input[type="text"].c-input--datetime').nth(1); | ||||
|         await endTimeInput.click(); | ||||
|  | ||||
|         let endDate = await endTimeInput.inputValue(); | ||||
|         endDate = new Date(endDate); | ||||
|  | ||||
|         endDate.setUTCMinutes(endDate.getUTCMinutes() - 5); | ||||
|         endDate = endDate.toISOString().replace(/T/, ' '); | ||||
|  | ||||
|         await endTimeInput.fill(''); | ||||
|         await endTimeInput.fill(endDate); | ||||
|         await page.keyboard.press('Enter'); | ||||
|  | ||||
|         await expect(tableWrapper).not.toHaveClass(/is-paused/); | ||||
|  | ||||
|         // Get the most recent telemetry date | ||||
|         const latestTelemetryDate = await page.locator('table.c-telemetry-table__body > tbody > tr').last().locator('td').nth(1).getAttribute('title'); | ||||
|  | ||||
|         // Verify that it is <= our new end bound | ||||
|         const latestMilliseconds = Date.parse(latestTelemetryDate); | ||||
|         const endBoundMilliseconds = Date.parse(endDate); | ||||
|         expect(latestMilliseconds).toBeLessThanOrEqual(endBoundMilliseconds); | ||||
|     }); | ||||
| }); | ||||
| @@ -20,11 +20,12 @@ | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| const { test, expect } = require('@playwright/test'); | ||||
| const { test } = require('../../../fixtures.js'); | ||||
| const { expect } = require('@playwright/test'); | ||||
|  | ||||
| test.describe('Time counductor operations', () => { | ||||
| test.describe('Time conductor operations', () => { | ||||
|     test('validate start time does not exceeds end time', async ({ page }) => { | ||||
|         //Go to baseURL | ||||
|         // Go to baseURL | ||||
|         await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|         const year = new Date().getFullYear(); | ||||
|  | ||||
| @@ -67,3 +68,168 @@ test.describe('Time counductor operations', () => { | ||||
|         expect(endDateValidityStatus).not.toBeTruthy(); | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| // Testing instructions: | ||||
| // Try to change the realtime offsets when in realtime (local clock) mode. | ||||
| test.describe('Time conductor input fields real-time mode', () => { | ||||
|     test('validate input fields in real-time mode', async ({ page }) => { | ||||
|         const startOffset = { | ||||
|             secs: '23' | ||||
|         }; | ||||
|  | ||||
|         const endOffset = { | ||||
|             secs: '31' | ||||
|         }; | ||||
|  | ||||
|         // Go to baseURL | ||||
|         await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|         // Switch to real-time mode | ||||
|         await setRealTimeMode(page); | ||||
|  | ||||
|         // Set start time offset | ||||
|         await setStartOffset(page, startOffset); | ||||
|  | ||||
|         // Verify time was updated on time offset button | ||||
|         await expect(page.locator('data-testid=conductor-start-offset-button')).toContainText('00:30:23'); | ||||
|  | ||||
|         // Set end time offset | ||||
|         await setEndOffset(page, endOffset); | ||||
|  | ||||
|         // Verify time was updated on preceding time offset button | ||||
|         await expect(page.locator('data-testid=conductor-end-offset-button')).toContainText('00:00:31'); | ||||
|     }); | ||||
|  | ||||
|     /** | ||||
|      * Verify that offsets and url params are preserved when switching | ||||
|      * between fixed timespan and real-time mode. | ||||
|      */ | ||||
|     test('preserve offsets and url params when switching between fixed and real-time mode', async ({ page }) => { | ||||
|         const startOffset = { | ||||
|             mins: '30', | ||||
|             secs: '23' | ||||
|         }; | ||||
|  | ||||
|         const endOffset = { | ||||
|             secs: '01' | ||||
|         }; | ||||
|  | ||||
|         // Convert offsets to milliseconds | ||||
|         const startDelta = (30 * 60 * 1000) + (23 * 1000); | ||||
|         const endDelta = (1 * 1000); | ||||
|  | ||||
|         // Go to baseURL | ||||
|         await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|         // Switch to real-time mode | ||||
|         await setRealTimeMode(page); | ||||
|  | ||||
|         // Set start time offset | ||||
|         await setStartOffset(page, startOffset); | ||||
|  | ||||
|         // Set end time offset | ||||
|         await setEndOffset(page, endOffset); | ||||
|  | ||||
|         // Switch to fixed timespan mode | ||||
|         await setFixedTimeMode(page); | ||||
|  | ||||
|         // Switch back to real-time mode | ||||
|         await setRealTimeMode(page); | ||||
|  | ||||
|         // Verify updated start time offset persists after mode switch | ||||
|         await expect(page.locator('data-testid=conductor-start-offset-button')).toContainText('00:30:23'); | ||||
|  | ||||
|         // Verify updated end time offset persists after mode switch | ||||
|         await expect(page.locator('data-testid=conductor-end-offset-button')).toContainText('00:00:01'); | ||||
|  | ||||
|         // Verify url parameters persist after mode switch | ||||
|         await page.waitForNavigation(); | ||||
|         expect(page.url()).toContain(`startDelta=${startDelta}`); | ||||
|         expect(page.url()).toContain(`endDelta=${endDelta}`); | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| /** | ||||
|  * @typedef {Object} OffsetValues | ||||
|  * @property {string | undefined} hours | ||||
|  * @property {string | undefined} mins | ||||
|  * @property {string | undefined} secs | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * Set the values (hours, mins, secs) for the start time offset when in realtime mode | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  * @param {OffsetValues} offset | ||||
|  */ | ||||
| async function setStartOffset(page, offset) { | ||||
|     const startOffsetButton = page.locator('data-testid=conductor-start-offset-button'); | ||||
|     await setTimeConductorOffset(page, offset, startOffsetButton); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Set the values (hours, mins, secs) for the end time offset when in realtime mode | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  * @param {OffsetValues} offset | ||||
|  */ | ||||
| async function setEndOffset(page, offset) { | ||||
|     const endOffsetButton = page.locator('data-testid=conductor-end-offset-button'); | ||||
|     await setTimeConductorOffset(page, offset, endOffsetButton); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Set the time conductor to fixed timespan mode | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function setFixedTimeMode(page) { | ||||
|     await setTimeConductorMode(page, true); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Set the time conductor to realtime mode | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function setRealTimeMode(page) { | ||||
|     await setTimeConductorMode(page, false); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Set the values (hours, mins, secs) for the TimeConductor offsets when in realtime mode | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  * @param {OffsetValues} offset | ||||
|  * @param {import('@playwright/test').Locator} offsetButton | ||||
|  */ | ||||
| async function setTimeConductorOffset(page, {hours, mins, secs}, offsetButton) { | ||||
|     await offsetButton.click(); | ||||
|  | ||||
|     if (hours) { | ||||
|         await page.fill('.pr-time-controls__hrs', hours); | ||||
|     } | ||||
|  | ||||
|     if (mins) { | ||||
|         await page.fill('.pr-time-controls__mins', mins); | ||||
|     } | ||||
|  | ||||
|     if (secs) { | ||||
|         await page.fill('.pr-time-controls__secs', secs); | ||||
|     } | ||||
|  | ||||
|     // Click the check button | ||||
|     await page.locator('.icon-check').click(); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Set the time conductor mode to either fixed timespan or realtime mode. | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  * @param {boolean} [isFixedTimespan=true] true for fixed timespan mode, false for realtime mode; default is true | ||||
|  */ | ||||
| async function setTimeConductorMode(page, isFixedTimespan = true) { | ||||
|     // Click 'mode' button | ||||
|     await page.locator('.c-mode-button').click(); | ||||
|  | ||||
|     // Switch time conductor mode | ||||
|     if (isFixedTimespan) { | ||||
|         await page.locator('data-testid=conductor-modeOption-fixed').click(); | ||||
|     } else { | ||||
|         await page.locator('data-testid=conductor-modeOption-realtime').click(); | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										185
									
								
								e2e/tests/plugins/timer/timer.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										185
									
								
								e2e/tests/plugins/timer/timer.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,185 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| const { test } = require('../../../fixtures.js'); | ||||
| const { expect } = require('@playwright/test'); | ||||
|  | ||||
| test.describe('Timer', () => { | ||||
|  | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         //Go to baseURL | ||||
|         await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|         //Click the Create button | ||||
|         await page.click('button:has-text("Create")'); | ||||
|  | ||||
|         // Click 'Timer' | ||||
|         await page.click('text=Timer'); | ||||
|  | ||||
|         // Click text=OK | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation({waitUntil: 'networkidle'}), | ||||
|             page.click('text=OK') | ||||
|         ]); | ||||
|  | ||||
|         await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Timer'); | ||||
|     }); | ||||
|  | ||||
|     test('Can perform actions on the Timer', async ({ page }) => { | ||||
|         test.info().annotations.push({ | ||||
|             type: 'issue', | ||||
|             description: 'https://github.com/nasa/openmct/issues/4313' | ||||
|         }); | ||||
|  | ||||
|         await test.step("From the tree context menu", async () => { | ||||
|             await triggerTimerContextMenuAction(page, 'Start'); | ||||
|             await triggerTimerContextMenuAction(page, 'Pause'); | ||||
|             await triggerTimerContextMenuAction(page, 'Restart at 0'); | ||||
|             await triggerTimerContextMenuAction(page, 'Stop'); | ||||
|         }); | ||||
|  | ||||
|         await test.step("From the 3dot menu", async () => { | ||||
|             await triggerTimer3dotMenuAction(page, 'Start'); | ||||
|             await triggerTimer3dotMenuAction(page, 'Pause'); | ||||
|             await triggerTimer3dotMenuAction(page, 'Restart at 0'); | ||||
|             await triggerTimer3dotMenuAction(page, 'Stop'); | ||||
|         }); | ||||
|  | ||||
|         await test.step("From the object view", async () => { | ||||
|             await triggerTimerViewAction(page, 'Start'); | ||||
|             await triggerTimerViewAction(page, 'Pause'); | ||||
|             await triggerTimerViewAction(page, 'Restart at 0'); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| /** | ||||
|  * Actions that can be performed on a timer from context menus. | ||||
|  * @typedef {'Start' | 'Stop' | 'Pause' | 'Restart at 0'} TimerAction | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * Actions that can be performed on a timer from the object view. | ||||
|  * @typedef {'Start' | 'Pause' | 'Restart at 0'} TimerViewAction | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * Open the timer context menu from the object tree. | ||||
|  * Expands the 'My Items' folder if it is not already expanded. | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function openTimerContextMenu(page) { | ||||
|     const myItemsFolder = page.locator('text=Open MCT My Items >> span').nth(3); | ||||
|     const className = await myItemsFolder.getAttribute('class'); | ||||
|     if (!className.includes('c-disclosure-triangle--expanded')) { | ||||
|         await myItemsFolder.click(); | ||||
|     } | ||||
|  | ||||
|     await page.locator(`a:has-text("Unnamed Timer")`).click({ | ||||
|         button: 'right' | ||||
|     }); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Trigger a timer action from the tree context menu | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  * @param {TimerAction} action | ||||
|  */ | ||||
| async function triggerTimerContextMenuAction(page, action) { | ||||
|     const menuAction = `.c-menu ul li >> text="${action}"`; | ||||
|     await openTimerContextMenu(page); | ||||
|     await page.locator(menuAction).click(); | ||||
|     assertTimerStateAfterAction(page, action); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Trigger a timer action from the 3dot menu | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  * @param {TimerAction} action | ||||
|  */ | ||||
| async function triggerTimer3dotMenuAction(page, action) { | ||||
|     const menuAction = `.c-menu ul li >> text="${action}"`; | ||||
|     const threeDotMenuButton = 'button[title="More options"]'; | ||||
|     let isActionAvailable = false; | ||||
|     let iterations = 0; | ||||
|     // Dismiss/open the 3dot menu until the action is available | ||||
|     // or a maxiumum number of iterations is reached | ||||
|     while (!isActionAvailable && iterations <= 20) { | ||||
|         await page.click('.c-object-view'); | ||||
|         await page.click(threeDotMenuButton); | ||||
|         isActionAvailable = await page.locator(menuAction).isVisible(); | ||||
|         iterations++; | ||||
|     } | ||||
|  | ||||
|     await page.locator(menuAction).click(); | ||||
|     assertTimerStateAfterAction(page, action); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Trigger a timer action from the object view | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  * @param {TimerViewAction} action | ||||
|  */ | ||||
| async function triggerTimerViewAction(page, action) { | ||||
|     await page.locator('.c-timer').hover({trial: true}); | ||||
|     const buttonTitle = buttonTitleFromAction(action); | ||||
|     await page.click(`button[title="${buttonTitle}"]`); | ||||
|     assertTimerStateAfterAction(page, action); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Takes in a TimerViewAction and returns the button title | ||||
|  * @param {TimerViewAction} action | ||||
|  */ | ||||
| function buttonTitleFromAction(action) { | ||||
|     switch (action) { | ||||
|     case 'Start': | ||||
|         return 'Start'; | ||||
|     case 'Pause': | ||||
|         return 'Pause'; | ||||
|     case 'Restart at 0': | ||||
|         return 'Reset'; | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Verify the timer state after a timer action has been performed. | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  * @param {TimerAction} action | ||||
|  */ | ||||
| async function assertTimerStateAfterAction(page, action) { | ||||
|     let timerStateClass; | ||||
|     switch (action) { | ||||
|     case 'Start': | ||||
|     case 'Restart at 0': | ||||
|         timerStateClass = "is-started"; | ||||
|         break; | ||||
|     case 'Stop': | ||||
|         timerStateClass = 'is-stopped'; | ||||
|         break; | ||||
|     case 'Pause': | ||||
|         timerStateClass = 'is-paused'; | ||||
|         break; | ||||
|     } | ||||
|  | ||||
|     await expect.soft(page.locator('.c-timer')).toHaveClass(new RegExp(timerStateClass)); | ||||
| } | ||||
| @@ -33,7 +33,8 @@ comfortable running this test during a live mission?" Avoid creating or deleting | ||||
| Make no assumptions about the order that elements appear in the DOM. | ||||
| */ | ||||
| 
 | ||||
| const { test, expect } = require('@playwright/test'); | ||||
| const { test } = require('../fixtures.js'); | ||||
| const { expect } = require('@playwright/test'); | ||||
| 
 | ||||
| test('Verify that the create button appears and that the Folder Domain Object is available for selection', async ({ page }) => { | ||||
| 
 | ||||
| @@ -44,6 +45,15 @@ test('Verify that the create button appears and that the Folder Domain Object is | ||||
|     await page.click('button:has-text("Create")'); | ||||
| 
 | ||||
|     // Verify that Create Folder appears in the dropdown
 | ||||
|     const locator = page.locator(':nth-match(:text("Folder"), 2)'); | ||||
|     await expect(locator).toBeEnabled(); | ||||
|     await expect(page.locator(':nth-match(:text("Folder"), 2)')).toBeEnabled(); | ||||
| }); | ||||
| 
 | ||||
| test('Verify that My Items Tree appears @ipad', async ({ page }) => { | ||||
|     //Test.slow annotation is currently broken. Needs to be fixed in https://github.com/nasa/openmct/issues/5374
 | ||||
|     test.slow(); | ||||
|     //Go to baseURL
 | ||||
|     await page.goto('/'); | ||||
| 
 | ||||
|     //My Items to be visible
 | ||||
|     await expect(page.locator('a:has-text("My Items")')).toBeEnabled(); | ||||
| }); | ||||
							
								
								
									
										111
									
								
								e2e/tests/ui/layout/search/grandsearch.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								e2e/tests/ui/layout/search/grandsearch.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,111 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* | ||||
| This test suite is dedicated to tests which verify search functionality. | ||||
| */ | ||||
|  | ||||
| const { expect } = require('@playwright/test'); | ||||
| const { test } = require('../../../../fixtures'); | ||||
|  | ||||
| /** | ||||
|   * Creates a notebook object and adds an entry. | ||||
|   * @param {import('@playwright/test').Page} page | ||||
|   */ | ||||
| async function createClockAndDisplayLayout(page) { | ||||
|     //Go to baseURL | ||||
|     await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|     // Click button:has-text("Create") | ||||
|     await page.locator('button:has-text("Create")').click(); | ||||
|     // Click li:has-text("Notebook") | ||||
|     await page.locator('li:has-text("Clock")').click(); | ||||
|     // Click button:has-text("OK") | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('button:has-text("OK")').click() | ||||
|     ]); | ||||
|  | ||||
|     // Click a:has-text("My Items") | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('a:has-text("My Items") >> nth=0').click() | ||||
|     ]); | ||||
|     // Click button:has-text("Create") | ||||
|     await page.locator('button:has-text("Create")').click(); | ||||
|     // Click li:has-text("Notebook") | ||||
|     await page.locator('li:has-text("Display Layout")').click(); | ||||
|     // Click button:has-text("OK") | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('button:has-text("OK")').click() | ||||
|     ]); | ||||
| } | ||||
|  | ||||
| test.describe('Grand Search', () => { | ||||
|     test('Can search for objects, and subsequent search dropdown behaves properly', async ({ page }) => { | ||||
|         await createClockAndDisplayLayout(page); | ||||
|  | ||||
|         // Click [aria-label="OpenMCT Search"] input[type="search"] | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); | ||||
|         // Fill [aria-label="OpenMCT Search"] input[type="search"] | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Cl'); | ||||
|         await expect(page.locator('[aria-label="Search Result"]')).toContainText('Clock'); | ||||
|         // Click text=Elements >> nth=0 | ||||
|         await page.locator('text=Elements').first().click(); | ||||
|         await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible(); | ||||
|  | ||||
|         // Click [aria-label="OpenMCT Search"] [aria-label="Search Input"] | ||||
|         await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click(); | ||||
|         // Click [aria-label="Unnamed Clock clock result"] >> text=Unnamed Clock | ||||
|         await page.locator('[aria-label="Unnamed Clock clock result"] >> text=Unnamed Clock').click(); | ||||
|         await expect(page.locator('.js-preview-window')).toBeVisible(); | ||||
|  | ||||
|         // Click [aria-label="Close"] | ||||
|         await page.locator('[aria-label="Close"]').click(); | ||||
|         await expect(page.locator('[aria-label="Search Result"]')).toBeVisible(); | ||||
|         await expect(page.locator('[aria-label="Search Result"]')).toContainText('Cloc'); | ||||
|  | ||||
|         // Click [aria-label="OpenMCT Search"] a >> nth=0 | ||||
|         await page.locator('[aria-label="OpenMCT Search"] a').first().click(); | ||||
|         await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible(); | ||||
|  | ||||
|         // Fill [aria-label="OpenMCT Search"] input[type="search"] | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('foo'); | ||||
|         await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible(); | ||||
|  | ||||
|         // Click text=Snapshot Save and Finish Editing Save and Continue Editing >> button >> nth=1 | ||||
|         await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click(); | ||||
|         // Click text=Save and Finish Editing | ||||
|         await page.locator('text=Save and Finish Editing').click(); | ||||
|         // Click [aria-label="OpenMCT Search"] [aria-label="Search Input"] | ||||
|         await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click(); | ||||
|         // Fill [aria-label="OpenMCT Search"] [aria-label="Search Input"] | ||||
|         await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').fill('Cl'); | ||||
|         // Click text=Unnamed Clock | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('text=Unnamed Clock').click() | ||||
|         ]); | ||||
|         await expect(page.locator('.is-object-type-clock')).toBeVisible(); | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										76
									
								
								e2e/tests/visual/addInit.visual.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								e2e/tests/visual/addInit.visual.spec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | ||||
| /* eslint-disable no-undef */ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* | ||||
| Collection of Visual Tests set to run with modified init scripts to inject plugins not otherwise available in the default contexts. | ||||
|  | ||||
| These should only use functional expect statements to verify assumptions about the state | ||||
| in a test and not for functional verification of correctness. Visual tests are not supposed | ||||
| to "fail" on assertions. Instead, they should be used to detect changes between builds or branches. | ||||
|  | ||||
| Note: Larger testsuite sizes are OK due to the setup time associated with these tests. | ||||
| */ | ||||
|  | ||||
| const { test } = require('../../fixtures.js'); | ||||
| const percySnapshot = require('@percy/playwright'); | ||||
| const path = require('path'); | ||||
| const sinon = require('sinon'); | ||||
|  | ||||
| const VISUAL_GRACE_PERIOD = 5 * 1000; //Lets the application "simmer" before the snapshot is taken | ||||
|  | ||||
| const CUSTOM_NAME = 'CUSTOM_NAME'; | ||||
|  | ||||
| // Snippet from https://github.com/microsoft/playwright/issues/6347#issuecomment-965887758 | ||||
| // Will replace with cy.clock() equivalent | ||||
| test.beforeEach(async ({ context }) => { | ||||
|     await context.addInitScript({ | ||||
|         path: path.join(__dirname, '../../..', './node_modules/sinon/pkg/sinon.js') | ||||
|     }); | ||||
|     await context.addInitScript(() => { | ||||
|         window.__clock = sinon.useFakeTimers({ | ||||
|             now: 0, | ||||
|             shouldAdvanceTime: true | ||||
|         }); //Set browser clock to UNIX Epoch | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| test('Visual - Restricted Notebook is visually correct @addInit', async ({ page }) => { | ||||
|     // eslint-disable-next-line no-undef | ||||
|     await page.addInitScript({ path: path.join(__dirname, '../plugins/notebook', './addInitRestrictedNotebook.js') }); | ||||
|     //Go to baseURL | ||||
|     await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|     //Click the Create button | ||||
|     await page.click('button:has-text("Create")'); | ||||
|     // Click text=CUSTOM_NAME | ||||
|     await page.click(`text=${CUSTOM_NAME}`); | ||||
|     // Click text=OK | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation({waitUntil: 'networkidle'}), | ||||
|         page.click('text=OK') | ||||
|     ]); | ||||
|  | ||||
|     // Take a snapshot of the newly created CUSTOM_NAME notebook | ||||
|     await page.waitForTimeout(VISUAL_GRACE_PERIOD); | ||||
|     await percySnapshot(page, 'Restricted Notebook with CUSTOM_NAME'); | ||||
|  | ||||
| }); | ||||
							
								
								
									
										71
									
								
								e2e/tests/visual/controlledClock.visual.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								e2e/tests/visual/controlledClock.visual.spec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,71 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* | ||||
| Collection of Visual Tests set to run in a default context. The tests within this suite | ||||
| are only meant to run against openmct's app.js started by `npm run start` within the | ||||
| `./e2e/playwright-visual.config.js` file. | ||||
|  | ||||
| These should only use functional expect statements to verify assumptions about the state | ||||
| in a test and not for functional verification of correctness. Visual tests are not supposed | ||||
| to "fail" on assertions. Instead, they should be used to detect changes between builds or branches. | ||||
|  | ||||
| Note: Larger testsuite sizes are OK due to the setup time associated with these tests. | ||||
| */ | ||||
|  | ||||
| const { test } = require('../../fixtures.js'); | ||||
| const { expect } = require('@playwright/test'); | ||||
| const percySnapshot = require('@percy/playwright'); | ||||
| const path = require('path'); | ||||
| const sinon = require('sinon'); | ||||
|  | ||||
| // Snippet from https://github.com/microsoft/playwright/issues/6347#issuecomment-965887758 | ||||
| // Will replace with cy.clock() equivalent | ||||
| test.beforeEach(async ({ context }) => { | ||||
|     await context.addInitScript({ | ||||
|         // eslint-disable-next-line no-undef | ||||
|         path: path.join(__dirname, '../../..', './node_modules/sinon/pkg/sinon.js') | ||||
|     }); | ||||
|     await context.addInitScript(() => { | ||||
|         window.__clock = sinon.useFakeTimers({ | ||||
|             now: 0, //Set browser clock to UNIX Epoch | ||||
|             shouldAdvanceTime: false, //Don't advance the clock | ||||
|             toFake: ["setTimeout", "nextTick"] | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
| test.use({ storageState: './e2e/test-data/VisualTestData_storage.json' }); | ||||
|  | ||||
| test('Visual - Overlay Plot Loading Indicator @localstorage', async ({ page }) => { | ||||
|     // Go to baseURL | ||||
|     await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|     await page.locator('a:has-text("Unnamed Overlay Plot Overlay Plot")').click(); | ||||
|     //Ensure that we're on the Unnamed Overlay Plot object | ||||
|     await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Overlay Plot'); | ||||
|  | ||||
|     //Wait for canvas to be rendered and stop animating | ||||
|     await page.locator('canvas >> nth=1').hover({trial: true}); | ||||
|  | ||||
|     //Take snapshot of Sine Wave Generator within Overlay Plot | ||||
|     await percySnapshot(page, 'SineWaveInOverlayPlot'); | ||||
| }); | ||||
| @@ -32,7 +32,8 @@ to "fail" on assertions. Instead, they should be used to detect changes between | ||||
| Note: Larger testsuite sizes are OK due to the setup time associated with these tests. | ||||
| */ | ||||
| 
 | ||||
| const { test, expect } = require('@playwright/test'); | ||||
| const { test } = require('../../fixtures.js'); | ||||
| const { expect } = require('@playwright/test'); | ||||
| const percySnapshot = require('@percy/playwright'); | ||||
| const path = require('path'); | ||||
| const sinon = require('sinon'); | ||||
| @@ -47,7 +48,10 @@ test.beforeEach(async ({ context }) => { | ||||
|         path: path.join(__dirname, '../../..', './node_modules/sinon/pkg/sinon.js') | ||||
|     }); | ||||
|     await context.addInitScript(() => { | ||||
|         window.__clock = sinon.useFakeTimers(); //Set browser clock to UNIX Epoch
 | ||||
|         window.__clock = sinon.useFakeTimers({ | ||||
|             now: 0, | ||||
|             shouldAdvanceTime: true | ||||
|         }); //Set browser clock to UNIX Epoch
 | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| @@ -56,8 +60,7 @@ test('Visual - Root and About', async ({ page }) => { | ||||
|     await page.goto('/', { waitUntil: 'networkidle' }); | ||||
| 
 | ||||
|     // Verify that Create button is actionable
 | ||||
|     const createButtonLocator = page.locator('button:has-text("Create")'); | ||||
|     await expect(createButtonLocator).toBeEnabled(); | ||||
|     await expect(page.locator('button:has-text("Create")')).toBeEnabled(); | ||||
| 
 | ||||
|     // Take a snapshot of the Dashboard
 | ||||
|     await page.waitForTimeout(VISUAL_GRACE_PERIOD); | ||||
| @@ -94,7 +97,11 @@ test('Visual - Default Condition Set', async ({ page }) => { | ||||
|     await percySnapshot(page, 'Default Condition Set'); | ||||
| }); | ||||
| 
 | ||||
| test('Visual - Default Condition Widget', async ({ page }) => { | ||||
| test.fixme('Visual - Default Condition Widget', async ({ page }) => { | ||||
|     test.info().annotations.push({ | ||||
|         type: 'issue', | ||||
|         description: 'https://github.com/nasa/openmct/issues/5349' | ||||
|     }); | ||||
|     //Go to baseURL
 | ||||
|     await page.goto('/', { waitUntil: 'networkidle' }); | ||||
| 
 | ||||
| @@ -171,3 +178,55 @@ test('Visual - Sine Wave Generator Form', async ({ page }) => { | ||||
|     await page.waitForTimeout(VISUAL_GRACE_PERIOD); | ||||
|     await percySnapshot(page, 'removed amplitude property value'); | ||||
| }); | ||||
| 
 | ||||
| test('Visual - Save Successful Banner', async ({ page }) => { | ||||
|     //Go to baseURL
 | ||||
|     await page.goto('/', { waitUntil: 'networkidle' }); | ||||
| 
 | ||||
|     //Click the Create button
 | ||||
|     await page.click('button:has-text("Create")'); | ||||
| 
 | ||||
|     //NOTE Something other than example imagery
 | ||||
|     await page.click('text=Timer'); | ||||
| 
 | ||||
|     // Click text=OK
 | ||||
|     await page.click('text=OK'); | ||||
|     await page.locator('.c-message-banner__message').hover({ trial: true }); | ||||
|     await percySnapshot(page, 'Banner message shown'); | ||||
| 
 | ||||
|     //Wait until Save Banner is gone
 | ||||
|     await page.waitForSelector('.c-message-banner__message', { state: 'detached'}); | ||||
|     await percySnapshot(page, 'Banner message gone'); | ||||
| }); | ||||
| 
 | ||||
| test('Visual - Display Layout Icon is correct', async ({ page }) => { | ||||
|     //Go to baseURL
 | ||||
|     await page.goto('/', { waitUntil: 'networkidle' }); | ||||
| 
 | ||||
|     //Click the Create button
 | ||||
|     await page.click('button:has-text("Create")'); | ||||
| 
 | ||||
|     //Hover on Display Layout option.
 | ||||
|     await page.locator('text=Display Layout').hover(); | ||||
|     await percySnapshot(page, 'Display Layout Create Menu'); | ||||
| 
 | ||||
| }); | ||||
| 
 | ||||
| test('Visual - Default Gauge is correct', async ({ page }) => { | ||||
| 
 | ||||
|     //Go to baseURL
 | ||||
|     await page.goto('/', { waitUntil: 'networkidle' }); | ||||
| 
 | ||||
|     //Click the Create button
 | ||||
|     await page.click('button:has-text("Create")'); | ||||
| 
 | ||||
|     await page.click('text=Gauge'); | ||||
| 
 | ||||
|     await page.click('text=OK'); | ||||
| 
 | ||||
|     // Take a snapshot of the newly created Gauge object
 | ||||
|     await page.waitForTimeout(VISUAL_GRACE_PERIOD); | ||||
|     await percySnapshot(page, 'Default Gauge'); | ||||
| 
 | ||||
| }); | ||||
| 
 | ||||
							
								
								
									
										86
									
								
								e2e/tests/visual/generateVisualTestData.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								e2e/tests/visual/generateVisualTestData.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,86 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* | ||||
| This test suite is dedicated to generating LocalStorage via Session Storage to be used | ||||
| in some visual test suites like controlledClock.visual.spec.js. This suite should run to completion | ||||
| and generate an artifact named ./e2e/test-data/VisualTestData_storage.json . This will run | ||||
| on every Commit to ensure that this object still loads into tests correctly and will retain the | ||||
| .e2e.spec.js suffix. | ||||
|  | ||||
| TODO: Provide additional validation of object properties as it grows. | ||||
|  | ||||
| */ | ||||
|  | ||||
| const { test } = require('../../fixtures.js'); | ||||
| const { expect } = require('@playwright/test'); | ||||
|  | ||||
| test('Generate Visual Test Data @localStorage', async ({ page, context }) => { | ||||
|     //Go to baseURL | ||||
|     await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|     await page.locator('button:has-text("Create")').click(); | ||||
|  | ||||
|     // add overlay plot with defaults | ||||
|     await page.locator('li:has-text("Overlay Plot")').click(); | ||||
|  | ||||
|     // Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184 | ||||
|     await page.click('form[name="mctForm"] a:has-text("My Items")'); | ||||
|  | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('text=OK').click(), | ||||
|         //Wait for Save Banner to appear1 | ||||
|         page.waitForSelector('.c-message-banner__message') | ||||
|     ]); | ||||
|  | ||||
|     // save (exit edit mode) | ||||
|     await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click(); | ||||
|     await page.locator('text=Save and Finish Editing').click(); | ||||
|  | ||||
|     // click create button | ||||
|     await page.locator('button:has-text("Create")').click(); | ||||
|  | ||||
|     // add sine wave generator with defaults | ||||
|     await page.locator('li:has-text("Sine Wave Generator")').click(); | ||||
|  | ||||
|     //Add a 5000 ms Delay | ||||
|     await page.locator('[aria-label="Loading Delay \\(ms\\)"]').fill('5000'); | ||||
|  | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('text=OK').click(), | ||||
|         //Wait for Save Banner to appear1 | ||||
|         page.waitForSelector('.c-message-banner__message') | ||||
|     ]); | ||||
|  | ||||
|     // focus the overlay plot | ||||
|     await page.locator('text=Open MCT My Items >> span').nth(3).click(); | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('text=Unnamed Overlay Plot').first().click() | ||||
|     ]); | ||||
|  | ||||
|     await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Overlay Plot'); | ||||
|     //Save localStorage for future test execution | ||||
|     await context.storageState({ path: './e2e/test-data/VisualTestData_storage.json' }); | ||||
| }); | ||||
							
								
								
									
										105
									
								
								e2e/tests/visual/search.visual.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								e2e/tests/visual/search.visual.spec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,105 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* | ||||
| This test suite is dedicated to tests which verify search functionality. | ||||
| */ | ||||
|  | ||||
| const { test } = require('../../fixtures.js'); | ||||
| const { expect } = require('@playwright/test'); | ||||
| const percySnapshot = require('@percy/playwright'); | ||||
|  | ||||
| /** | ||||
|   * Creates a notebook object and adds an entry. | ||||
|   * @param {import('@playwright/test').Page} page | ||||
|   */ | ||||
| async function createClockAndDisplayLayout(page) { | ||||
|     //Go to baseURL | ||||
|     await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|     // Click button:has-text("Create") | ||||
|     await page.locator('button:has-text("Create")').click(); | ||||
|     // Click li:has-text("Notebook") | ||||
|     await page.locator('li:has-text("Clock")').click(); | ||||
|     // Click button:has-text("OK") | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('button:has-text("OK")').click() | ||||
|     ]); | ||||
|  | ||||
|     // Click a:has-text("My Items") | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('a:has-text("My Items") >> nth=0').click() | ||||
|     ]); | ||||
|     // Click button:has-text("Create") | ||||
|     await page.locator('button:has-text("Create")').click(); | ||||
|     // Click li:has-text("Notebook") | ||||
|     await page.locator('li:has-text("Display Layout")').click(); | ||||
|     // Click button:has-text("OK") | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('button:has-text("OK")').click() | ||||
|     ]); | ||||
| } | ||||
|  | ||||
| test.describe('Grand Search', () => { | ||||
|     test('Can search for objects, and subsequent search dropdown behaves properly', async ({ page }) => { | ||||
|         await createClockAndDisplayLayout(page); | ||||
|  | ||||
|         // Click [aria-label="OpenMCT Search"] input[type="search"] | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); | ||||
|         // Fill [aria-label="OpenMCT Search"] input[type="search"] | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Cl'); | ||||
|         await expect(page.locator('[aria-label="Search Result"]')).toContainText('Clock'); | ||||
|         await percySnapshot(page, 'Searching for Clocks'); | ||||
|         // Click text=Elements >> nth=0 | ||||
|         await page.locator('text=Elements').first().click(); | ||||
|         await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible(); | ||||
|  | ||||
|         // Click [aria-label="OpenMCT Search"] [aria-label="Search Input"] | ||||
|         await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click(); | ||||
|         // Click [aria-label="Unnamed Clock clock result"] >> text=Unnamed Clock | ||||
|         await page.locator('[aria-label="Unnamed Clock clock result"] >> text=Unnamed Clock').click(); | ||||
|         await percySnapshot(page, 'Preview for clock should display when editing enabled and search item clicked'); | ||||
|  | ||||
|         // Click [aria-label="Close"] | ||||
|         await page.locator('[aria-label="Close"]').click(); | ||||
|         await percySnapshot(page, 'Search should still be showing after preview closed'); | ||||
|  | ||||
|         // Click text=Snapshot Save and Finish Editing Save and Continue Editing >> button >> nth=1 | ||||
|         await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click(); | ||||
|         // Click text=Save and Finish Editing | ||||
|         await page.locator('text=Save and Finish Editing').click(); | ||||
|         // Click [aria-label="OpenMCT Search"] [aria-label="Search Input"] | ||||
|         await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click(); | ||||
|         // Fill [aria-label="OpenMCT Search"] [aria-label="Search Input"] | ||||
|         await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').fill('Cl'); | ||||
|         // Click text=Unnamed Clock | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('text=Unnamed Clock').click() | ||||
|         ]); | ||||
|         await percySnapshot(page, 'Clicking on search results should navigate to them if not editing'); | ||||
|  | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										33
									
								
								example/exampleTags/plugin.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								example/exampleTags/plugin.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
| import availableTags from './tags.json'; | ||||
| /** | ||||
|  * @returns {function} The plugin install function | ||||
|  */ | ||||
| export default function exampleTagsPlugin() { | ||||
|     return function install(openmct) { | ||||
|         Object.keys(availableTags.tags).forEach(tagKey => { | ||||
|             const tagDefinition = availableTags.tags[tagKey]; | ||||
|             openmct.annotation.defineTag(tagKey, tagDefinition); | ||||
|         }); | ||||
|     }; | ||||
| } | ||||
							
								
								
									
										19
									
								
								example/exampleTags/tags.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								example/exampleTags/tags.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| { | ||||
|     "tags": { | ||||
|         "46a62ad1-bb86-4f88-9a17-2a029e12669d": { | ||||
|             "label": "Science", | ||||
|             "backgroundColor": "#cc0000", | ||||
|             "foregroundColor": "#ffffff" | ||||
|         }, | ||||
|         "65f150ef-73b7-409a-b2e8-258cbd8b7323": { | ||||
|             "label": "Driving", | ||||
|             "backgroundColor": "#ffad32", | ||||
|             "foregroundColor": "#333333" | ||||
|         }, | ||||
|         "f156b038-c605-46db-88a6-67cf2489a371": { | ||||
|             "label": "Drilling", | ||||
|             "backgroundColor": "#b0ac4e", | ||||
|             "foregroundColor": "#FFFFFF" | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -21,19 +21,56 @@ | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import EventEmitter from 'EventEmitter'; | ||||
| import uuid from 'uuid'; | ||||
| import { v4 as uuid } from 'uuid'; | ||||
| import createExampleUser from './exampleUserCreator'; | ||||
|  | ||||
| const STATUSES = [{ | ||||
|     key: "NO_STATUS", | ||||
|     label: "Not set", | ||||
|     iconClass: "icon-question-mark", | ||||
|     iconClassPoll: "icon-status-poll-question-mark" | ||||
| }, { | ||||
|     key: "GO", | ||||
|     label: "Go", | ||||
|     iconClass: "icon-check", | ||||
|     iconClassPoll: "icon-status-poll-question-mark", | ||||
|     statusClass: "s-status-ok", | ||||
|     statusBgColor: "#33cc33", | ||||
|     statusFgColor: "#000" | ||||
| }, { | ||||
|     key: "MAYBE", | ||||
|     label: "Maybe", | ||||
|     iconClass: "icon-alert-triangle", | ||||
|     iconClassPoll: "icon-status-poll-question-mark", | ||||
|     statusClass: "s-status-warning", | ||||
|     statusBgColor: "#ffb66c", | ||||
|     statusFgColor: "#000" | ||||
| }, { | ||||
|     key: "NO_GO", | ||||
|     label: "No go", | ||||
|     iconClass: "icon-circle-slash", | ||||
|     iconClassPoll: "icon-status-poll-question-mark", | ||||
|     statusClass: "s-status-error", | ||||
|     statusBgColor: "#9900cc", | ||||
|     statusFgColor: "#fff" | ||||
| }]; | ||||
| /** | ||||
|  * @implements {StatusUserProvider} | ||||
|  */ | ||||
| export default class ExampleUserProvider extends EventEmitter { | ||||
|     constructor(openmct) { | ||||
|     constructor(openmct, {defaultStatusRole} = {defaultStatusRole: undefined}) { | ||||
|         super(); | ||||
|  | ||||
|         this.openmct = openmct; | ||||
|         this.user = undefined; | ||||
|         this.loggedIn = false; | ||||
|         this.autoLoginUser = undefined; | ||||
|         this.status = STATUSES[1]; | ||||
|         this.pollQuestion = undefined; | ||||
|         this.defaultStatusRole = defaultStatusRole; | ||||
|  | ||||
|         this.ExampleUser = createExampleUser(this.openmct.user.User); | ||||
|         this.loginPromise = undefined; | ||||
|     } | ||||
|  | ||||
|     isLoggedIn() { | ||||
| @@ -45,11 +82,19 @@ export default class ExampleUserProvider extends EventEmitter { | ||||
|     } | ||||
|  | ||||
|     getCurrentUser() { | ||||
|         if (this.loggedIn) { | ||||
|             return Promise.resolve(this.user); | ||||
|         if (!this.loginPromise) { | ||||
|             this.loginPromise = this._login().then(() => this.user); | ||||
|         } | ||||
|  | ||||
|         return this._login().then(() => this.user); | ||||
|         return this.loginPromise; | ||||
|     } | ||||
|  | ||||
|     canProvideStatusForRole() { | ||||
|         return Promise.resolve(true); | ||||
|     } | ||||
|  | ||||
|     canSetPollQuestion() { | ||||
|         return Promise.resolve(true); | ||||
|     } | ||||
|  | ||||
|     hasRole(roleId) { | ||||
| @@ -60,6 +105,55 @@ export default class ExampleUserProvider extends EventEmitter { | ||||
|         return Promise.resolve(this.user.getRoles().includes(roleId)); | ||||
|     } | ||||
|  | ||||
|     getStatusRoleForCurrentUser() { | ||||
|         return Promise.resolve(this.defaultStatusRole); | ||||
|     } | ||||
|  | ||||
|     getAllStatusRoles() { | ||||
|         return Promise.resolve([this.defaultStatusRole]); | ||||
|     } | ||||
|  | ||||
|     getStatusForRole(role) { | ||||
|         return Promise.resolve(this.status); | ||||
|     } | ||||
|  | ||||
|     async getDefaultStatusForRole(role) { | ||||
|         const allRoles = await this.getPossibleStatuses(); | ||||
|  | ||||
|         return allRoles?.[0]; | ||||
|     } | ||||
|  | ||||
|     setStatusForRole(role, status) { | ||||
|         this.status = status; | ||||
|         this.emit('statusChange', { | ||||
|             role, | ||||
|             status | ||||
|         }); | ||||
|  | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     getPollQuestion() { | ||||
|         return Promise.resolve({ | ||||
|             question: 'Set "GO" if your position is ready for a boarding action on the Klingon cruiser', | ||||
|             timestamp: Date.now() | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     setPollQuestion(pollQuestion) { | ||||
|         this.pollQuestion = { | ||||
|             question: pollQuestion, | ||||
|             timestamp: Date.now() | ||||
|         }; | ||||
|         this.emit("pollQuestionChange", this.pollQuestion); | ||||
|  | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     getPossibleStatuses() { | ||||
|         return Promise.resolve(STATUSES); | ||||
|     } | ||||
|  | ||||
|     _login() { | ||||
|         const id = uuid(); | ||||
|  | ||||
| @@ -108,3 +202,6 @@ export default class ExampleUserProvider extends EventEmitter { | ||||
|         ); | ||||
|     } | ||||
| } | ||||
| /** | ||||
|  * @typedef {import('@/api/user/StatusUserProvider').default} StatusUserProvider | ||||
|  */ | ||||
|   | ||||
| @@ -22,8 +22,19 @@ | ||||
|  | ||||
| import ExampleUserProvider from './ExampleUserProvider'; | ||||
|  | ||||
| export default function ExampleUserPlugin() { | ||||
| export default function ExampleUserPlugin({autoLoginUser, defaultStatusRole} = { | ||||
|     autoLoginUser: 'guest', | ||||
|     defaultStatusRole: 'test-role' | ||||
| }) { | ||||
|     return function install(openmct) { | ||||
|         openmct.user.setProvider(new ExampleUserProvider(openmct)); | ||||
|         const userProvider = new ExampleUserProvider(openmct, { | ||||
|             defaultStatusRole | ||||
|         }); | ||||
|  | ||||
|         if (autoLoginUser !== undefined) { | ||||
|             userProvider.autoLogin(autoLoginUser); | ||||
|         } | ||||
|  | ||||
|         openmct.user.setProvider(userProvider); | ||||
|     }; | ||||
| } | ||||
|   | ||||
| @@ -26,7 +26,7 @@ import { | ||||
| } from '../../src/utils/testing'; | ||||
| import ExampleUserProvider from './ExampleUserProvider'; | ||||
|  | ||||
| xdescribe("The Example User Plugin", () => { | ||||
| describe("The Example User Plugin", () => { | ||||
|     let openmct; | ||||
|  | ||||
|     beforeEach(() => { | ||||
| @@ -47,9 +47,4 @@ xdescribe("The Example User Plugin", () => { | ||||
|         }); | ||||
|         openmct.install(openmct.plugins.example.ExampleUser()); | ||||
|     }); | ||||
|  | ||||
|     // The rest of the functionality of the ExampleUser Plugin is | ||||
|     // tested in both the UserAPISpec.js and in the UserIndicatorPlugin spec. | ||||
|     // If that changes, those tests can be moved here. | ||||
|  | ||||
| }); | ||||
|   | ||||
							
								
								
									
										83
									
								
								example/faultManagment/exampleFaultSource.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								example/faultManagment/exampleFaultSource.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,83 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| export default function () { | ||||
|     return function install(openmct) { | ||||
|         openmct.install(openmct.plugins.FaultManagement()); | ||||
|  | ||||
|         openmct.faults.addProvider({ | ||||
|             request(domainObject, options) { | ||||
|                 const faults = JSON.parse(localStorage.getItem('faults')); | ||||
|  | ||||
|                 return Promise.resolve(faults.alarms); | ||||
|             }, | ||||
|             subscribe(domainObject, callback) { | ||||
|                 const faultsData = JSON.parse(localStorage.getItem('faults')).alarms; | ||||
|  | ||||
|                 function getRandomIndex(start, end) { | ||||
|                     return Math.floor(start + (Math.random() * (end - start + 1))); | ||||
|                 } | ||||
|  | ||||
|                 let id = setInterval(() => { | ||||
|                     const index = getRandomIndex(0, faultsData.length - 1); | ||||
|                     const randomFaultData = faultsData[index]; | ||||
|                     const randomFault = randomFaultData.fault; | ||||
|                     randomFault.currentValueInfo.value = Math.random(); | ||||
|                     callback({ | ||||
|                         fault: randomFault, | ||||
|                         type: 'alarms' | ||||
|                     }); | ||||
|                 }, 300); | ||||
|  | ||||
|                 return () => { | ||||
|                     clearInterval(id); | ||||
|                 }; | ||||
|             }, | ||||
|             supportsRequest(domainObject) { | ||||
|                 const faults = localStorage.getItem('faults'); | ||||
|  | ||||
|                 return faults && domainObject.type === 'faultManagement'; | ||||
|             }, | ||||
|             supportsSubscribe(domainObject) { | ||||
|                 const faults = localStorage.getItem('faults'); | ||||
|  | ||||
|                 return faults && domainObject.type === 'faultManagement'; | ||||
|             }, | ||||
|             acknowledgeFault(fault, { comment = '' }) { | ||||
|                 console.log('acknowledgeFault', fault); | ||||
|                 console.log('comment', comment); | ||||
|  | ||||
|                 return Promise.resolve({ | ||||
|                     success: true | ||||
|                 }); | ||||
|             }, | ||||
|             shelveFault(fault, shelveData) { | ||||
|                 console.log('shelveFault', fault); | ||||
|                 console.log('shelveData', shelveData); | ||||
|  | ||||
|                 return Promise.resolve({ | ||||
|                     success: true | ||||
|                 }); | ||||
|             } | ||||
|         }); | ||||
|     }; | ||||
| } | ||||
							
								
								
									
										47
									
								
								example/faultManagment/pluginSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								example/faultManagment/pluginSpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import { | ||||
|     createOpenMct, | ||||
|     resetApplicationState | ||||
| } from '../../src/utils/testing'; | ||||
|  | ||||
| describe("The Example Fault Source Plugin", () => { | ||||
|     let openmct; | ||||
|  | ||||
|     beforeEach(() => { | ||||
|         openmct = createOpenMct(); | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     it('is not installed by default', () => { | ||||
|         expect(openmct.faults.provider).toBeUndefined(); | ||||
|     }); | ||||
|  | ||||
|     it('can be installed', () => { | ||||
|         openmct.install(openmct.plugins.example.ExampleFaultSource()); | ||||
|         expect(openmct.faults.provider).not.toBeUndefined(); | ||||
|     }); | ||||
| }); | ||||
| @@ -29,12 +29,12 @@ define([ | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     key: "cos", | ||||
|                     name: "Cosine", | ||||
|                     unit: "deg", | ||||
|                     formatString: '%0.2f', | ||||
|                     key: "wavelengths", | ||||
|                     name: "Wavelength", | ||||
|                     unit: "nm", | ||||
|                     format: 'string[]', | ||||
|                     hints: { | ||||
|                         domain: 3 | ||||
|                         range: 4 | ||||
|                     } | ||||
|                 }, | ||||
|                 // Need to enable "LocalTimeSystem" plugin to make use of this | ||||
| @@ -64,6 +64,14 @@ define([ | ||||
|                     hints: { | ||||
|                         range: 2 | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     key: "intensities", | ||||
|                     name: "Intensities", | ||||
|                     format: 'number[]', | ||||
|                     hints: { | ||||
|                         range: 3 | ||||
|                     } | ||||
|                 } | ||||
|             ] | ||||
|         }, | ||||
|   | ||||
| @@ -32,11 +32,12 @@ define([ | ||||
|         offset: 0, | ||||
|         dataRateInHz: 1, | ||||
|         randomness: 0, | ||||
|         phase: 0 | ||||
|         phase: 0, | ||||
|         loadDelay: 0 | ||||
|     }; | ||||
|  | ||||
|     function GeneratorProvider() { | ||||
|         this.workerInterface = new WorkerInterface(); | ||||
|     function GeneratorProvider(openmct) { | ||||
|         this.workerInterface = new WorkerInterface(openmct); | ||||
|     } | ||||
|  | ||||
|     GeneratorProvider.prototype.canProvideTelemetry = function (domainObject) { | ||||
| @@ -53,8 +54,9 @@ define([ | ||||
|             'period', | ||||
|             'offset', | ||||
|             'dataRateInHz', | ||||
|             'randomness', | ||||
|             'phase', | ||||
|             'randomness' | ||||
|             'loadDelay' | ||||
|         ]; | ||||
|  | ||||
|         request = request || {}; | ||||
|   | ||||
| @@ -21,20 +21,13 @@ | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define([ | ||||
|     'raw-loader!./generatorWorker.js', | ||||
|     'uuid' | ||||
| ], function ( | ||||
|     workerText, | ||||
|     uuid | ||||
|     { v4: uuid } | ||||
| ) { | ||||
|  | ||||
|     var workerBlob = new Blob( | ||||
|         [workerText], | ||||
|         {type: 'application/javascript'} | ||||
|     ); | ||||
|     var workerUrl = URL.createObjectURL(workerBlob); | ||||
|  | ||||
|     function WorkerInterface() { | ||||
|     function WorkerInterface(openmct) { | ||||
|         // eslint-disable-next-line no-undef | ||||
|         const workerUrl = `${openmct.getAssetPath()}${__OPENMCT_ROOT_RELATIVE__}generatorWorker.js`; | ||||
|         this.worker = new Worker(workerUrl); | ||||
|         this.worker.onmessage = this.onMessage.bind(this); | ||||
|         this.callbacks = {}; | ||||
|   | ||||
| @@ -77,7 +77,8 @@ | ||||
|                             utc: nextStep, | ||||
|                             yesterday: nextStep - 60 * 60 * 24 * 1000, | ||||
|                             sin: sin(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness), | ||||
|                             wavelength: wavelength(start, nextStep), | ||||
|                             wavelengths: wavelengths(), | ||||
|                             intensities: intensities(), | ||||
|                             cos: cos(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness) | ||||
|                         } | ||||
|                     }); | ||||
| @@ -115,6 +116,7 @@ | ||||
|         var dataRateInHz = request.dataRateInHz; | ||||
|         var phase = request.phase; | ||||
|         var randomness = request.randomness; | ||||
|         var loadDelay = Math.max(request.loadDelay, 0); | ||||
|  | ||||
|         var step = 1000 / dataRateInHz; | ||||
|         var nextStep = start - (start % step) + step; | ||||
| @@ -126,11 +128,20 @@ | ||||
|                 utc: nextStep, | ||||
|                 yesterday: nextStep - 60 * 60 * 24 * 1000, | ||||
|                 sin: sin(nextStep, period, amplitude, offset, phase, randomness), | ||||
|                 wavelength: wavelength(start, nextStep), | ||||
|                 wavelengths: wavelengths(), | ||||
|                 intensities: intensities(), | ||||
|                 cos: cos(nextStep, period, amplitude, offset, phase, randomness) | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         if (loadDelay === 0) { | ||||
|             postOnRequest(message, request, data); | ||||
|         } else { | ||||
|             setTimeout(() => postOnRequest(message, request, data), loadDelay); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function postOnRequest(message, request, data) { | ||||
|         self.postMessage({ | ||||
|             id: message.id, | ||||
|             data: request.spectra ? { | ||||
| @@ -154,8 +165,28 @@ | ||||
|             * Math.sin(phase + (timestamp / period / 1000 * Math.PI * 2)) + (amplitude * Math.random() * randomness) + offset; | ||||
|     } | ||||
|  | ||||
|     function wavelength(start, nextStep) { | ||||
|         return (nextStep - start) / 10; | ||||
|     function wavelengths() { | ||||
|         let values = []; | ||||
|         while (values.length < 5) { | ||||
|             const randomValue = Math.random() * 100; | ||||
|             if (!values.includes(randomValue)) { | ||||
|                 values.push(String(randomValue)); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return values; | ||||
|     } | ||||
|  | ||||
|     function intensities() { | ||||
|         let values = []; | ||||
|         while (values.length < 5) { | ||||
|             const randomValue = Math.random() * 10; | ||||
|             if (!values.includes(randomValue)) { | ||||
|                 values.push(String(randomValue)); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return values; | ||||
|     } | ||||
|  | ||||
|     function sendError(error, message) { | ||||
|   | ||||
| @@ -81,7 +81,7 @@ define([ | ||||
|                 { | ||||
|                     name: "Amplitude", | ||||
|                     control: "numberfield", | ||||
|                     cssClass: "l-input-sm l-numeric", | ||||
|                     cssClass: "l-numeric", | ||||
|                     key: "amplitude", | ||||
|                     required: true, | ||||
|                     property: [ | ||||
| @@ -92,7 +92,7 @@ define([ | ||||
|                 { | ||||
|                     name: "Offset", | ||||
|                     control: "numberfield", | ||||
|                     cssClass: "l-input-sm l-numeric", | ||||
|                     cssClass: "l-numeric", | ||||
|                     key: "offset", | ||||
|                     required: true, | ||||
|                     property: [ | ||||
| @@ -132,6 +132,17 @@ define([ | ||||
|                         "telemetry", | ||||
|                         "randomness" | ||||
|                     ] | ||||
|                 }, | ||||
|                 { | ||||
|                     name: "Loading Delay (ms)", | ||||
|                     control: "numberfield", | ||||
|                     cssClass: "l-input-sm l-numeric", | ||||
|                     key: "loadDelay", | ||||
|                     required: true, | ||||
|                     property: [ | ||||
|                         "telemetry", | ||||
|                         "loadDelay" | ||||
|                     ] | ||||
|                 } | ||||
|             ], | ||||
|             initialize: function (object) { | ||||
| @@ -141,12 +152,13 @@ define([ | ||||
|                     offset: 0, | ||||
|                     dataRateInHz: 1, | ||||
|                     phase: 0, | ||||
|                     randomness: 0 | ||||
|                     randomness: 0, | ||||
|                     loadDelay: 0 | ||||
|                 }; | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         openmct.telemetry.addProvider(new GeneratorProvider()); | ||||
|         openmct.telemetry.addProvider(new GeneratorProvider(openmct)); | ||||
|         openmct.telemetry.addProvider(new GeneratorMetadataProvider()); | ||||
|         openmct.telemetry.addProvider(new SinewaveLimitProvider()); | ||||
|     }; | ||||
|   | ||||
| @@ -59,7 +59,8 @@ export default function () { | ||||
|                 object.configuration = { | ||||
|                     imageLocation: '', | ||||
|                     imageLoadDelayInMilliSeconds: DEFAULT_IMAGE_LOAD_DELAY_IN_MILISECONDS, | ||||
|                     imageSamples: [] | ||||
|                     imageSamples: [], | ||||
|                     layers: [] | ||||
|                 }; | ||||
|  | ||||
|                 object.telemetry = { | ||||
| @@ -90,7 +91,21 @@ export default function () { | ||||
|                             format: 'image', | ||||
|                             hints: { | ||||
|                                 image: 1 | ||||
|                             } | ||||
|                             }, | ||||
|                             layers: [ | ||||
|                                 { | ||||
|                                     source: 'dist/imagery/example-imagery-layer-16x9.png', | ||||
|                                     name: '16:9' | ||||
|                                 }, | ||||
|                                 { | ||||
|                                     source: 'dist/imagery/example-imagery-layer-safe.png', | ||||
|                                     name: 'Safe' | ||||
|                                 }, | ||||
|                                 { | ||||
|                                     source: 'dist/imagery/example-imagery-layer-scale.png', | ||||
|                                     name: 'Scale' | ||||
|                                 } | ||||
|                             ] | ||||
|                         }, | ||||
|                         { | ||||
|                             name: 'Image Download Name', | ||||
| @@ -153,7 +168,7 @@ function getImageUrlListFromConfig(configuration) { | ||||
| } | ||||
|  | ||||
| function getImageLoadDelay(domainObject) { | ||||
|     const imageLoadDelay = domainObject.configuration.imageLoadDelayInMilliSeconds; | ||||
|     const imageLoadDelay = Math.trunc(Number(domainObject.configuration.imageLoadDelayInMilliSeconds)); | ||||
|     if (!imageLoadDelay) { | ||||
|         openmctInstance.objects.mutate(domainObject, 'configuration.imageLoadDelayInMilliSeconds', DEFAULT_IMAGE_LOAD_DELAY_IN_MILISECONDS); | ||||
|  | ||||
| @@ -175,7 +190,9 @@ function getRealtimeProvider() { | ||||
|         subscribe: (domainObject, callback) => { | ||||
|             const delay = getImageLoadDelay(domainObject); | ||||
|             const interval = setInterval(() => { | ||||
|                 callback(pointForTimestamp(Date.now(), domainObject.name, getImageSamples(domainObject.configuration), delay)); | ||||
|                 const imageSamples = getImageSamples(domainObject.configuration); | ||||
|                 const datum = pointForTimestamp(Date.now(), domainObject.name, imageSamples, delay); | ||||
|                 callback(datum); | ||||
|             }, delay); | ||||
|  | ||||
|             return () => { | ||||
| @@ -214,8 +231,9 @@ function getLadProvider() { | ||||
|         }, | ||||
|         request: (domainObject, options) => { | ||||
|             const delay = getImageLoadDelay(domainObject); | ||||
|             const datum = pointForTimestamp(Date.now(), domainObject.name, getImageSamples(domainObject.configuration), delay); | ||||
|  | ||||
|             return Promise.resolve([pointForTimestamp(Date.now(), domainObject.name, delay)]); | ||||
|             return Promise.resolve([datum]); | ||||
|         } | ||||
|     }; | ||||
| } | ||||
|   | ||||
| @@ -75,12 +75,12 @@ | ||||
|         const TWO_HOURS = ONE_HOUR * 2; | ||||
|         const ONE_DAY = ONE_HOUR * 24; | ||||
|  | ||||
|  | ||||
|         openmct.install(openmct.plugins.LocalStorage()); | ||||
|        | ||||
|  | ||||
|         openmct.install(openmct.plugins.example.Generator()); | ||||
|         openmct.install(openmct.plugins.example.EventGeneratorPlugin()); | ||||
|         openmct.install(openmct.plugins.example.ExampleImagery()); | ||||
|         openmct.install(openmct.plugins.example.ExampleTags()); | ||||
|  | ||||
|         openmct.install(openmct.plugins.Espresso()); | ||||
|         openmct.install(openmct.plugins.MyItems()); | ||||
| @@ -191,10 +191,13 @@ | ||||
|         openmct.install(openmct.plugins.ObjectMigration()); | ||||
|         openmct.install(openmct.plugins.ClearData( | ||||
|             ['table', 'telemetry.plot.overlay', 'telemetry.plot.stacked', 'example.imagery'], | ||||
|             {indicator: true} | ||||
|             { indicator: true } | ||||
|         )); | ||||
|         openmct.install(openmct.plugins.Clock({ enableClockIndicator: true })); | ||||
|         openmct.install(openmct.plugins.Timer()); | ||||
|         openmct.install(openmct.plugins.Timelist()); | ||||
|         openmct.install(openmct.plugins.BarChart()); | ||||
|         openmct.install(openmct.plugins.ScatterPlot()); | ||||
|         openmct.start(); | ||||
|     </script> | ||||
| </html> | ||||
|   | ||||
| @@ -38,6 +38,10 @@ module.exports = (config) => { | ||||
|             { | ||||
|                 pattern: 'dist/inMemorySearchWorker.js*', | ||||
|                 included: false | ||||
|             }, | ||||
|             { | ||||
|                 pattern: 'dist/generatorWorker.js*', | ||||
|                 included: false | ||||
|             } | ||||
|         ], | ||||
|         port: 9876, | ||||
| @@ -70,13 +74,8 @@ module.exports = (config) => { | ||||
|         }, | ||||
|         coverageIstanbulReporter: { | ||||
|             fixWebpackSourcePaths: true, | ||||
|             dir: "dist/reports/coverage", | ||||
|             reports: ['lcovonly', 'text-summary'], | ||||
|             thresholds: { | ||||
|                 global: { | ||||
|                     lines: 52 | ||||
|                 } | ||||
|             } | ||||
|             dir: "coverage/unit", | ||||
|             reports: ['lcovonly'] | ||||
|         }, | ||||
|         specReporter: { | ||||
|             maxLogLines: 5, | ||||
| @@ -92,8 +91,7 @@ module.exports = (config) => { | ||||
|         }, | ||||
|         webpack: webpackConfig, | ||||
|         webpackMiddleware: { | ||||
|             stats: 'errors-only', | ||||
|             logLevel: 'warn' | ||||
|             stats: 'errors-warnings' | ||||
|         }, | ||||
|         concurrency: 1, | ||||
|         singleRun: true, | ||||
|   | ||||
							
								
								
									
										113
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										113
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,105 +1,113 @@ | ||||
| { | ||||
|   "name": "openmct", | ||||
|   "version": "2.0.2-SNAPSHOT", | ||||
|   "version": "2.1.0-SNAPSHOT", | ||||
|   "description": "The Open MCT core platform", | ||||
|   "devDependencies": { | ||||
|     "@babel/eslint-parser": "7.16.3", | ||||
|     "@babel/eslint-parser": "7.18.2", | ||||
|     "@braintree/sanitize-url": "6.0.0", | ||||
|     "@percy/cli": "1.0.0-beta.76", | ||||
|     "@percy/playwright": "1.0.1", | ||||
|     "@playwright/test": "1.19.2", | ||||
|     "allure-playwright": "2.0.0-beta.15", | ||||
|     "babel-loader": "8.2.3", | ||||
|     "@percy/cli": "1.2.1", | ||||
|     "@percy/playwright": "1.0.4", | ||||
|     "@playwright/test": "1.23.0", | ||||
|     "@types/eventemitter3": "^1.0.0", | ||||
|     "@types/jasmine": "^4.0.1", | ||||
|     "@types/karma": "^6.3.2", | ||||
|     "@types/lodash": "^4.14.178", | ||||
|     "@types/mocha": "^9.1.0", | ||||
|     "babel-loader": "8.2.5", | ||||
|     "babel-plugin-istanbul": "6.1.1", | ||||
|     "comma-separated-values": "3.6.4", | ||||
|     "copy-webpack-plugin": "10.2.0", | ||||
|     "core-js": "3.21.1", | ||||
|     "codecov":"3.8.3", | ||||
|     "copy-webpack-plugin": "11.0.0", | ||||
|     "cross-env": "7.0.3", | ||||
|     "css-loader": "4.0.0", | ||||
|     "d3-axis": "1.0.x", | ||||
|     "d3-scale": "1.0.x", | ||||
|     "d3-selection": "1.3.x", | ||||
|     "eslint": "8.11.0", | ||||
|     "d3-axis": "3.0.0", | ||||
|     "d3-scale": "3.3.0", | ||||
|     "d3-selection": "3.0.0", | ||||
|     "eslint": "8.18.0", | ||||
|     "eslint-plugin-compat": "4.0.2", | ||||
|     "eslint-plugin-playwright": "0.8.0", | ||||
|     "eslint-plugin-vue": "8.5.0", | ||||
|     "eslint-plugin-playwright": "0.9.0", | ||||
|     "eslint-plugin-vue": "9.1.1", | ||||
|     "eslint-plugin-you-dont-need-lodash-underscore": "6.12.0", | ||||
|     "eventemitter3": "1.2.0", | ||||
|     "exports-loader": "0.7.0", | ||||
|     "express": "4.13.1", | ||||
|     "file-loader": "6.2.0", | ||||
|     "file-saver": "2.0.5", | ||||
|     "git-rev-sync": "3.0.2", | ||||
|     "html-loader": "0.5.5", | ||||
|     "html2canvas": "1.4.1", | ||||
|     "imports-loader": "0.8.0", | ||||
|     "jasmine-core": "4.0.1", | ||||
|     "jasmine-core": "4.2.0", | ||||
|     "jsdoc": "3.5.5", | ||||
|     "karma": "6.3.15", | ||||
|     "karma": "6.3.20", | ||||
|     "karma-chrome-launcher": "3.1.1", | ||||
|     "karma-cli": "2.0.0", | ||||
|     "karma-coverage": "2.1.1", | ||||
|     "karma-coverage": "2.2.0", | ||||
|     "karma-coverage-istanbul-reporter": "3.0.3", | ||||
|     "karma-firefox-launcher": "2.1.2", | ||||
|     "karma-jasmine": "4.0.1", | ||||
|     "karma-jasmine": "5.1.0", | ||||
|     "karma-junit-reporter": "2.0.1", | ||||
|     "karma-sourcemap-loader": "0.3.8", | ||||
|     "karma-spec-reporter": "0.0.33", | ||||
|     "karma-spec-reporter": "0.0.34", | ||||
|     "karma-webpack": "5.0.0", | ||||
|     "lighthouse": "9.6.1", | ||||
|     "location-bar": "3.0.1", | ||||
|     "lodash": "4.17.12", | ||||
|     "mini-css-extract-plugin": "2.4.5", | ||||
|     "moment": "2.29.1", | ||||
|     "moment-duration-format": "2.2.2", | ||||
|     "moment-timezone": "0.5.28", | ||||
|     "lodash": "4.17.21", | ||||
|     "mini-css-extract-plugin": "2.6.1", | ||||
|     "moment": "2.29.4", | ||||
|     "moment-duration-format": "2.3.2", | ||||
|     "moment-timezone": "0.5.34", | ||||
|     "node-bourbon": "4.2.3", | ||||
|     "painterro": "1.2.56", | ||||
|     "plotly.js-basic-dist": "2.5.0", | ||||
|     "plotly.js-gl2d-dist": "2.5.0", | ||||
|     "nyc":"15.1.0", | ||||
|     "painterro": "1.2.78", | ||||
|     "plotly.js-basic-dist": "2.12.0", | ||||
|     "plotly.js-gl2d-dist": "2.12.0", | ||||
|     "printj": "1.3.1", | ||||
|     "raw-loader": "4.0.2", | ||||
|     "request": "2.88.2", | ||||
|     "resolve-url-loader": "4.0.0", | ||||
|     "sass": "1.49.0", | ||||
|     "sass-loader": "12.4.0", | ||||
|     "sinon": "13.0.1", | ||||
|     "resolve-url-loader": "5.0.0", | ||||
|     "sass": "1.52.2", | ||||
|     "sass-loader": "13.0.2", | ||||
|     "sinon": "14.0.0", | ||||
|     "style-loader": "^1.0.1", | ||||
|     "uuid": "3.3.3", | ||||
|     "uuid": "8.3.2", | ||||
|     "vue": "2.6.14", | ||||
|     "vue-eslint-parser": "8.2.0", | ||||
|     "vue-eslint-parser": "9.0.2", | ||||
|     "vue-loader": "15.9.8", | ||||
|     "vue-template-compiler": "2.6.14", | ||||
|     "webpack": "5.68.0", | ||||
|     "webpack-cli": "4.9.2", | ||||
|     "webpack-dev-middleware": "3.7.3", | ||||
|     "webpack-cli": "4.10.0", | ||||
|     "webpack-dev-middleware": "5.3.3", | ||||
|     "webpack-hot-middleware": "2.25.1", | ||||
|     "webpack-merge": "5.8.0", | ||||
|     "zepto": "1.2.0" | ||||
|     "webpack-merge": "5.8.0" | ||||
|   }, | ||||
|   "scripts": { | ||||
|     "clean": "rm -rf ./dist ./node_modules; rm package-lock.json", | ||||
|     "clean": "rm -rf ./dist ./node_modules ./package-lock.json", | ||||
|     "clean-test-lint": "npm run clean; npm install; npm run test; npm run lint", | ||||
|     "start": "node app.js", | ||||
|     "lint": "eslint example src --ext .js,.vue openmct.js", | ||||
|     "lint:fix": "eslint example src --ext .js,.vue openmct.js --fix", | ||||
|     "lint": "eslint example src e2e --ext .js,.vue openmct.js --max-warnings=0", | ||||
|     "lint:fix": "eslint example src e2e --ext .js,.vue openmct.js --fix", | ||||
|     "build:prod": "cross-env webpack --config webpack.prod.js", | ||||
|     "build:dev": "webpack --config webpack.dev.js", | ||||
|     "build:coverage": "webpack --config webpack.coverage.js", | ||||
|     "build:watch": "webpack --config webpack.dev.js --watch", | ||||
|     "info": "npx envinfo --system --browsers --npmPackages --binaries --languages --markdown", | ||||
|     "test": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run", | ||||
|     "test:firefox": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run --browsers=FirefoxHeadless", | ||||
|     "test": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run", | ||||
|     "test:firefox": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run --browsers=FirefoxHeadless", | ||||
|     "test:debug": "cross-env NODE_ENV=debug karma start --no-single-run", | ||||
|     "test:e2e:ci": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome smoke default condition timeConductor", | ||||
|     "test:e2e": "npx playwright test", | ||||
|     "test:e2e:ci": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome visual smoke branding default condition timeConductor clock persistence performance grandsearch tags", | ||||
|     "test:e2e:local": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome", | ||||
|     "test:e2e:visual": "percy exec --config ./e2e/.percy.yml -- npx playwright test --config=e2e/playwright-visual.config.js default", | ||||
|     "test:e2e:updatesnapshots": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @snapshot --update-snapshots", | ||||
|     "test:e2e:visual": "percy exec --config ./e2e/.percy.yml -- npx playwright test --config=e2e/playwright-visual.config.js", | ||||
|     "test:e2e:full": "npx playwright test --config=e2e/playwright-ci.config.js", | ||||
|     "test:watch": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --no-single-run", | ||||
|     "test:perf": "npx playwright test --config=e2e/playwright-performance.config.js", | ||||
|     "test:watch": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --no-single-run", | ||||
|     "jsdoc": "jsdoc -c jsdoc.json -R API.md -r -d dist/docs/api", | ||||
|     "update-about-dialog-copyright": "perl -pi -e 's/20\\d\\d\\-202\\d/2014\\-2022/gm' ./src/ui/layout/AboutDialog.vue", | ||||
|     "update-copyright-date": "npm run update-about-dialog-copyright && grep -lr --null --include=*.{js,scss,vue,ts,sh,html,md,frag} 'Copyright (c) 20' . | xargs -r0 perl -pi -e 's/Copyright\\s\\(c\\)\\s20\\d\\d\\-20\\d\\d/Copyright \\(c\\)\\ 2014\\-2022/gm'", | ||||
|     "otherdoc": "node docs/gendocs.js --in docs/src --out dist/docs --suppress-toc 'docs/src/index.md|docs/src/process/index.md'", | ||||
|     "docs": "npm run jsdoc ; npm run otherdoc", | ||||
|     "cov:e2e:report":"nyc report --reporter=lcovonly --report-dir=./coverage/e2e", | ||||
|     "cov:e2e:full:publish":"codecov --disable=gcov -f ./coverage/e2e/lcov.info -F e2e-full", | ||||
|     "cov:e2e:ci:publish":"codecov --disable=gcov -f ./coverage/e2e/lcov.info -F e2e-ci", | ||||
|     "cov:unit:publish":"codecov --disable=gcov -f ./coverage/unit/lcov.info -F unit", | ||||
|     "prepare": "npm run build:prod" | ||||
|   }, | ||||
|   "repository": { | ||||
| @@ -107,7 +115,10 @@ | ||||
|     "url": "https://github.com/nasa/openmct.git" | ||||
|   }, | ||||
|   "engines": { | ||||
|     "node": ">=12.22.0" | ||||
|     "node": ">=14.19.1" | ||||
|   }, | ||||
|   "overrides": { | ||||
|     "core-js": "3.21.1" | ||||
|   }, | ||||
|   "browserslist": [ | ||||
|     "Firefox ESR", | ||||
|   | ||||
							
								
								
									
										270
									
								
								src/MCT.js
									
									
									
									
									
								
							
							
						
						
									
										270
									
								
								src/MCT.js
									
									
									
									
									
								
							| @@ -42,6 +42,7 @@ define([ | ||||
|     './plugins/duplicate/plugin', | ||||
|     './plugins/importFromJSONAction/plugin', | ||||
|     './plugins/exportAsJSONAction/plugin', | ||||
|     './ui/components/components', | ||||
|     'vue' | ||||
| ], function ( | ||||
|     EventEmitter, | ||||
| @@ -65,6 +66,7 @@ define([ | ||||
|     DuplicateActionPlugin, | ||||
|     ImportFromJSONAction, | ||||
|     ExportAsJSONAction, | ||||
|     components, | ||||
|     Vue | ||||
| ) { | ||||
|     /** | ||||
| @@ -94,156 +96,170 @@ define([ | ||||
|         }; | ||||
|  | ||||
|         this.destroy = this.destroy.bind(this); | ||||
|         /** | ||||
|          * Tracks current selection state of the application. | ||||
|          * @private | ||||
|          */ | ||||
|         this.selection = new Selection(this); | ||||
|         [ | ||||
|             /** | ||||
|             * Tracks current selection state of the application. | ||||
|             * @private | ||||
|             */ | ||||
|             ['selection', () => new Selection(this)], | ||||
|  | ||||
|         /** | ||||
|          * MCT's time conductor, which may be used to synchronize view contents | ||||
|          * for telemetry- or time-based views. | ||||
|          * @type {module:openmct.TimeConductor} | ||||
|          * @memberof module:openmct.MCT# | ||||
|          * @name conductor | ||||
|          */ | ||||
|         this.time = new api.TimeAPI(this); | ||||
|             /** | ||||
|              * MCT's time conductor, which may be used to synchronize view contents | ||||
|              * for telemetry- or time-based views. | ||||
|              * @type {module:openmct.TimeConductor} | ||||
|              * @memberof module:openmct.MCT# | ||||
|              * @name conductor | ||||
|              */ | ||||
|             ['time', () => new api.TimeAPI(this)], | ||||
|  | ||||
|         /** | ||||
|          * An interface for interacting with the composition of domain objects. | ||||
|          * The composition of a domain object is the list of other domain | ||||
|          * objects it "contains" (for instance, that should be displayed | ||||
|          * beneath it in the tree.) | ||||
|          * | ||||
|          * `composition` may be called as a function, in which case it acts | ||||
|          * as [`composition.get`]{@link module:openmct.CompositionAPI#get}. | ||||
|          * | ||||
|          * @type {module:openmct.CompositionAPI} | ||||
|          * @memberof module:openmct.MCT# | ||||
|          * @name composition | ||||
|          */ | ||||
|         this.composition = new api.CompositionAPI(this); | ||||
|             /** | ||||
|              * An interface for interacting with the composition of domain objects. | ||||
|              * The composition of a domain object is the list of other domain | ||||
|              * objects it "contains" (for instance, that should be displayed | ||||
|              * beneath it in the tree.) | ||||
|              * | ||||
|              * `composition` may be called as a function, in which case it acts | ||||
|              * as [`composition.get`]{@link module:openmct.CompositionAPI#get}. | ||||
|              * | ||||
|              * @type {module:openmct.CompositionAPI} | ||||
|              * @memberof module:openmct.MCT# | ||||
|              * @name composition | ||||
|              */ | ||||
|             ['composition', () => new api.CompositionAPI(this)], | ||||
|  | ||||
|         /** | ||||
|          * Registry for views of domain objects which should appear in the | ||||
|          * main viewing area. | ||||
|          * | ||||
|          * @type {module:openmct.ViewRegistry} | ||||
|          * @memberof module:openmct.MCT# | ||||
|          * @name objectViews | ||||
|          */ | ||||
|         this.objectViews = new ViewRegistry(); | ||||
|             /** | ||||
|              * Registry for views of domain objects which should appear in the | ||||
|              * main viewing area. | ||||
|              * | ||||
|              * @type {module:openmct.ViewRegistry} | ||||
|              * @memberof module:openmct.MCT# | ||||
|              * @name objectViews | ||||
|              */ | ||||
|             ['objectViews', () => new ViewRegistry()], | ||||
|  | ||||
|         /** | ||||
|          * Registry for views which should appear in the Inspector area. | ||||
|          * These views will be chosen based on the selection state. | ||||
|          * | ||||
|          * @type {module:openmct.InspectorViewRegistry} | ||||
|          * @memberof module:openmct.MCT# | ||||
|          * @name inspectorViews | ||||
|          */ | ||||
|         this.inspectorViews = new InspectorViewRegistry(); | ||||
|             /** | ||||
|              * Registry for views which should appear in the Inspector area. | ||||
|              * These views will be chosen based on the selection state. | ||||
|              * | ||||
|              * @type {module:openmct.InspectorViewRegistry} | ||||
|              * @memberof module:openmct.MCT# | ||||
|              * @name inspectorViews | ||||
|              */ | ||||
|             ['inspectorViews', () => new InspectorViewRegistry()], | ||||
|  | ||||
|         /** | ||||
|          * Registry for views which should appear in Edit Properties | ||||
|          * dialogs, and similar user interface elements used for | ||||
|          * modifying domain objects external to its regular views. | ||||
|          * | ||||
|          * @type {module:openmct.ViewRegistry} | ||||
|          * @memberof module:openmct.MCT# | ||||
|          * @name propertyEditors | ||||
|          */ | ||||
|         this.propertyEditors = new ViewRegistry(); | ||||
|             /** | ||||
|              * Registry for views which should appear in Edit Properties | ||||
|              * dialogs, and similar user interface elements used for | ||||
|              * modifying domain objects external to its regular views. | ||||
|              * | ||||
|              * @type {module:openmct.ViewRegistry} | ||||
|              * @memberof module:openmct.MCT# | ||||
|              * @name propertyEditors | ||||
|              */ | ||||
|             ['propertyEditors', () => new ViewRegistry()], | ||||
|  | ||||
|         /** | ||||
|          * Registry for views which should appear in the status indicator area. | ||||
|          * @type {module:openmct.ViewRegistry} | ||||
|          * @memberof module:openmct.MCT# | ||||
|          * @name indicators | ||||
|          */ | ||||
|         this.indicators = new ViewRegistry(); | ||||
|             /** | ||||
|              * Registry for views which should appear in the toolbar area while | ||||
|              * editing. These views will be chosen based on the selection state. | ||||
|              * | ||||
|              * @type {module:openmct.ToolbarRegistry} | ||||
|              * @memberof module:openmct.MCT# | ||||
|              * @name toolbars | ||||
|              */ | ||||
|             ['toolbars', () => new ToolbarRegistry()], | ||||
|  | ||||
|         /** | ||||
|          * Registry for views which should appear in the toolbar area while | ||||
|          * editing. These views will be chosen based on the selection state. | ||||
|          * | ||||
|          * @type {module:openmct.ToolbarRegistry} | ||||
|          * @memberof module:openmct.MCT# | ||||
|          * @name toolbars | ||||
|          */ | ||||
|         this.toolbars = new ToolbarRegistry(); | ||||
|             /** | ||||
|              * Registry for domain object types which may exist within this | ||||
|              * instance of Open MCT. | ||||
|              * | ||||
|              * @type {module:openmct.TypeRegistry} | ||||
|              * @memberof module:openmct.MCT# | ||||
|              * @name types | ||||
|              */ | ||||
|             ['types', () => new api.TypeRegistry()], | ||||
|  | ||||
|         /** | ||||
|          * Registry for domain object types which may exist within this | ||||
|          * instance of Open MCT. | ||||
|          * | ||||
|          * @type {module:openmct.TypeRegistry} | ||||
|          * @memberof module:openmct.MCT# | ||||
|          * @name types | ||||
|          */ | ||||
|         this.types = new api.TypeRegistry(); | ||||
|             /** | ||||
|              * An interface for interacting with domain objects and the domain | ||||
|              * object hierarchy. | ||||
|              * | ||||
|              * @type {module:openmct.ObjectAPI} | ||||
|              * @memberof module:openmct.MCT# | ||||
|              * @name objects | ||||
|              */ | ||||
|             ['objects', () => new api.ObjectAPI.default(this.types, this)], | ||||
|  | ||||
|         /** | ||||
|          * An interface for interacting with domain objects and the domain | ||||
|          * object hierarchy. | ||||
|          * | ||||
|          * @type {module:openmct.ObjectAPI} | ||||
|          * @memberof module:openmct.MCT# | ||||
|          * @name objects | ||||
|          */ | ||||
|         this.objects = new api.ObjectAPI.default(this.types, this); | ||||
|             /** | ||||
|              * An interface for retrieving and interpreting telemetry data associated | ||||
|              * with a domain object. | ||||
|              * | ||||
|              * @type {module:openmct.TelemetryAPI} | ||||
|              * @memberof module:openmct.MCT# | ||||
|              * @name telemetry | ||||
|              */ | ||||
|             ['telemetry', () => new api.TelemetryAPI.default(this)], | ||||
|  | ||||
|         /** | ||||
|          * An interface for retrieving and interpreting telemetry data associated | ||||
|          * with a domain object. | ||||
|          * | ||||
|          * @type {module:openmct.TelemetryAPI} | ||||
|          * @memberof module:openmct.MCT# | ||||
|          * @name telemetry | ||||
|          */ | ||||
|         this.telemetry = new api.TelemetryAPI(this); | ||||
|             /** | ||||
|              * An interface for creating new indicators and changing them dynamically. | ||||
|              * | ||||
|              * @type {module:openmct.IndicatorAPI} | ||||
|              * @memberof module:openmct.MCT# | ||||
|              * @name indicators | ||||
|              */ | ||||
|             ['indicators', () => new api.IndicatorAPI(this)], | ||||
|  | ||||
|         /** | ||||
|          * An interface for creating new indicators and changing them dynamically. | ||||
|          * | ||||
|          * @type {module:openmct.IndicatorAPI} | ||||
|          * @memberof module:openmct.MCT# | ||||
|          * @name indicators | ||||
|          */ | ||||
|         this.indicators = new api.IndicatorAPI(this); | ||||
|             /** | ||||
|              * MCT's user awareness management, to enable user and | ||||
|              * role specific functionality. | ||||
|              * @type {module:openmct.UserAPI} | ||||
|              * @memberof module:openmct.MCT# | ||||
|              * @name user | ||||
|              */ | ||||
|             ['user', () => new api.UserAPI(this)], | ||||
|  | ||||
|         /** | ||||
|          * MCT's user awareness management, to enable user and | ||||
|          * role specific functionality. | ||||
|          * @type {module:openmct.UserAPI} | ||||
|          * @memberof module:openmct.MCT# | ||||
|          * @name user | ||||
|          */ | ||||
|         this.user = new api.UserAPI(this); | ||||
|             ['notifications', () => new api.NotificationAPI()], | ||||
|  | ||||
|         this.notifications = new api.NotificationAPI(); | ||||
|             ['editor', () => new api.EditorAPI.default(this)], | ||||
|  | ||||
|         this.editor = new api.EditorAPI.default(this); | ||||
|             ['overlays', () => new OverlayAPI.default()], | ||||
|  | ||||
|         this.overlays = new OverlayAPI.default(); | ||||
|             ['menus', () => new api.MenuAPI(this)], | ||||
|  | ||||
|         this.menus = new api.MenuAPI(this); | ||||
|             ['actions', () => new api.ActionsAPI(this)], | ||||
|  | ||||
|         this.actions = new api.ActionsAPI(this); | ||||
|             ['status', () => new api.StatusAPI(this)], | ||||
|  | ||||
|         this.status = new api.StatusAPI(this); | ||||
|             ['priority', () => api.PriorityAPI], | ||||
|  | ||||
|         this.priority = api.PriorityAPI; | ||||
|             ['router', () => new ApplicationRouter(this)], | ||||
|  | ||||
|         this.router = new ApplicationRouter(this); | ||||
|         this.forms = new api.FormsAPI.default(this); | ||||
|             ['faults', () => new api.FaultManagementAPI.default(this)], | ||||
|  | ||||
|         this.branding = BrandingAPI.default; | ||||
|             ['forms', () => new api.FormsAPI.default(this)], | ||||
|  | ||||
|             ['branding', () => BrandingAPI.default], | ||||
|  | ||||
|             /** | ||||
|              * MCT's annotation API that enables | ||||
|              * human-created comments and categorization linked to data products | ||||
|              * @type {module:openmct.AnnotationAPI} | ||||
|              * @memberof module:openmct.MCT# | ||||
|              * @name annotation | ||||
|              */ | ||||
|             ['annotation', () => new api.AnnotationAPI(this)] | ||||
|         ].forEach(apiEntry => { | ||||
|             const apiName = apiEntry[0]; | ||||
|             const apiObject = apiEntry[1](); | ||||
|  | ||||
|             Object.defineProperty(this, apiName, { | ||||
|                 value: apiObject, | ||||
|                 enumerable: false, | ||||
|                 configurable: false, | ||||
|                 writable: true | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         // Plugins that are installed by default | ||||
|  | ||||
|         this.install(this.plugins.Plot()); | ||||
|         this.install(this.plugins.Chart()); | ||||
|         this.install(this.plugins.TelemetryTable.default()); | ||||
|         this.install(PreviewPlugin.default()); | ||||
|         this.install(LicensesPlugin.default()); | ||||
| @@ -271,6 +287,7 @@ define([ | ||||
|         this.install(this.plugins.ObjectInterceptors()); | ||||
|         this.install(this.plugins.DeviceClassifier()); | ||||
|         this.install(this.plugins.UserIndicator()); | ||||
|         this.install(this.plugins.Gauge()); | ||||
|     } | ||||
|  | ||||
|     MCT.prototype = Object.create(EventEmitter.prototype); | ||||
| @@ -379,6 +396,7 @@ define([ | ||||
|     }; | ||||
|  | ||||
|     MCT.prototype.plugins = plugins; | ||||
|     MCT.prototype.components = components.default; | ||||
|  | ||||
|     return MCT; | ||||
| }); | ||||
|   | ||||
| @@ -85,8 +85,6 @@ class ActionCollection extends EventEmitter { | ||||
|     } | ||||
|  | ||||
|     destroy() { | ||||
|         super.removeAllListeners(); | ||||
|  | ||||
|         if (!this.skipEnvironmentObservers) { | ||||
|             this.objectUnsubscribes.forEach(unsubscribe => { | ||||
|                 unsubscribe(); | ||||
| @@ -96,6 +94,7 @@ class ActionCollection extends EventEmitter { | ||||
|         } | ||||
|  | ||||
|         this.emit('destroy', this.view); | ||||
|         this.removeAllListeners(); | ||||
|     } | ||||
|  | ||||
|     getVisibleActions() { | ||||
|   | ||||
							
								
								
									
										277
									
								
								src/api/annotation/AnnotationAPI.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										277
									
								
								src/api/annotation/AnnotationAPI.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,277 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import { v4 as uuid } from 'uuid'; | ||||
| import EventEmitter from 'EventEmitter'; | ||||
|  | ||||
| /** | ||||
|  * @readonly | ||||
|  * @enum {String} AnnotationType | ||||
|  * @property {String} NOTEBOOK The notebook annotation type | ||||
|  * @property {String} GEOSPATIAL The geospatial annotation type | ||||
|  * @property {String} PIXEL_SPATIAL The pixel-spatial annotation type | ||||
|  * @property {String} TEMPORAL The temporal annotation type | ||||
|  * @property {String} PLOT_SPATIAL The plot-spatial annotation type | ||||
|  */ | ||||
| const ANNOTATION_TYPES = Object.freeze({ | ||||
|     NOTEBOOK: 'NOTEBOOK', | ||||
|     GEOSPATIAL: 'GEOSPATIAL', | ||||
|     PIXEL_SPATIAL: 'PIXEL_SPATIAL', | ||||
|     TEMPORAL: 'TEMPORAL', | ||||
|     PLOT_SPATIAL: 'PLOT_SPATIAL' | ||||
| }); | ||||
|  | ||||
| /** | ||||
|  * @typedef {Object} Tag | ||||
|  * @property {String} key a unique identifier for the tag | ||||
|  * @property {String} backgroundColor eg. "#cc0000" | ||||
|  * @property {String} foregroundColor eg. "#ffffff" | ||||
|  */ | ||||
| export default class AnnotationAPI extends EventEmitter { | ||||
|     constructor(openmct) { | ||||
|         super(); | ||||
|         this.openmct = openmct; | ||||
|         this.availableTags = {}; | ||||
|  | ||||
|         this.ANNOTATION_TYPES = ANNOTATION_TYPES; | ||||
|  | ||||
|         this.openmct.types.addType('annotation', { | ||||
|             name: 'Annotation', | ||||
|             description: 'A user created note or comment about time ranges, pixel space, and geospatial features.', | ||||
|             creatable: false, | ||||
|             cssClass: 'icon-notebook', | ||||
|             initialize: function (domainObject) { | ||||
|                 domainObject.targets = domainObject.targets || {}; | ||||
|                 domainObject.originalContextPath = domainObject.originalContextPath || ''; | ||||
|                 domainObject.tags = domainObject.tags || []; | ||||
|                 domainObject.contentText = domainObject.contentText || ''; | ||||
|                 domainObject.annotationType = domainObject.annotationType || 'plotspatial'; | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|     * Create the a generic annotation | ||||
|     * @typedef {Object} CreateAnnotationOptions | ||||
|     * @property {String} name a name for the new parameter | ||||
|     * @property {import('../objects/ObjectAPI').DomainObject} domainObject the domain object to create | ||||
|     * @property {ANNOTATION_TYPES} annotationType the type of annotation to create | ||||
|     * @property {Tag[]} tags | ||||
|     * @property {String} contentText | ||||
|     * @property {import('../objects/ObjectAPI').Identifier[]} targets | ||||
|     */ | ||||
|     /** | ||||
|     * @method create | ||||
|     * @param {CreateAnnotationOptions} options | ||||
|     * @returns {Promise<import('../objects/ObjectAPI').DomainObject>} a promise which will resolve when the domain object | ||||
|     *          has been created, or be rejected if it cannot be saved | ||||
|     */ | ||||
|     async create({name, domainObject, annotationType, tags, contentText, targets}) { | ||||
|         if (!Object.keys(this.ANNOTATION_TYPES).includes(annotationType)) { | ||||
|             throw new Error(`Unknown annotation type: ${annotationType}`); | ||||
|         } | ||||
|  | ||||
|         if (!Object.keys(targets).length) { | ||||
|             throw new Error(`At least one target is required to create an annotation`); | ||||
|         } | ||||
|  | ||||
|         const domainObjectKeyString = this.openmct.objects.makeKeyString(domainObject.identifier); | ||||
|         const originalPathObjects = await this.openmct.objects.getOriginalPath(domainObjectKeyString); | ||||
|         const originalContextPath = this.openmct.objects.getRelativePath(originalPathObjects); | ||||
|         const namespace = domainObject.identifier.namespace; | ||||
|         const type = 'annotation'; | ||||
|         const typeDefinition = this.openmct.types.get(type); | ||||
|         const definition = typeDefinition.definition; | ||||
|  | ||||
|         const createdObject = { | ||||
|             name, | ||||
|             type, | ||||
|             identifier: { | ||||
|                 key: uuid(), | ||||
|                 namespace | ||||
|             }, | ||||
|             tags, | ||||
|             annotationType, | ||||
|             contentText, | ||||
|             originalContextPath | ||||
|         }; | ||||
|  | ||||
|         if (definition.initialize) { | ||||
|             definition.initialize(createdObject); | ||||
|         } | ||||
|  | ||||
|         createdObject.targets = targets; | ||||
|         createdObject.originalContextPath = originalContextPath; | ||||
|  | ||||
|         const success = await this.openmct.objects.save(createdObject); | ||||
|         if (success) { | ||||
|             this.emit('annotationCreated', createdObject); | ||||
|  | ||||
|             return createdObject; | ||||
|         } else { | ||||
|             throw new Error('Failed to create object'); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     defineTag(tagKey, tagsDefinition) { | ||||
|         this.availableTags[tagKey] = tagsDefinition; | ||||
|     } | ||||
|  | ||||
|     getAvailableTags() { | ||||
|         if (this.availableTags) { | ||||
|             const rearrangedToArray = Object.keys(this.availableTags).map(tagKey => { | ||||
|                 return { | ||||
|                     id: tagKey, | ||||
|                     ...this.availableTags[tagKey] | ||||
|                 }; | ||||
|             }); | ||||
|  | ||||
|             return rearrangedToArray; | ||||
|         } else { | ||||
|             return []; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     async getAnnotation(query, searchType) { | ||||
|         let foundAnnotation = null; | ||||
|  | ||||
|         const searchResults = (await Promise.all(this.openmct.objects.search(query, null, searchType))).flat(); | ||||
|         if (searchResults) { | ||||
|             foundAnnotation = searchResults[0]; | ||||
|         } | ||||
|  | ||||
|         return foundAnnotation; | ||||
|     } | ||||
|  | ||||
|     async addAnnotationTag(existingAnnotation, targetDomainObject, targetSpecificDetails, annotationType, tag) { | ||||
|         if (!existingAnnotation) { | ||||
|             const targets = {}; | ||||
|             const targetKeyString = this.openmct.objects.makeKeyString(targetDomainObject.identifier); | ||||
|             targets[targetKeyString] = targetSpecificDetails; | ||||
|             const contentText = `${annotationType} tag`; | ||||
|             const annotationCreationArguments = { | ||||
|                 name: contentText, | ||||
|                 domainObject: targetDomainObject, | ||||
|                 annotationType, | ||||
|                 tags: [tag], | ||||
|                 contentText, | ||||
|                 targets | ||||
|             }; | ||||
|             const newAnnotation = await this.create(annotationCreationArguments); | ||||
|  | ||||
|             return newAnnotation; | ||||
|         } else { | ||||
|             const tagArray = [tag, ...existingAnnotation.tags]; | ||||
|             this.openmct.objects.mutate(existingAnnotation, 'tags', tagArray); | ||||
|  | ||||
|             return existingAnnotation; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     removeAnnotationTag(existingAnnotation, tagToRemove) { | ||||
|         if (existingAnnotation && existingAnnotation.tags.includes(tagToRemove)) { | ||||
|             const cleanedArray = existingAnnotation.tags.filter(extantTag => extantTag !== tagToRemove); | ||||
|             this.openmct.objects.mutate(existingAnnotation, 'tags', cleanedArray); | ||||
|         } else { | ||||
|             throw new Error(`Asked to remove tag (${tagToRemove}) that doesn't exist`, existingAnnotation); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     removeAnnotationTags(existingAnnotation) { | ||||
|         // just removes tags on the annotation as we can't really delete objects | ||||
|         if (existingAnnotation && existingAnnotation.tags) { | ||||
|             this.openmct.objects.mutate(existingAnnotation, 'tags', []); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     #getMatchingTags(query) { | ||||
|         if (!query) { | ||||
|             return []; | ||||
|         } | ||||
|  | ||||
|         const matchingTags = Object.keys(this.availableTags).filter(tagKey => { | ||||
|             if (this.availableTags[tagKey] && this.availableTags[tagKey].label) { | ||||
|                 return this.availableTags[tagKey].label.toLowerCase().includes(query.toLowerCase()); | ||||
|             } | ||||
|  | ||||
|             return false; | ||||
|         }); | ||||
|  | ||||
|         return matchingTags; | ||||
|     } | ||||
|  | ||||
|     #addTagMetaInformationToResults(results, matchingTagKeys) { | ||||
|         const tagsAddedToResults = results.map(result => { | ||||
|             const fullTagModels = result.tags.map(tagKey => { | ||||
|                 const tagModel = this.availableTags[tagKey]; | ||||
|                 tagModel.tagID = tagKey; | ||||
|  | ||||
|                 return tagModel; | ||||
|             }); | ||||
|  | ||||
|             return { | ||||
|                 fullTagModels, | ||||
|                 matchingTagKeys, | ||||
|                 ...result | ||||
|             }; | ||||
|         }); | ||||
|  | ||||
|         return tagsAddedToResults; | ||||
|     } | ||||
|  | ||||
|     async #addTargetModelsToResults(results) { | ||||
|         const modelAddedToResults = await Promise.all(results.map(async result => { | ||||
|             const targetModels = await Promise.all(Object.keys(result.targets).map(async (targetID) => { | ||||
|                 const targetModel = await this.openmct.objects.get(targetID); | ||||
|                 const targetKeyString = this.openmct.objects.makeKeyString(targetModel.identifier); | ||||
|                 const originalPathObjects = await this.openmct.objects.getOriginalPath(targetKeyString); | ||||
|  | ||||
|                 return { | ||||
|                     originalPath: originalPathObjects, | ||||
|                     ...targetModel | ||||
|                 }; | ||||
|             })); | ||||
|  | ||||
|             return { | ||||
|                 targetModels, | ||||
|                 ...result | ||||
|             }; | ||||
|         })); | ||||
|  | ||||
|         return modelAddedToResults; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|     * @method searchForTags | ||||
|     * @param {String} query A query to match against tags. E.g., "dr" will match the tags "drilling" and "driving" | ||||
|     * @param {Object} abortController An optional abort method to stop the query | ||||
|     * @returns {Promise} returns a model of matching tags with their target domain objects attached | ||||
|     */ | ||||
|     async searchForTags(query, abortController) { | ||||
|         const matchingTagKeys = this.#getMatchingTags(query); | ||||
|         const searchResults = (await Promise.all(this.openmct.objects.search(matchingTagKeys, abortController, this.openmct.objects.SEARCH_TYPES.TAGS))).flat(); | ||||
|         const appliedTagSearchResults = this.#addTagMetaInformationToResults(searchResults, matchingTagKeys); | ||||
|         const appliedTargetsModels = await this.#addTargetModelsToResults(appliedTagSearchResults); | ||||
|  | ||||
|         return appliedTargetsModels; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										176
									
								
								src/api/annotation/AnnotationAPISpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										176
									
								
								src/api/annotation/AnnotationAPISpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,176 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import { createOpenMct, resetApplicationState } from '../../utils/testing'; | ||||
| import ExampleTagsPlugin from "../../../example/exampleTags/plugin"; | ||||
|  | ||||
| describe("The Annotation API", () => { | ||||
|     let openmct; | ||||
|     let mockObjectProvider; | ||||
|     let mockDomainObject; | ||||
|     let mockAnnotationObject; | ||||
|  | ||||
|     beforeEach((done) => { | ||||
|         openmct = createOpenMct(); | ||||
|         openmct.install(new ExampleTagsPlugin()); | ||||
|         const availableTags = openmct.annotation.getAvailableTags(); | ||||
|         mockDomainObject = { | ||||
|             type: 'notebook', | ||||
|             name: 'fooRabbitNotebook', | ||||
|             identifier: { | ||||
|                 key: 'some-object', | ||||
|                 namespace: 'fooNameSpace' | ||||
|             } | ||||
|         }; | ||||
|         mockAnnotationObject = { | ||||
|             type: 'annotation', | ||||
|             name: 'Some Notebook Annotation', | ||||
|             annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, | ||||
|             tags: [availableTags[0].id, availableTags[1].id], | ||||
|             identifier: { | ||||
|                 key: 'anAnnotationKey', | ||||
|                 namespace: 'fooNameSpace' | ||||
|             }, | ||||
|             targets: { | ||||
|                 'fooNameSpace:some-object': { | ||||
|                     entryId: 'fooBarEntry' | ||||
|                 } | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         mockObjectProvider = jasmine.createSpyObj("mock provider", [ | ||||
|             "create", | ||||
|             "update", | ||||
|             "get" | ||||
|         ]); | ||||
|         // eslint-disable-next-line require-await | ||||
|         mockObjectProvider.get = async (identifier) => { | ||||
|             if (identifier.key === mockDomainObject.identifier.key) { | ||||
|                 return mockDomainObject; | ||||
|             } else if (identifier.key === mockAnnotationObject.identifier.key) { | ||||
|                 return mockAnnotationObject; | ||||
|             } else { | ||||
|                 return null; | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         mockObjectProvider.create.and.returnValue(Promise.resolve(true)); | ||||
|         mockObjectProvider.update.and.returnValue(Promise.resolve(true)); | ||||
|  | ||||
|         openmct.objects.addProvider('fooNameSpace', mockObjectProvider); | ||||
|         openmct.on('start', done); | ||||
|         openmct.startHeadless(); | ||||
|     }); | ||||
|     afterEach(async () => { | ||||
|         openmct.objects.providers = {}; | ||||
|         await resetApplicationState(openmct); | ||||
|     }); | ||||
|     it("is defined", () => { | ||||
|         expect(openmct.annotation).toBeDefined(); | ||||
|     }); | ||||
|  | ||||
|     describe("Creation", () => { | ||||
|         it("can create annotations", async () => { | ||||
|             const annotationCreationArguments = { | ||||
|                 name: 'Test Annotation', | ||||
|                 domainObject: mockDomainObject, | ||||
|                 annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, | ||||
|                 tags: ['sometag'], | ||||
|                 contentText: "fooContext", | ||||
|                 targets: {'fooTarget': {}} | ||||
|             }; | ||||
|             const annotationObject = await openmct.annotation.create(annotationCreationArguments); | ||||
|             expect(annotationObject).toBeDefined(); | ||||
|             expect(annotationObject.type).toEqual('annotation'); | ||||
|         }); | ||||
|         it("fails if annotation is an unknown type", async () => { | ||||
|             try { | ||||
|                 await openmct.annotation.create('Garbage Annotation', mockDomainObject, 'garbageAnnotation', ['sometag'], "fooContext", {'fooTarget': {}}); | ||||
|             } catch (error) { | ||||
|                 expect(error).toBeDefined(); | ||||
|             } | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe("Tagging", () => { | ||||
|         it("can create a tag", async () => { | ||||
|             const annotationObject = await openmct.annotation.addAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag'); | ||||
|             expect(annotationObject).toBeDefined(); | ||||
|             expect(annotationObject.type).toEqual('annotation'); | ||||
|             expect(annotationObject.tags).toContain('aWonderfulTag'); | ||||
|         }); | ||||
|         it("can delete a tag", async () => { | ||||
|             const originalAnnotationObject = await openmct.annotation.addAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag'); | ||||
|             const annotationObject = await openmct.annotation.addAnnotationTag(originalAnnotationObject, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'anotherTagToRemove'); | ||||
|             expect(annotationObject).toBeDefined(); | ||||
|             openmct.annotation.removeAnnotationTag(annotationObject, 'anotherTagToRemove'); | ||||
|             expect(annotationObject.tags).toEqual(['aWonderfulTag']); | ||||
|             openmct.annotation.removeAnnotationTag(annotationObject, 'aWonderfulTag'); | ||||
|             expect(annotationObject.tags).toEqual([]); | ||||
|         }); | ||||
|         it("throws an error if deleting non-existent tag", async () => { | ||||
|             const annotationObject = await openmct.annotation.addAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag'); | ||||
|             expect(annotationObject).toBeDefined(); | ||||
|             expect(() => { | ||||
|                 openmct.annotation.removeAnnotationTag(annotationObject, 'ThisTagShouldNotExist'); | ||||
|             }).toThrow(); | ||||
|         }); | ||||
|         it("can remove all tags", async () => { | ||||
|             const annotationObject = await openmct.annotation.addAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag'); | ||||
|             expect(annotationObject).toBeDefined(); | ||||
|             expect(() => { | ||||
|                 openmct.annotation.removeAnnotationTags(annotationObject); | ||||
|             }).not.toThrow(); | ||||
|             expect(annotationObject.tags).toEqual([]); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe("Search", () => { | ||||
|         let sharedWorkerToRestore; | ||||
|         beforeEach(async () => { | ||||
|             // use local worker | ||||
|             sharedWorkerToRestore = openmct.objects.inMemorySearchProvider.worker; | ||||
|             openmct.objects.inMemorySearchProvider.worker = null; | ||||
|             await openmct.objects.inMemorySearchProvider.index(mockDomainObject); | ||||
|             await openmct.objects.inMemorySearchProvider.index(mockAnnotationObject); | ||||
|         }); | ||||
|         afterEach(() => { | ||||
|             openmct.objects.inMemorySearchProvider.worker = sharedWorkerToRestore; | ||||
|         }); | ||||
|         it("can search for tags", async () => { | ||||
|             const results = await openmct.annotation.searchForTags('S'); | ||||
|             expect(results).toBeDefined(); | ||||
|             expect(results.length).toEqual(1); | ||||
|         }); | ||||
|         it("can get notebook annotations", async () => { | ||||
|             const targetKeyString = openmct.objects.makeKeyString(mockDomainObject.identifier); | ||||
|             const query = { | ||||
|                 targetKeyString, | ||||
|                 entryId: 'fooBarEntry' | ||||
|             }; | ||||
|  | ||||
|             const results = await openmct.annotation.getAnnotation(query, openmct.objects.SEARCH_TYPES.NOTEBOOK_ANNOTATIONS); | ||||
|             expect(results).toBeDefined(); | ||||
|             expect(results.tags.length).toEqual(2); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
| @@ -24,6 +24,7 @@ define([ | ||||
|     './actions/ActionsAPI', | ||||
|     './composition/CompositionAPI', | ||||
|     './Editor', | ||||
|     './faultmanagement/FaultManagementAPI', | ||||
|     './forms/FormsAPI', | ||||
|     './indicators/IndicatorAPI', | ||||
|     './menu/MenuAPI', | ||||
| @@ -34,11 +35,13 @@ define([ | ||||
|     './telemetry/TelemetryAPI', | ||||
|     './time/TimeAPI', | ||||
|     './types/TypeRegistry', | ||||
|     './user/UserAPI' | ||||
|     './user/UserAPI', | ||||
|     './annotation/AnnotationAPI' | ||||
| ], function ( | ||||
|     ActionsAPI, | ||||
|     CompositionAPI, | ||||
|     EditorAPI, | ||||
|     FaultManagementAPI, | ||||
|     FormsAPI, | ||||
|     IndicatorAPI, | ||||
|     MenuAPI, | ||||
| @@ -49,14 +52,16 @@ define([ | ||||
|     TelemetryAPI, | ||||
|     TimeAPI, | ||||
|     TypeRegistry, | ||||
|     UserAPI | ||||
|     UserAPI, | ||||
|     AnnotationAPI | ||||
| ) { | ||||
|     return { | ||||
|         ActionsAPI: ActionsAPI.default, | ||||
|         CompositionAPI: CompositionAPI, | ||||
|         EditorAPI: EditorAPI, | ||||
|         FaultManagementAPI: FaultManagementAPI, | ||||
|         FormsAPI: FormsAPI, | ||||
|         IndicatorAPI: IndicatorAPI, | ||||
|         IndicatorAPI: IndicatorAPI.default, | ||||
|         MenuAPI: MenuAPI.default, | ||||
|         NotificationAPI: NotificationAPI.default, | ||||
|         ObjectAPI: ObjectAPI, | ||||
| @@ -65,6 +70,7 @@ define([ | ||||
|         TelemetryAPI: TelemetryAPI, | ||||
|         TimeAPI: TimeAPI.default, | ||||
|         TypeRegistry: TypeRegistry, | ||||
|         UserAPI: UserAPI.default | ||||
|         UserAPI: UserAPI.default, | ||||
|         AnnotationAPI: AnnotationAPI.default | ||||
|     }; | ||||
| }); | ||||
|   | ||||
							
								
								
									
										106
									
								
								src/api/faultmanagement/FaultManagementAPI.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								src/api/faultmanagement/FaultManagementAPI.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,106 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| export default class FaultManagementAPI { | ||||
|     constructor(openmct) { | ||||
|         this.openmct = openmct; | ||||
|     } | ||||
|  | ||||
|     addProvider(provider) { | ||||
|         this.provider = provider; | ||||
|     } | ||||
|  | ||||
|     supportsActions() { | ||||
|         return this.provider?.acknowledgeFault !== undefined && this.provider?.shelveFault !== undefined; | ||||
|     } | ||||
|  | ||||
|     request(domainObject) { | ||||
|         if (!this.provider?.supportsRequest(domainObject)) { | ||||
|             return Promise.reject(); | ||||
|         } | ||||
|  | ||||
|         return this.provider.request(domainObject); | ||||
|     } | ||||
|  | ||||
|     subscribe(domainObject, callback) { | ||||
|         if (!this.provider?.supportsSubscribe(domainObject)) { | ||||
|             return Promise.reject(); | ||||
|         } | ||||
|  | ||||
|         return this.provider.subscribe(domainObject, callback); | ||||
|     } | ||||
|  | ||||
|     acknowledgeFault(fault, ackData) { | ||||
|         return this.provider.acknowledgeFault(fault, ackData); | ||||
|     } | ||||
|  | ||||
|     shelveFault(fault, shelveData) { | ||||
|         return this.provider.shelveFault(fault, shelveData); | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** @typedef {object} Fault | ||||
|  * @property {string} type | ||||
|  * @property {object} fault | ||||
|  * @property {boolean} fault.acknowledged | ||||
|  * @property {object} fault.currentValueInfo | ||||
|  * @property {number} fault.currentValueInfo.value | ||||
|  * @property {string} fault.currentValueInfo.rangeCondition | ||||
|  * @property {string} fault.currentValueInfo.monitoringResult | ||||
|  * @property {string} fault.id | ||||
|  * @property {string} fault.name | ||||
|  * @property {string} fault.namespace | ||||
|  * @property {number} fault.seqNum | ||||
|  * @property {string} fault.severity | ||||
|  * @property {boolean} fault.shelved | ||||
|  * @property {string} fault.shortDescription | ||||
|  * @property {string} fault.triggerTime | ||||
|  * @property {object} fault.triggerValueInfo | ||||
|  * @property {number} fault.triggerValueInfo.value | ||||
|  * @property {string} fault.triggerValueInfo.rangeCondition | ||||
|  * @property {string} fault.triggerValueInfo.monitoringResult | ||||
|  * @example | ||||
|  *  { | ||||
|  *     "type": "", | ||||
|  *     "fault": { | ||||
|  *         "acknowledged": true, | ||||
|  *         "currentValueInfo": { | ||||
|  *             "value": 0, | ||||
|  *             "rangeCondition": "", | ||||
|  *             "monitoringResult": "" | ||||
|  *         }, | ||||
|  *         "id": "", | ||||
|  *         "name": "", | ||||
|  *         "namespace": "", | ||||
|  *         "seqNum": 0, | ||||
|  *         "severity": "", | ||||
|  *         "shelved": true, | ||||
|  *         "shortDescription": "", | ||||
|  *         "triggerTime": "", | ||||
|  *         "triggerValueInfo": { | ||||
|  *             "value": 0, | ||||
|  *             "rangeCondition": "", | ||||
|  *             "monitoringResult": "" | ||||
|  *         } | ||||
|  *     } | ||||
|  * } | ||||
|  */ | ||||
							
								
								
									
										144
									
								
								src/api/faultmanagement/FaultManagementAPISpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								src/api/faultmanagement/FaultManagementAPISpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,144 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * License); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an AS IS BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import { | ||||
|     createOpenMct, | ||||
|     resetApplicationState | ||||
| } from '../../utils/testing'; | ||||
|  | ||||
| const faultName = 'super duper fault'; | ||||
| const aFault = { | ||||
|     type: '', | ||||
|     fault: { | ||||
|         acknowledged: true, | ||||
|         currentValueInfo: { | ||||
|             value: 0, | ||||
|             rangeCondition: '', | ||||
|             monitoringResult: '' | ||||
|         }, | ||||
|         id: '', | ||||
|         name: faultName, | ||||
|         namespace: '', | ||||
|         seqNum: 0, | ||||
|         severity: '', | ||||
|         shelved: true, | ||||
|         shortDescription: '', | ||||
|         triggerTime: '', | ||||
|         triggerValueInfo: { | ||||
|             value: 0, | ||||
|             rangeCondition: '', | ||||
|             monitoringResult: '' | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| const faultDomainObject = { | ||||
|     name: 'it is not your fault', | ||||
|     type: 'faultManagement', | ||||
|     identifier: { | ||||
|         key: 'nobodies', | ||||
|         namespace: 'fault' | ||||
|     } | ||||
| }; | ||||
| const aComment = 'THIS is my fault.'; | ||||
| const faultManagementProvider = { | ||||
|     request() { | ||||
|         return Promise.resolve([aFault]); | ||||
|     }, | ||||
|     subscribe(domainObject, callback) { | ||||
|         return () => {}; | ||||
|     }, | ||||
|     supportsRequest(domainObject) { | ||||
|         return domainObject.type === 'faultManagement'; | ||||
|     }, | ||||
|     supportsSubscribe(domainObject) { | ||||
|         return domainObject.type === 'faultManagement'; | ||||
|     }, | ||||
|     acknowledgeFault(fault, { comment = '' }) { | ||||
|         return Promise.resolve({ | ||||
|             success: true | ||||
|         }); | ||||
|     }, | ||||
|     shelveFault(fault, shelveData) { | ||||
|         return Promise.resolve({ | ||||
|             success: true | ||||
|         }); | ||||
|     } | ||||
| }; | ||||
|  | ||||
| describe('The Fault Management API', () => { | ||||
|     let openmct; | ||||
|  | ||||
|     beforeEach(() => { | ||||
|         openmct = createOpenMct(); | ||||
|         openmct.install(openmct.plugins.FaultManagement()); | ||||
|         // openmct.install(openmct.plugins.example.ExampleFaultSource()); | ||||
|         openmct.faults.addProvider(faultManagementProvider); | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     it('allows you to request a fault', async () => { | ||||
|         spyOn(faultManagementProvider, 'supportsRequest').and.callThrough(); | ||||
|  | ||||
|         let faultResponse = await openmct.faults.request(faultDomainObject); | ||||
|  | ||||
|         expect(faultManagementProvider.supportsRequest).toHaveBeenCalledWith(faultDomainObject); | ||||
|         expect(faultResponse[0].fault.name).toEqual(faultName); | ||||
|     }); | ||||
|  | ||||
|     it('allows you to subscribe to a fault', () => { | ||||
|         spyOn(faultManagementProvider, 'subscribe').and.callThrough(); | ||||
|         spyOn(faultManagementProvider, 'supportsSubscribe').and.callThrough(); | ||||
|  | ||||
|         let unsubscribe = openmct.faults.subscribe(faultDomainObject, () => {}); | ||||
|  | ||||
|         expect(unsubscribe).toEqual(jasmine.any(Function)); | ||||
|         expect(faultManagementProvider.supportsSubscribe).toHaveBeenCalledWith(faultDomainObject); | ||||
|         expect(faultManagementProvider.subscribe).toHaveBeenCalledOnceWith(faultDomainObject, jasmine.any(Function)); | ||||
|  | ||||
|     }); | ||||
|  | ||||
|     it('will tell you if the fault management provider supports actions', () => { | ||||
|         expect(openmct.faults.supportsActions()).toBeTrue(); | ||||
|     }); | ||||
|  | ||||
|     it('will allow you to acknowledge a fault', async () => { | ||||
|         spyOn(faultManagementProvider, 'acknowledgeFault').and.callThrough(); | ||||
|  | ||||
|         let ackResponse = await openmct.faults.acknowledgeFault(aFault, aComment); | ||||
|  | ||||
|         expect(faultManagementProvider.acknowledgeFault).toHaveBeenCalledWith(aFault, aComment); | ||||
|         expect(ackResponse.success).toBeTrue(); | ||||
|     }); | ||||
|  | ||||
|     it('will allow you to shelve a fault', async () => { | ||||
|         spyOn(faultManagementProvider, 'shelveFault').and.callThrough(); | ||||
|  | ||||
|         let shelveResponse = await openmct.faults.shelveFault(aFault, aComment); | ||||
|  | ||||
|         expect(faultManagementProvider.shelveFault).toHaveBeenCalledWith(aFault, aComment); | ||||
|         expect(shelveResponse.success).toBeTrue(); | ||||
|     }); | ||||
|  | ||||
| }); | ||||
| @@ -1,5 +1,6 @@ | ||||
| import AutoCompleteField from './components/controls/AutoCompleteField.vue'; | ||||
| import ClockDisplayFormatField from './components/controls/ClockDisplayFormatField.vue'; | ||||
| import CheckBoxField from './components/controls/CheckBoxField.vue'; | ||||
| import Datetime from './components/controls/Datetime.vue'; | ||||
| import FileInput from './components/controls/FileInput.vue'; | ||||
| import Locator from './components/controls/Locator.vue'; | ||||
| @@ -7,11 +8,13 @@ import NumberField from './components/controls/NumberField.vue'; | ||||
| import SelectField from './components/controls/SelectField.vue'; | ||||
| import TextAreaField from './components/controls/TextAreaField.vue'; | ||||
| import TextField from './components/controls/TextField.vue'; | ||||
| import ToggleSwitchField from './components/controls/ToggleSwitchField.vue'; | ||||
|  | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| export const DEFAULT_CONTROLS_MAP = { | ||||
|     'autocomplete': AutoCompleteField, | ||||
|     'checkbox': CheckBoxField, | ||||
|     'composite': ClockDisplayFormatField, | ||||
|     'datetime': Datetime, | ||||
|     'file-input': FileInput, | ||||
| @@ -19,7 +22,8 @@ export const DEFAULT_CONTROLS_MAP = { | ||||
|     'numberfield': NumberField, | ||||
|     'select': SelectField, | ||||
|     'textarea': TextAreaField, | ||||
|     'textfield': TextField | ||||
|     'textfield': TextField, | ||||
|     'toggleSwitch': ToggleSwitchField | ||||
| }; | ||||
|  | ||||
| export default class FormControl { | ||||
| @@ -94,4 +98,3 @@ export default class FormControl { | ||||
|         }; | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -23,10 +23,13 @@ | ||||
| import FormController from './FormController'; | ||||
| import FormProperties from './components/FormProperties.vue'; | ||||
|  | ||||
| import EventEmitter from 'EventEmitter'; | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| export default class FormsAPI { | ||||
| export default class FormsAPI extends EventEmitter { | ||||
|     constructor(openmct) { | ||||
|         super(); | ||||
|  | ||||
|         this.openmct = openmct; | ||||
|         this.formController = new FormController(openmct); | ||||
|     } | ||||
| @@ -107,6 +110,8 @@ export default class FormsAPI { | ||||
|         let onDismiss; | ||||
|         let onSave; | ||||
|  | ||||
|         const self = this; | ||||
|  | ||||
|         const promise = new Promise((resolve, reject) => { | ||||
|             onSave = onFormSave(resolve); | ||||
|             onDismiss = onFormDismiss(reject); | ||||
| @@ -115,7 +120,7 @@ export default class FormsAPI { | ||||
|         const vm = new Vue({ | ||||
|             components: { FormProperties }, | ||||
|             provide: { | ||||
|                 openmct: this.openmct | ||||
|                 openmct: self.openmct | ||||
|             }, | ||||
|             data() { | ||||
|                 return { | ||||
| @@ -132,7 +137,7 @@ export default class FormsAPI { | ||||
|         if (element) { | ||||
|             element.append(formElement); | ||||
|         } else { | ||||
|             overlay = this.openmct.overlays.overlay({ | ||||
|             overlay = self.openmct.overlays.overlay({ | ||||
|                 element: vm.$el, | ||||
|                 size: 'small', | ||||
|                 onDestroy: () => vm.$destroy() | ||||
| @@ -140,6 +145,7 @@ export default class FormsAPI { | ||||
|         } | ||||
|  | ||||
|         function onFormPropertyChange(data) { | ||||
|             self.emit('onFormPropertyChange', data); | ||||
|             if (onChange) { | ||||
|                 onChange(data); | ||||
|             } | ||||
|   | ||||
							
								
								
									
										157
									
								
								src/api/forms/FormsAPISpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										157
									
								
								src/api/forms/FormsAPISpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,157 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
| import { createOpenMct, resetApplicationState } from '../../utils/testing'; | ||||
|  | ||||
| describe('The Forms API', () => { | ||||
|     let openmct; | ||||
|     let element; | ||||
|  | ||||
|     beforeEach((done) => { | ||||
|         element = document.createElement('div'); | ||||
|         element.style.display = 'block'; | ||||
|         element.style.width = '1920px'; | ||||
|         element.style.height = '1080px'; | ||||
|  | ||||
|         openmct = createOpenMct(); | ||||
|         openmct.on('start', done); | ||||
|  | ||||
|         openmct.startHeadless(element); | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     it('openmct supports form API', () => { | ||||
|         expect(openmct.forms).not.toBe(null); | ||||
|     }); | ||||
|  | ||||
|     describe('check default form controls exists', () => { | ||||
|         it('autocomplete', () => { | ||||
|             const control = openmct.forms.getFormControl('autocomplete'); | ||||
|             expect(control).not.toBe(null); | ||||
|         }); | ||||
|  | ||||
|         it('clock', () => { | ||||
|             const control = openmct.forms.getFormControl('composite'); | ||||
|             expect(control).not.toBe(null); | ||||
|         }); | ||||
|  | ||||
|         it('datetime', () => { | ||||
|             const control = openmct.forms.getFormControl('datetime'); | ||||
|             expect(control).not.toBe(null); | ||||
|         }); | ||||
|  | ||||
|         it('file-input', () => { | ||||
|             const control = openmct.forms.getFormControl('file-input'); | ||||
|             expect(control).not.toBe(null); | ||||
|         }); | ||||
|  | ||||
|         it('locator', () => { | ||||
|             const control = openmct.forms.getFormControl('locator'); | ||||
|             expect(control).not.toBe(null); | ||||
|         }); | ||||
|  | ||||
|         it('numberfield', () => { | ||||
|             const control = openmct.forms.getFormControl('numberfield'); | ||||
|             expect(control).not.toBe(null); | ||||
|         }); | ||||
|  | ||||
|         it('select', () => { | ||||
|             const control = openmct.forms.getFormControl('select'); | ||||
|             expect(control).not.toBe(null); | ||||
|         }); | ||||
|  | ||||
|         it('textarea', () => { | ||||
|             const control = openmct.forms.getFormControl('textarea'); | ||||
|             expect(control).not.toBe(null); | ||||
|         }); | ||||
|  | ||||
|         it('textfield', () => { | ||||
|             const control = openmct.forms.getFormControl('textfield'); | ||||
|             expect(control).not.toBe(null); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     it('supports user defined form controls', () => { | ||||
|         const newFormControl = { | ||||
|             show: () => { | ||||
|                 console.log('show new control'); | ||||
|             }, | ||||
|             destroy: () => { | ||||
|                 console.log('destroy'); | ||||
|             } | ||||
|         }; | ||||
|         openmct.forms.addNewFormControl('newFormControl', newFormControl); | ||||
|         const control = openmct.forms.getFormControl('newFormControl'); | ||||
|         expect(control).not.toBe(null); | ||||
|         expect(control.show).not.toBe(null); | ||||
|         expect(control.destroy).not.toBe(null); | ||||
|     }); | ||||
|  | ||||
|     describe('show form on UI', () => { | ||||
|         let formStructure; | ||||
|  | ||||
|         beforeEach(() => { | ||||
|             formStructure = { | ||||
|                 title: 'Test Show Form', | ||||
|                 sections: [ | ||||
|                     { | ||||
|                         rows: [ | ||||
|                             { | ||||
|                                 key: 'name', | ||||
|                                 control: 'textfield', | ||||
|                                 name: 'Title', | ||||
|                                 pattern: '\\S+', | ||||
|                                 required: false, | ||||
|                                 cssClass: 'l-input-lg', | ||||
|                                 value: 'Test Name' | ||||
|                             } | ||||
|                         ] | ||||
|                     } | ||||
|                 ] | ||||
|             }; | ||||
|         }); | ||||
|  | ||||
|         it('when container element is provided', (done) => { | ||||
|             openmct.forms.showForm(formStructure, { element }).catch(() => { | ||||
|                 done(); | ||||
|             }); | ||||
|             const titleElement = element.querySelector('.c-overlay__dialog-title'); | ||||
|             expect(titleElement.textContent).toBe(formStructure.title); | ||||
|  | ||||
|             element.querySelector('.js-cancel-button').click(); | ||||
|         }); | ||||
|  | ||||
|         it('when container element is not provided', (done) => { | ||||
|             openmct.forms.showForm(formStructure).catch(() => { | ||||
|                 done(); | ||||
|             }); | ||||
|  | ||||
|             const titleElement = document.querySelector('.c-overlay__dialog-title'); | ||||
|             const title = titleElement.textContent; | ||||
|  | ||||
|             expect(title).toBe(formStructure.title); | ||||
|             document.querySelector('.js-cancel-button').click(); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
| @@ -21,9 +21,9 @@ | ||||
| *****************************************************************************/ | ||||
|  | ||||
| <template> | ||||
| <div class="c-form"> | ||||
| <div class="c-form js-form"> | ||||
|     <div class="c-overlay__top-bar c-form__top-bar"> | ||||
|         <div class="c-overlay__dialog-title">{{ model.title }}</div> | ||||
|         <div class="c-overlay__dialog-title js-form-title">{{ model.title }}</div> | ||||
|         <div class="c-overlay__dialog-hint hint">All fields marked <span class="req icon-asterisk"></span> are required.</div> | ||||
|     </div> | ||||
|     <form | ||||
| @@ -44,18 +44,14 @@ | ||||
|             > | ||||
|                 {{ section.name }} | ||||
|             </h2> | ||||
|             <div | ||||
|             <FormRow | ||||
|                 v-for="(row, index) in section.rows" | ||||
|                 :key="row.id" | ||||
|                 class="u-contents" | ||||
|             > | ||||
|                 <FormRow | ||||
|                     :css-class="section.cssClass" | ||||
|                     :first="index < 1" | ||||
|                     :row="row" | ||||
|                     @onChange="onChange" | ||||
|                 /> | ||||
|             </div> | ||||
|                 :css-class="row.cssClass" | ||||
|                 :first="index < 1" | ||||
|                 :row="row" | ||||
|                 @onChange="onChange" | ||||
|             /> | ||||
|         </div> | ||||
|     </form> | ||||
|  | ||||
| @@ -64,13 +60,15 @@ | ||||
|             tabindex="0" | ||||
|             :disabled="isInvalid" | ||||
|             class="c-button c-button--major" | ||||
|             aria-label="Save" | ||||
|             @click="onSave" | ||||
|         > | ||||
|             {{ submitLabel }} | ||||
|         </button> | ||||
|         <button | ||||
|             tabindex="0" | ||||
|             class="c-button" | ||||
|             class="c-button js-cancel-button" | ||||
|             aria-label="Cancel" | ||||
|             @click="onDismiss" | ||||
|         > | ||||
|             {{ cancelLabel }} | ||||
| @@ -81,7 +79,7 @@ | ||||
|  | ||||
| <script> | ||||
| import FormRow from "@/api/forms/components/FormRow.vue"; | ||||
| import uuid from 'uuid'; | ||||
| import { v4 as uuid } from 'uuid'; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
|   | ||||
| @@ -23,7 +23,10 @@ | ||||
| <template> | ||||
| <div | ||||
|     class="form-row c-form__row" | ||||
|     :class="[{ 'first': first }]" | ||||
|     :class="[ | ||||
|         { 'first': first }, | ||||
|         cssClass | ||||
|     ]" | ||||
|     @onChange="onChange" | ||||
| > | ||||
|     <div | ||||
| @@ -34,7 +37,7 @@ | ||||
|     </div> | ||||
|     <div | ||||
|         class="c-form-row__state-indicator" | ||||
|         :class="rowClass" | ||||
|         :class="reqClass" | ||||
|     > | ||||
|     </div> | ||||
|     <div | ||||
| @@ -76,22 +79,22 @@ export default { | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|         rowClass() { | ||||
|             let cssClass = this.cssClass; | ||||
|         reqClass() { | ||||
|             let reqClass = 'req'; | ||||
|  | ||||
|             if (this.row.required) { | ||||
|                 cssClass = `${cssClass} req`; | ||||
|             if (!this.row.required) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             if (this.visited && this.valid !== undefined) { | ||||
|                 if (this.valid === true) { | ||||
|                     cssClass = `${cssClass} valid`; | ||||
|                     reqClass = 'valid'; | ||||
|                 } else { | ||||
|                     cssClass = `${cssClass} invalid`; | ||||
|                     reqClass = 'invalid'; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             return cssClass; | ||||
|             return reqClass; | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|   | ||||
| @@ -19,35 +19,47 @@ | ||||
| * this source code distribution or the Licensing information page available | ||||
| * at runtime from the About dialog for additional information. | ||||
| *****************************************************************************/ | ||||
|  | ||||
| <template> | ||||
| <div class="form-control autocomplete"> | ||||
|     <span class="autocompleteInputAndArrow"> | ||||
| <div | ||||
|     ref="autoCompleteForm" | ||||
|     class="form-control c-input--autocomplete js-autocomplete" | ||||
| > | ||||
|     <div | ||||
|         class="c-input--autocomplete__wrapper" | ||||
|     > | ||||
|         <input | ||||
|             ref="autoCompleteInput" | ||||
|             v-model="field" | ||||
|             class="autocompleteInput" | ||||
|             class="c-input--autocomplete__input js-autocomplete__input" | ||||
|             type="text" | ||||
|             :placeholder="placeHolderText" | ||||
|             @click="inputClicked()" | ||||
|             @keydown="keyDown($event)" | ||||
|         > | ||||
|         <span | ||||
|             class="icon-arrow-down" | ||||
|         <div | ||||
|             class="icon-arrow-down c-icon-button c-input--autocomplete__afford-arrow js-autocomplete__afford-arrow" | ||||
|             @click="arrowClicked()" | ||||
|         ></span> | ||||
|     </span> | ||||
|         ></div> | ||||
|     </div> | ||||
|     <div | ||||
|         class="autocompleteOptions" | ||||
|         v-if="!hideOptions" | ||||
|         class="c-menu c-input--autocomplete__options" | ||||
|         aria-label="Autocomplete Options" | ||||
|         @blur="hideOptions = true" | ||||
|     > | ||||
|         <ul v-if="!hideOptions"> | ||||
|         <ul> | ||||
|             <li | ||||
|                 v-for="opt in filteredOptions" | ||||
|                 :key="opt.optionId" | ||||
|                 :class="{'optionPreSelected': optionIndex === opt.optionId}" | ||||
|                 :class="[ | ||||
|                     {'optionPreSelected': optionIndex === opt.optionId}, | ||||
|                     itemCssClass | ||||
|                 ]" | ||||
|                 :style="itemStyle(opt)" | ||||
|                 @click="fillInputWithString(opt.name)" | ||||
|                 @mouseover="optionMouseover(opt.optionId)" | ||||
|             > | ||||
|                 <span class="optionText">{{ opt.name }}</span> | ||||
|                 {{ opt.name }} | ||||
|             </li> | ||||
|         </ul> | ||||
|     </div> | ||||
| @@ -65,7 +77,23 @@ export default { | ||||
|     props: { | ||||
|         model: { | ||||
|             type: Object, | ||||
|             required: true | ||||
|             required: true, | ||||
|             default() { | ||||
|                 return {}; | ||||
|             } | ||||
|         }, | ||||
|         placeHolderText: { | ||||
|             type: String, | ||||
|             default() { | ||||
|                 return ""; | ||||
|             } | ||||
|         }, | ||||
|         itemCssClass: { | ||||
|             type: String, | ||||
|             required: false, | ||||
|             default() { | ||||
|                 return ""; | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
| @@ -78,31 +106,40 @@ export default { | ||||
|     }, | ||||
|     computed: { | ||||
|         filteredOptions() { | ||||
|             const options = this.optionNames || []; | ||||
|             const fullOptions = this.options || []; | ||||
|             if (this.showFilteredOptions) { | ||||
|                 return options | ||||
|                 const optionsFiltered = fullOptions | ||||
|                     .filter(option => { | ||||
|                         return option.toLowerCase().indexOf(this.field.toLowerCase()) >= 0; | ||||
|                         if (option.name && this.field) { | ||||
|                             return option.name.toLowerCase().indexOf(this.field.toLowerCase()) >= 0; | ||||
|                         } | ||||
|  | ||||
|                         return false; | ||||
|                     }).map((option, index) => { | ||||
|                         return { | ||||
|                             optionId: index, | ||||
|                             name: option | ||||
|                             name: option.name, | ||||
|                             color: option.color | ||||
|                         }; | ||||
|                     }); | ||||
|  | ||||
|                 return optionsFiltered; | ||||
|             } | ||||
|  | ||||
|             return options.map((option, index) => { | ||||
|             const optionsFiltered = fullOptions.map((option, index) => { | ||||
|                 return { | ||||
|                     optionId: index, | ||||
|                     name: option | ||||
|                     name: option.name, | ||||
|                     color: option.color | ||||
|                 }; | ||||
|             }); | ||||
|  | ||||
|             return optionsFiltered; | ||||
|         } | ||||
|     }, | ||||
|     watch: { | ||||
|         field(newValue, oldValue) { | ||||
|             if (newValue !== oldValue) { | ||||
|  | ||||
|                 const data = { | ||||
|                     model: this.model, | ||||
|                     value: newValue | ||||
| @@ -123,17 +160,17 @@ export default { | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.options = this.model.options; | ||||
|         this.autocompleteInputAndArrow = this.$el.getElementsByClassName('autocompleteInputAndArrow')[0]; | ||||
|         this.autocompleteInputElement = this.$el.getElementsByClassName('autocompleteInput')[0]; | ||||
|         if (this.options[0].name) { | ||||
|         // If "options" include name, value pair | ||||
|             this.optionNames = this.options.map((opt) => { | ||||
|                 return opt.name; | ||||
|         this.autocompleteInputAndArrow = this.$refs.autoCompleteForm; | ||||
|         this.autocompleteInputElement = this.$refs.autoCompleteInput; | ||||
|         if (this.model.options && this.model.options.length && !this.model.options[0].name) { | ||||
|             // If options is only an array of string. | ||||
|             this.options = this.model.options.map((option) => { | ||||
|                 return { | ||||
|                     name: option | ||||
|                 }; | ||||
|             }); | ||||
|         } else { | ||||
|         // If options is only an array of string. | ||||
|             this.optionNames = this.options; | ||||
|             this.options = this.model.options; | ||||
|         } | ||||
|     }, | ||||
|     destroyed() { | ||||
| @@ -222,6 +259,12 @@ export default { | ||||
|                     }); | ||||
|                 } | ||||
|             }); | ||||
|         }, | ||||
|         itemStyle(option) { | ||||
|             if (option.color) { | ||||
|  | ||||
|                 return { '--optionIconColor': option.color }; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
							
								
								
									
										55
									
								
								src/api/forms/components/controls/CheckBoxField.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								src/api/forms/components/controls/CheckBoxField.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | ||||
| /***************************************************************************** | ||||
| * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
| * as represented by the Administrator of the National Aeronautics and Space | ||||
| * Administration. All rights reserved. | ||||
| * | ||||
| * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
| * "License"); you may not use this file except in compliance with the License. | ||||
| * You may obtain a copy of the License at | ||||
| * http://www.apache.org/licenses/LICENSE-2.0. | ||||
| * | ||||
| * Unless required by applicable law or agreed to in writing, software | ||||
| * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
| * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
| * License for the specific language governing permissions and limitations | ||||
| * under the License. | ||||
| * | ||||
| * Open MCT includes source code licensed under additional open source | ||||
| * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
| * this source code distribution or the Licensing information page available | ||||
| * at runtime from the About dialog for additional information. | ||||
| *****************************************************************************/ | ||||
|  | ||||
| <template> | ||||
| <span class="form-control shell"> | ||||
|     <span | ||||
|         class="field control" | ||||
|         :class="model.cssClass" | ||||
|     > | ||||
|         <input | ||||
|             type="checkbox" | ||||
|             :checked="isChecked" | ||||
|             @input="toggleCheckBox" | ||||
|         > | ||||
|     </span> | ||||
| </span> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import toggleMixin from '../../toggle-check-box-mixin'; | ||||
|  | ||||
| export default { | ||||
|     mixins: [toggleMixin], | ||||
|     props: { | ||||
|         model: { | ||||
|             type: Object, | ||||
|             required: true | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             isChecked: this.model.value | ||||
|         }; | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
| @@ -40,6 +40,12 @@ | ||||
|         > | ||||
|             {{ name }} | ||||
|         </button> | ||||
|         <button | ||||
|             v-if="removable" | ||||
|             class="c-button icon-trash" | ||||
|             title="Remove file" | ||||
|             @click="removeFile" | ||||
|         ></button> | ||||
|     </span> | ||||
| </span> | ||||
| </template> | ||||
| @@ -63,6 +69,9 @@ export default { | ||||
|             const fileInfo = this.fileInfo || this.model.value; | ||||
|  | ||||
|             return fileInfo && fileInfo.name || this.model.text; | ||||
|         }, | ||||
|         removable() { | ||||
|             return (this.fileInfo || this.model.value) && this.model.removable; | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
| @@ -97,6 +106,15 @@ export default { | ||||
|         }, | ||||
|         selectFile() { | ||||
|             this.$refs.fileInput.click(); | ||||
|         }, | ||||
|         removeFile() { | ||||
|             this.model.value = undefined; | ||||
|             this.fileInfo = undefined; | ||||
|             const data = { | ||||
|                 model: this.model, | ||||
|                 value: undefined | ||||
|             }; | ||||
|             this.$emit('onChange', data); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -28,6 +28,7 @@ | ||||
|     > | ||||
|         <input | ||||
|             v-model="field" | ||||
|             :aria-label="model.name" | ||||
|             type="number" | ||||
|             :min="model.min" | ||||
|             :max="model.max" | ||||
| @@ -58,7 +59,6 @@ export default { | ||||
|     }, | ||||
|     methods: { | ||||
|         updateText() { | ||||
|             console.log('updateText', this.field); | ||||
|             const data = { | ||||
|                 model: this.model, | ||||
|                 value: this.field | ||||
|   | ||||
							
								
								
									
										62
									
								
								src/api/forms/components/controls/ToggleSwitchField.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								src/api/forms/components/controls/ToggleSwitchField.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | ||||
| /***************************************************************************** | ||||
| * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
| * as represented by the Administrator of the National Aeronautics and Space | ||||
| * Administration. All rights reserved. | ||||
| * | ||||
| * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
| * "License"); you may not use this file except in compliance with the License. | ||||
| * You may obtain a copy of the License at | ||||
| * http://www.apache.org/licenses/LICENSE-2.0. | ||||
| * | ||||
| * Unless required by applicable law or agreed to in writing, software | ||||
| * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
| * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
| * License for the specific language governing permissions and limitations | ||||
| * under the License. | ||||
| * | ||||
| * Open MCT includes source code licensed under additional open source | ||||
| * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
| * this source code distribution or the Licensing information page available | ||||
| * at runtime from the About dialog for additional information. | ||||
| *****************************************************************************/ | ||||
|  | ||||
| <template> | ||||
| <span class="form-control shell"> | ||||
|     <span | ||||
|         class="field control" | ||||
|         :class="model.cssClass" | ||||
|     > | ||||
|         <ToggleSwitch | ||||
|             id="switchId" | ||||
|             :checked="isChecked" | ||||
|             @change="toggleCheckBox" | ||||
|         /> | ||||
|     </span> | ||||
| </span> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import toggleMixin from '../../toggle-check-box-mixin'; | ||||
| import ToggleSwitch from '@/ui/components/ToggleSwitch.vue'; | ||||
|  | ||||
| import { v4 as uuid } from 'uuid'; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
|         ToggleSwitch | ||||
|     }, | ||||
|     mixins: [toggleMixin], | ||||
|     props: { | ||||
|         model: { | ||||
|             type: Object, | ||||
|             required: true | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             switchId: `toggleSwitch-${uuid}`, | ||||
|             isChecked: this.model.value | ||||
|         }; | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
							
								
								
									
										19
									
								
								src/api/forms/toggle-check-box-mixin.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/api/forms/toggle-check-box-mixin.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| export default { | ||||
|     data() { | ||||
|         return { | ||||
|             isChecked: false | ||||
|         }; | ||||
|     }, | ||||
|     methods: { | ||||
|         toggleCheckBox(event) { | ||||
|             this.isChecked = !this.isChecked; | ||||
|  | ||||
|             const data = { | ||||
|                 model: this.model, | ||||
|                 value: this.isChecked | ||||
|             }; | ||||
|  | ||||
|             this.$emit('onChange', data); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| @@ -19,27 +19,27 @@ | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
| define([ | ||||
|     './SimpleIndicator', | ||||
|     'lodash' | ||||
| ], function ( | ||||
|     SimpleIndicator, | ||||
|     _ | ||||
| ) { | ||||
|     function IndicatorAPI(openmct) { | ||||
|  | ||||
| import EventEmitter from "EventEmitter"; | ||||
| import SimpleIndicator from "./SimpleIndicator"; | ||||
|  | ||||
| class IndicatorAPI extends EventEmitter { | ||||
|     constructor(openmct) { | ||||
|         super(); | ||||
|  | ||||
|         this.openmct = openmct; | ||||
|         this.indicatorObjects = []; | ||||
|     } | ||||
|  | ||||
|     IndicatorAPI.prototype.getIndicatorObjectsByPriority = function () { | ||||
|     getIndicatorObjectsByPriority() { | ||||
|         const sortedIndicators = this.indicatorObjects.sort((a, b) => b.priority - a.priority); | ||||
|  | ||||
|         return sortedIndicators; | ||||
|     }; | ||||
|     } | ||||
|  | ||||
|     IndicatorAPI.prototype.simpleIndicator = function () { | ||||
|     simpleIndicator() { | ||||
|         return new SimpleIndicator(this.openmct); | ||||
|     }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Accepts an indicator object, which is a simple object | ||||
| @@ -62,14 +62,16 @@ define([ | ||||
|      * myIndicator.iconClass("icon-info"); | ||||
|      * | ||||
|      */ | ||||
|     IndicatorAPI.prototype.add = function (indicator) { | ||||
|     add(indicator) { | ||||
|         if (!indicator.priority) { | ||||
|             indicator.priority = this.openmct.priority.DEFAULT; | ||||
|         } | ||||
|  | ||||
|         this.indicatorObjects.push(indicator); | ||||
|     }; | ||||
|  | ||||
|     return IndicatorAPI; | ||||
|         this.emit('addIndicator', indicator); | ||||
|     } | ||||
|  | ||||
| }); | ||||
| } | ||||
|  | ||||
| export default IndicatorAPI; | ||||
|   | ||||
| @@ -20,82 +20,101 @@ | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| define(['zepto', './res/indicator-template.html'], | ||||
|     function ($, indicatorTemplate) { | ||||
|         const DEFAULT_ICON_CLASS = 'icon-info'; | ||||
| import EventEmitter from 'EventEmitter'; | ||||
| import indicatorTemplate from './res/indicator-template.html'; | ||||
| import { convertTemplateToHTML } from '@/utils/template/templateHelpers'; | ||||
|  | ||||
|         function SimpleIndicator(openmct) { | ||||
|             this.openmct = openmct; | ||||
|             this.element = $(indicatorTemplate)[0]; | ||||
|             this.priority = openmct.priority.DEFAULT; | ||||
| const DEFAULT_ICON_CLASS = 'icon-info'; | ||||
|  | ||||
|             this.textElement = this.element.querySelector('.js-indicator-text'); | ||||
| class SimpleIndicator extends EventEmitter { | ||||
|     constructor(openmct) { | ||||
|         super(); | ||||
|  | ||||
|             //Set defaults | ||||
|             this.text('New Indicator'); | ||||
|             this.description(''); | ||||
|             this.iconClass(DEFAULT_ICON_CLASS); | ||||
|             this.statusClass(''); | ||||
|         this.openmct = openmct; | ||||
|         this.element = convertTemplateToHTML(indicatorTemplate)[0]; | ||||
|         this.priority = openmct.priority.DEFAULT; | ||||
|  | ||||
|         this.textElement = this.element.querySelector('.js-indicator-text'); | ||||
|  | ||||
|         //Set defaults | ||||
|         this.text('New Indicator'); | ||||
|         this.description(''); | ||||
|         this.iconClass(DEFAULT_ICON_CLASS); | ||||
|  | ||||
|         this.click = this.click.bind(this); | ||||
|  | ||||
|         this.element.addEventListener('click', this.click); | ||||
|         openmct.once('destroy', () => { | ||||
|             this.removeAllListeners(); | ||||
|             this.element.removeEventListener('click', this.click); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     text(text) { | ||||
|         if (text !== undefined && text !== this.textValue) { | ||||
|             this.textValue = text; | ||||
|             this.textElement.innerText = text; | ||||
|  | ||||
|             if (!text) { | ||||
|                 this.element.classList.add('hidden'); | ||||
|             } else { | ||||
|                 this.element.classList.remove('hidden'); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         SimpleIndicator.prototype.text = function (text) { | ||||
|             if (text !== undefined && text !== this.textValue) { | ||||
|                 this.textValue = text; | ||||
|                 this.textElement.innerText = text; | ||||
|  | ||||
|                 if (!text) { | ||||
|                     this.element.classList.add('hidden'); | ||||
|                 } else { | ||||
|                     this.element.classList.remove('hidden'); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             return this.textValue; | ||||
|         }; | ||||
|  | ||||
|         SimpleIndicator.prototype.description = function (description) { | ||||
|             if (description !== undefined && description !== this.descriptionValue) { | ||||
|                 this.descriptionValue = description; | ||||
|                 this.element.title = description; | ||||
|             } | ||||
|  | ||||
|             return this.descriptionValue; | ||||
|         }; | ||||
|  | ||||
|         SimpleIndicator.prototype.iconClass = function (iconClass) { | ||||
|             if (iconClass !== undefined && iconClass !== this.iconClassValue) { | ||||
|                 // element.classList is precious and throws errors if you try and add | ||||
|                 // or remove empty strings | ||||
|                 if (this.iconClassValue) { | ||||
|                     this.element.classList.remove(this.iconClassValue); | ||||
|                 } | ||||
|  | ||||
|                 if (iconClass) { | ||||
|                     this.element.classList.add(iconClass); | ||||
|                 } | ||||
|  | ||||
|                 this.iconClassValue = iconClass; | ||||
|             } | ||||
|  | ||||
|             return this.iconClassValue; | ||||
|         }; | ||||
|  | ||||
|         SimpleIndicator.prototype.statusClass = function (statusClass) { | ||||
|             if (statusClass !== undefined && statusClass !== this.statusClassValue) { | ||||
|                 if (this.statusClassValue) { | ||||
|                     this.element.classList.remove(this.statusClassValue); | ||||
|                 } | ||||
|  | ||||
|                 if (statusClass) { | ||||
|                     this.element.classList.add(statusClass); | ||||
|                 } | ||||
|  | ||||
|                 this.statusClassValue = statusClass; | ||||
|             } | ||||
|  | ||||
|             return this.statusClassValue; | ||||
|         }; | ||||
|  | ||||
|         return SimpleIndicator; | ||||
|         return this.textValue; | ||||
|     } | ||||
| ); | ||||
|  | ||||
|     description(description) { | ||||
|         if (description !== undefined && description !== this.descriptionValue) { | ||||
|             this.descriptionValue = description; | ||||
|             this.element.title = description; | ||||
|         } | ||||
|  | ||||
|         return this.descriptionValue; | ||||
|     } | ||||
|  | ||||
|     iconClass(iconClass) { | ||||
|         if (iconClass !== undefined && iconClass !== this.iconClassValue) { | ||||
|             // element.classList is precious and throws errors if you try and add | ||||
|             // or remove empty strings | ||||
|             if (this.iconClassValue) { | ||||
|                 this.element.classList.remove(this.iconClassValue); | ||||
|             } | ||||
|  | ||||
|             if (iconClass) { | ||||
|                 this.element.classList.add(iconClass); | ||||
|             } | ||||
|  | ||||
|             this.iconClassValue = iconClass; | ||||
|         } | ||||
|  | ||||
|         return this.iconClassValue; | ||||
|     } | ||||
|  | ||||
|     statusClass(statusClass) { | ||||
|         if (arguments.length === 1 && statusClass !== this.statusClassValue) { | ||||
|             if (this.statusClassValue) { | ||||
|                 this.element.classList.remove(this.statusClassValue); | ||||
|             } | ||||
|  | ||||
|             if (statusClass !== undefined) { | ||||
|                 this.element.classList.add(statusClass); | ||||
|             } | ||||
|  | ||||
|             this.statusClassValue = statusClass; | ||||
|         } | ||||
|  | ||||
|         return this.statusClassValue; | ||||
|     } | ||||
|  | ||||
|     click(event) { | ||||
|         this.emit('click', event); | ||||
|     } | ||||
|  | ||||
|     getElement() { | ||||
|         return this.element; | ||||
|     } | ||||
| } | ||||
|  | ||||
| export default SimpleIndicator; | ||||
|   | ||||
| @@ -26,29 +26,31 @@ import { createOpenMct, createMouseEvent, resetApplicationState } from '../../ut | ||||
|  | ||||
| describe ('The Menu API', () => { | ||||
|     let openmct; | ||||
|     let element; | ||||
|     let appHolder; | ||||
|     let menuAPI; | ||||
|     let actionsArray; | ||||
|     let x; | ||||
|     let y; | ||||
|     let result; | ||||
|     let onDestroy; | ||||
|     let menuElement; | ||||
|  | ||||
|     const x = 8; | ||||
|     const y = 16; | ||||
|  | ||||
|     const menuOptions = { | ||||
|         onDestroy: () => { | ||||
|             console.log('default onDestroy'); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     beforeEach((done) => { | ||||
|         const appHolder = document.createElement('div'); | ||||
|         appHolder = document.createElement('div'); | ||||
|         appHolder.style.display = 'block'; | ||||
|         appHolder.style.width = '1920px'; | ||||
|         appHolder.style.height = '1080px'; | ||||
|  | ||||
|         openmct = createOpenMct(); | ||||
|  | ||||
|         element = document.createElement('div'); | ||||
|         element.style.display = 'block'; | ||||
|         element.style.width = '1920px'; | ||||
|         element.style.height = '1080px'; | ||||
|  | ||||
|         openmct.on('start', done); | ||||
|         openmct.startHeadless(appHolder); | ||||
|         openmct.startHeadless(); | ||||
|  | ||||
|         menuAPI = new MenuAPI(openmct); | ||||
|         actionsArray = [ | ||||
| @@ -56,7 +58,7 @@ describe ('The Menu API', () => { | ||||
|                 key: 'test-css-class-1', | ||||
|                 name: 'Test Action 1', | ||||
|                 cssClass: 'icon-clock', | ||||
|                 description: 'This is a test action', | ||||
|                 description: 'This is a test action 1', | ||||
|                 onItemClicked: () => { | ||||
|                     result = 'Test Action 1 Invoked'; | ||||
|                 } | ||||
| @@ -65,149 +67,165 @@ describe ('The Menu API', () => { | ||||
|                 key: 'test-css-class-2', | ||||
|                 name: 'Test Action 2', | ||||
|                 cssClass: 'icon-clock', | ||||
|                 description: 'This is a test action', | ||||
|                 description: 'This is a test action 2', | ||||
|                 onItemClicked: () => { | ||||
|                     result = 'Test Action 2 Invoked'; | ||||
|                 } | ||||
|             } | ||||
|         ]; | ||||
|         x = 8; | ||||
|         y = 16; | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     describe("showMenu method", () => { | ||||
|         it("creates an instance of Menu when invoked", () => { | ||||
|             menuAPI.showMenu(x, y, actionsArray); | ||||
|  | ||||
|             expect(menuAPI.menuComponent).toBeInstanceOf(Menu); | ||||
|     describe('showMenu method', () => { | ||||
|         beforeAll(() => { | ||||
|             spyOn(menuOptions, 'onDestroy').and.callThrough(); | ||||
|         }); | ||||
|  | ||||
|         describe("creates a menu component", () => { | ||||
|             let menuComponent; | ||||
|             let vueComponent; | ||||
|         it('creates an instance of Menu when invoked', (done) => { | ||||
|             menuOptions.onDestroy = done; | ||||
|  | ||||
|             beforeEach(() => { | ||||
|                 onDestroy = jasmine.createSpy('onDestroy'); | ||||
|             menuAPI.showMenu(x, y, actionsArray, menuOptions); | ||||
|  | ||||
|                 const menuOptions = { | ||||
|                     onDestroy | ||||
|                 }; | ||||
|             expect(menuAPI.menuComponent).toBeInstanceOf(Menu); | ||||
|             document.body.click(); | ||||
|         }); | ||||
|  | ||||
|         describe('creates a menu component', () => { | ||||
|             it('with all the actions passed in', (done) => { | ||||
|                 menuOptions.onDestroy = done; | ||||
|  | ||||
|                 menuAPI.showMenu(x, y, actionsArray, menuOptions); | ||||
|                 vueComponent = menuAPI.menuComponent.component; | ||||
|                 menuComponent = document.querySelector(".c-menu"); | ||||
|                 menuElement = document.querySelector('.c-menu'); | ||||
|                 expect(menuElement).toBeDefined(); | ||||
|  | ||||
|                 spyOn(vueComponent, '$destroy'); | ||||
|             }); | ||||
|  | ||||
|             it("renders a menu component in the expected x and y coordinates", () => { | ||||
|                 let boundingClientRect = menuComponent.getBoundingClientRect(); | ||||
|                 let left = boundingClientRect.left; | ||||
|                 let top = boundingClientRect.top; | ||||
|  | ||||
|                 expect(left).toEqual(x); | ||||
|                 expect(top).toEqual(y); | ||||
|             }); | ||||
|  | ||||
|             it("with all the actions passed in", () => { | ||||
|                 expect(menuComponent).toBeDefined(); | ||||
|  | ||||
|                 let listItems = menuComponent.children[0].children; | ||||
|                 const listItems = menuElement.children[0].children; | ||||
|  | ||||
|                 expect(listItems.length).toEqual(actionsArray.length); | ||||
|                 document.body.click(); | ||||
|             }); | ||||
|  | ||||
|             it("with click-able menu items, that will invoke the correct callBacks", () => { | ||||
|                 let listItem1 = menuComponent.children[0].children[0]; | ||||
|             it('with click-able menu items, that will invoke the correct callBack', (done) => { | ||||
|                 menuOptions.onDestroy = done; | ||||
|  | ||||
|                 menuAPI.showMenu(x, y, actionsArray, menuOptions); | ||||
|  | ||||
|                 menuElement = document.querySelector('.c-menu'); | ||||
|                 const listItem1 = menuElement.children[0].children[0]; | ||||
|  | ||||
|                 listItem1.click(); | ||||
|  | ||||
|                 expect(result).toEqual("Test Action 1 Invoked"); | ||||
|                 expect(result).toEqual('Test Action 1 Invoked'); | ||||
|             }); | ||||
|  | ||||
|             it("dismisses the menu when action is clicked on", () => { | ||||
|                 let listItem1 = menuComponent.children[0].children[0]; | ||||
|             it('dismisses the menu when action is clicked on', (done) => { | ||||
|                 menuOptions.onDestroy = done; | ||||
|  | ||||
|                 menuAPI.showMenu(x, y, actionsArray, menuOptions); | ||||
|  | ||||
|                 menuElement = document.querySelector('.c-menu'); | ||||
|                 const listItem1 = menuElement.children[0].children[0]; | ||||
|                 listItem1.click(); | ||||
|  | ||||
|                 let menu = document.querySelector('.c-menu'); | ||||
|                 menuElement = document.querySelector('.c-menu'); | ||||
|  | ||||
|                 expect(menu).toBeNull(); | ||||
|                 expect(menuElement).toBeNull(); | ||||
|             }); | ||||
|  | ||||
|             it("invokes the destroy method when menu is dismissed", () => { | ||||
|             it('invokes the destroy method when menu is dismissed', (done) => { | ||||
|                 menuOptions.onDestroy = done; | ||||
|  | ||||
|                 menuAPI.showMenu(x, y, actionsArray, menuOptions); | ||||
|  | ||||
|                 const vueComponent = menuAPI.menuComponent.component; | ||||
|                 spyOn(vueComponent, '$destroy'); | ||||
|  | ||||
|                 document.body.click(); | ||||
|  | ||||
|                 expect(vueComponent.$destroy).toHaveBeenCalled(); | ||||
|             }); | ||||
|  | ||||
|             it("invokes the onDestroy callback if passed in", () => { | ||||
|                 document.body.click(); | ||||
|             it('invokes the onDestroy callback if passed in', (done) => { | ||||
|                 let count = 0; | ||||
|                 menuOptions.onDestroy = () => { | ||||
|                     count++; | ||||
|                     expect(count).toEqual(1); | ||||
|                     done(); | ||||
|                 }; | ||||
|  | ||||
|                 expect(onDestroy).toHaveBeenCalled(); | ||||
|                 menuAPI.showMenu(x, y, actionsArray, menuOptions); | ||||
|  | ||||
|                 document.body.click(); | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe("superMenu method", () => { | ||||
|         it("creates a superMenu", () => { | ||||
|             menuAPI.showSuperMenu(x, y, actionsArray); | ||||
|     describe('superMenu method', () => { | ||||
|         it('creates a superMenu', (done) => { | ||||
|             menuOptions.onDestroy = done; | ||||
|  | ||||
|             const superMenu = document.querySelector('.c-super-menu__menu'); | ||||
|             menuAPI.showSuperMenu(x, y, actionsArray, menuOptions); | ||||
|             menuElement = document.querySelector('.c-super-menu__menu'); | ||||
|  | ||||
|             expect(superMenu).not.toBeNull(); | ||||
|             expect(menuElement).not.toBeNull(); | ||||
|             document.body.click(); | ||||
|         }); | ||||
|  | ||||
|         it("Mouse over a superMenu shows correct description", (done) => { | ||||
|             menuAPI.showSuperMenu(x, y, actionsArray); | ||||
|         it('Mouse over a superMenu shows correct description', (done) => { | ||||
|             menuOptions.onDestroy = done; | ||||
|  | ||||
|             const superMenu = document.querySelector('.c-super-menu__menu'); | ||||
|             const superMenuItem = superMenu.querySelector('li'); | ||||
|             menuAPI.showSuperMenu(x, y, actionsArray, menuOptions); | ||||
|             menuElement = document.querySelector('.c-super-menu__menu'); | ||||
|  | ||||
|             const superMenuItem = menuElement.querySelector('li'); | ||||
|             const mouseOverEvent = createMouseEvent('mouseover'); | ||||
|  | ||||
|             superMenuItem.dispatchEvent(mouseOverEvent); | ||||
|             const itemDescription = document.querySelector('.l-item-description__description'); | ||||
|  | ||||
|             setTimeout(() => { | ||||
|             menuAPI.menuComponent.component.$nextTick(() => { | ||||
|                 expect(menuElement).not.toBeNull(); | ||||
|                 expect(itemDescription.innerText).toEqual(actionsArray[0].description); | ||||
|                 expect(superMenu).not.toBeNull(); | ||||
|                 done(); | ||||
|             }, 300); | ||||
|  | ||||
|                 document.body.click(); | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe("Menu Placements", () => { | ||||
|         it("default menu position BOTTOM_RIGHT", () => { | ||||
|             menuAPI.showMenu(x, y, actionsArray); | ||||
|  | ||||
|             const menu = document.querySelector('.c-menu'); | ||||
|  | ||||
|             const boundingClientRect = menu.getBoundingClientRect(); | ||||
|             const left = boundingClientRect.left; | ||||
|             const top = boundingClientRect.top; | ||||
|  | ||||
|             expect(left).toEqual(x); | ||||
|             expect(top).toEqual(y); | ||||
|         }); | ||||
|  | ||||
|         it("menu position BOTTOM_RIGHT", () => { | ||||
|             const menuOptions = { | ||||
|                 placement: openmct.menus.menuPlacement.BOTTOM_RIGHT | ||||
|             }; | ||||
|     describe('Menu Placements', () => { | ||||
|         it('default menu position BOTTOM_RIGHT', (done) => { | ||||
|             menuOptions.onDestroy = done; | ||||
|  | ||||
|             menuAPI.showMenu(x, y, actionsArray, menuOptions); | ||||
|             menuElement = document.querySelector('.c-menu'); | ||||
|  | ||||
|             const menu = document.querySelector('.c-menu'); | ||||
|             const boundingClientRect = menu.getBoundingClientRect(); | ||||
|             const boundingClientRect = menuElement.getBoundingClientRect(); | ||||
|             const left = boundingClientRect.left; | ||||
|             const top = boundingClientRect.top; | ||||
|  | ||||
|             expect(left).toEqual(x); | ||||
|             expect(top).toEqual(y); | ||||
|  | ||||
|             document.body.click(); | ||||
|         }); | ||||
|  | ||||
|         it('menu position BOTTOM_RIGHT', (done) => { | ||||
|             menuOptions.onDestroy = done; | ||||
|             menuOptions.placement = openmct.menus.menuPlacement.BOTTOM_RIGHT; | ||||
|  | ||||
|             menuAPI.showMenu(x, y, actionsArray, menuOptions); | ||||
|             menuElement = document.querySelector('.c-menu'); | ||||
|  | ||||
|             const boundingClientRect = menuElement.getBoundingClientRect(); | ||||
|             const left = boundingClientRect.left; | ||||
|             const top = boundingClientRect.top; | ||||
|  | ||||
|             expect(left).toEqual(x); | ||||
|             expect(top).toEqual(y); | ||||
|  | ||||
|             document.body.click(); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
|   | ||||
| @@ -12,6 +12,7 @@ | ||||
|                 :key="action.name" | ||||
|                 :class="[action.cssClass, action.isDisabled ? 'disabled' : '']" | ||||
|                 :title="action.description" | ||||
|                 :data-testid="action.testId || false" | ||||
|                 @click="action.onItemClicked" | ||||
|             > | ||||
|                 {{ action.name }} | ||||
| @@ -35,8 +36,9 @@ | ||||
|         <li | ||||
|             v-for="action in options.actions" | ||||
|             :key="action.name" | ||||
|             :class="action.cssClass" | ||||
|             :class="[action.cssClass, action.isDisabled ? 'disabled' : '']" | ||||
|             :title="action.description" | ||||
|             :data-testid="action.testId || false" | ||||
|             @click="action.onItemClicked" | ||||
|         > | ||||
|             {{ action.name }} | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user