Compare commits
	
		
			116 Commits
		
	
	
		
			notebook-e
			...
			gauge-fixe
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | d3dc74500f | ||
|   | 59880955a2 | ||
|   | b51ed7e844 | ||
|   | 0f0c6a7b17 | ||
|   | 370e6a0c37 | ||
|   | 815506cf17 | ||
|   | bdb1867c73 | ||
|   | e288fdffea | ||
|   | 194060f30a | ||
|   | 45bc317a59 | ||
|   | e103ea44d8 | ||
|   | d13d7dc8f3 | ||
|   | 7bbaec4006 | ||
|   | 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 | ||
|   | c0f24b3925 | ||
|   | c46849b166 | ||
|   | 6c71fa01f5 | ||
|   | c56d458ecb | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | f74a35f45a | ||
|   | 4e79725897 | ||
|   | 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 | ||
|   | 0674c9fc33 | ||
|   | 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 | ||
|   | de1b877954 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | e063442d8c | ||
|   | 6a5823ab5c | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 0493e5ae3c | ||
|   | 24f13b6249 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 4db2f547d9 | ||
|   | 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 | 
| @@ -2,7 +2,7 @@ version: 2.1 | ||||
| executors: | ||||
|   pw-focal-development: | ||||
|     docker: | ||||
|       - image: mcr.microsoft.com/playwright:v1.19.2-focal | ||||
|       - image: mcr.microsoft.com/playwright:v1.21.1-focal | ||||
|     environment: | ||||
|       NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed | ||||
| parameters: | ||||
| @@ -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: | ||||
| @@ -61,10 +61,10 @@ commands: | ||||
|   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  | ||||
|         - run: curl -Os https://uploader.codecov.io/latest/linux/codecov;chmod +x codecov;./codecov | ||||
| 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 +101,7 @@ jobs: | ||||
|             equal: [ "FirefoxESR", <<parameters.browser>> ] | ||||
|           steps: | ||||
|             - browser-tools/install-firefox: | ||||
|                 version: "91.7.1esr" #https://archive.mozilla.org/pub/firefox/releases/           | ||||
|                 version: "91.7.1esr" #https://archive.mozilla.org/pub/firefox/releases/ | ||||
|       - when: | ||||
|           condition: | ||||
|             equal: [ "FirefoxHeadless", <<parameters.browser>> ] | ||||
| @@ -128,36 +128,52 @@ 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>> | ||||
|       - run: SHARD="$((${CIRCLE_NODE_INDEX}+1))"; npm run test:e2e:<<parameters.suite>> -- --shard=${SHARD}/${CIRCLE_NODE_TOTAL} | ||||
|       - store_test_results: | ||||
|           path: test-results/results.xml | ||||
|       - store_artifacts: | ||||
|           path: 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 | ||||
|       - generate_and_store_version_and_filesystem_artifacts       | ||||
| workflows: | ||||
|   overall-circleci-commit-status: #These jobs run on every commit | ||||
|     jobs: | ||||
|       - lint: | ||||
|           name: node16-lint | ||||
|           node-version: lts/gallium | ||||
|       - unit-test:  | ||||
|       - unit-test: | ||||
|           name: node14-chrome | ||||
|           node-version: lts/fermium | ||||
|           browser: ChromeHeadless | ||||
|           post-steps: | ||||
|             - upload_code_covio   | ||||
|             - upload_code_covio | ||||
|       - unit-test: | ||||
|           name: node16-chrome | ||||
|           node-version: lts/gallium | ||||
|           browser: ChromeHeadless              | ||||
|           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: | ||||
| @@ -176,6 +192,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 | ||||
|   | ||||
							
								
								
									
										4
									
								
								.github/workflows/e2e-pr.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/e2e-pr.yml
									
									
									
									
										vendored
									
									
								
							| @@ -30,11 +30,11 @@ jobs: | ||||
|       - uses: actions/setup-node@v3 | ||||
|         with: | ||||
|           node-version: '16' | ||||
|       - run: npx playwright@1.19.2 install | ||||
|       - run: npx playwright@1.21.1 install | ||||
|       - 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.21.1 install | ||||
|       - run: npm install | ||||
|       - name: Run the e2e visual tests | ||||
|         run: npm run test:e2e:visual | ||||
|   | ||||
							
								
								
									
										1
									
								
								.github/workflows/pr-platform.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/pr-platform.yml
									
									
									
									
										vendored
									
									
								
							| @@ -18,6 +18,7 @@ jobs: | ||||
|         node_version: | ||||
|           - 14 | ||||
|           - 16 | ||||
|           - 18 | ||||
|         architecture: | ||||
|           - x64 | ||||
|     name: Node ${{ matrix.node_version }} - ${{ matrix.architecture }} on ${{ matrix.os }} | ||||
|   | ||||
| @@ -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" | ||||
|             } | ||||
|         } | ||||
|     ] | ||||
| }; | ||||
|   | ||||
							
								
								
									
										41
									
								
								e2e/fixtures.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								e2e/fixtures.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| /* eslint-disable no-undef */ | ||||
|  | ||||
| // This file extends the base functionality of the playwright test framework | ||||
| const base = require('@playwright/test'); | ||||
| const { expect } = require('@playwright/test'); | ||||
|  | ||||
| /** | ||||
|  * 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})`; | ||||
| } | ||||
|  | ||||
| exports.test = base.test.extend({ | ||||
|     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,41 @@ | ||||
| // 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: 2, | ||||
|     retries: 1, | ||||
|     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, | ||||
|         timeout: 200 * 1000, | ||||
|         reuseExistingServer: !process.env.CI | ||||
|     }, | ||||
|     maxFailures: process.env.CI ? 5 : undefined, //Limits failures to 5 to reduce CI Waste | ||||
|     workers: 2, //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: 'on-first-retry' | ||||
|     }, | ||||
|     projects: [ | ||||
|         { | ||||
|             name: 'chrome', | ||||
|             use: { | ||||
|                 browserName: 'chromium', | ||||
|                 ...devices['Desktop Chrome'] | ||||
|                 browserName: 'chromium' | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|             name: 'MMOC', | ||||
|             grepInvert: /@snapshot/, | ||||
|             use: { | ||||
|                 browserName: 'chromium', | ||||
|                 viewport: { | ||||
| @@ -52,8 +55,11 @@ const config = { | ||||
|     ], | ||||
|     reporter: [ | ||||
|         ['list'], | ||||
|         ['html', { | ||||
|             open: 'never', | ||||
|             outputFolder: '../test-results/html/' | ||||
|         }], | ||||
|         ['junit', { outputFile: 'test-results/results.xml' }], | ||||
|         ['allure-playwright'], | ||||
|         ['github'] | ||||
|     ] | ||||
| }; | ||||
|   | ||||
| @@ -2,12 +2,14 @@ | ||||
| // 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', | ||||
| @@ -21,20 +23,20 @@ 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: 'retain-on-failure' | ||||
|     }, | ||||
|     projects: [ | ||||
|         { | ||||
|             name: 'chrome', | ||||
|             use: { | ||||
|                 browserName: 'chromium', | ||||
|                 ...devices['Desktop Chrome'] | ||||
|                 browserName: 'chromium' | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|             name: 'MMOC', | ||||
|             grepInvert: /@snapshot/, | ||||
|             use: { | ||||
|                 browserName: 'chromium', | ||||
|                 viewport: { | ||||
| @@ -53,7 +55,10 @@ const config = { | ||||
|     ], | ||||
|     reporter: [ | ||||
|         ['list'], | ||||
|         ['allure-playwright'] | ||||
|         ['html', { | ||||
|             open: 'on-failure', | ||||
|             outputFolder: '../test-results' | ||||
|         }] | ||||
|     ] | ||||
| }; | ||||
|  | ||||
|   | ||||
							
								
								
									
										41
									
								
								e2e/playwright-performance.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								e2e/playwright-performance.config.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| /* eslint-disable no-undef */ | ||||
| // playwright.config.js | ||||
| // @ts-check | ||||
|  | ||||
| /** @type {import('@playwright/test').PlaywrightTestConfig} */ | ||||
| const config = { | ||||
|     retries: 1, //Only for debugging purposes | ||||
|     testDir: 'tests/performance/', | ||||
|     timeout: 60 * 1000, | ||||
|     workers: 1, //Only run in serial with 1 worker | ||||
|     webServer: { | ||||
|         command: 'npm run start', | ||||
|         port: 8080, | ||||
|         timeout: 200 * 1000, | ||||
|         reuseExistingServer: !process.env.CI | ||||
|     }, | ||||
|     use: { | ||||
|         browserName: "chromium", | ||||
|         baseURL: 'http://localhost:8080/', | ||||
|         headless: Boolean(process.env.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,10 +4,10 @@ | ||||
|  | ||||
| /** @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, | ||||
| @@ -17,7 +17,7 @@ const config = { | ||||
|     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', | ||||
| @@ -25,8 +25,7 @@ const config = { | ||||
|     }, | ||||
|     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"} | ||||
							
								
								
									
										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 }) => { | ||||
| @@ -57,6 +58,6 @@ test.describe('Branding tests', () => { | ||||
|             page.waitForEvent('popup'), | ||||
|             page.locator('text=click here for third party licensing information').click() | ||||
|         ]); | ||||
|         expect(page2.waitForURL('**\/licenses**')).toBeTruthy(); | ||||
|         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,7 +24,8 @@ | ||||
| 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 }) => { | ||||
|   | ||||
| @@ -24,19 +24,123 @@ | ||||
| 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); | ||||
|  | ||||
|         // 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(), | ||||
|             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); | ||||
|  | ||||
|         // 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(), | ||||
|             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); | ||||
|  | ||||
|         // 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 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,7 +24,8 @@ | ||||
| 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 | ||||
|   | ||||
| @@ -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,139 @@ 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/tests/recycled_storage.json' }); | ||||
|  | ||||
|         //Set object identifier from url | ||||
|         conditionSetUrl = await page.url(); | ||||
|         console.log('conditionSetUrl ' + conditionSetUrl); | ||||
|  | ||||
|         getConditionSetIdentifierFromUrl = await conditionSetUrl.split('/').pop().split('?')[0]; | ||||
|         console.debug('getConditionSetIdentifierFromUrl ' + getConditionSetIdentifierFromUrl); | ||||
|     }); | ||||
|     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() | ||||
|     test.afterAll(async ({ browser }) => { | ||||
|         await browser.close(); | ||||
|     }); | ||||
|     test.fixme('condition set object can be modified', async ({ page }) => { | ||||
|         //Go to object created in step one | ||||
|     //Load localStorage for subsequent tests | ||||
|     test.use({ storageState: './e2e/tests/recycled_storage.json' }); | ||||
|     //Begin suite of tests again localStorage | ||||
|     test('Condition set object properties persist in main view and inspector', async ({ page }) => { | ||||
|         //Navigate to baseURL with injected localStorage | ||||
|         await page.goto(conditionSetUrl, { waitUntil: 'networkidle' }); | ||||
|  | ||||
|         //Assertions on loaded Condition Set in main view | ||||
|         await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set'); | ||||
|  | ||||
|         //Assertions on loaded Condition Set in Inspector | ||||
|         await 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 | ||||
|         await expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy; | ||||
|  | ||||
|     }); | ||||
|     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 | ||||
|         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 | ||||
|         await expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy(); | ||||
|         // Verify Inspector Details has updated Name property | ||||
|         await 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 | ||||
|         await 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'); | ||||
|         await 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 | ||||
|         await expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy(); | ||||
|         // Verify Inspector Details has updated Name property | ||||
|         await 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 | ||||
|         await 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'); | ||||
|         await 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' }); | ||||
|  | ||||
|         const numberOfConditionSetsToStart = await page.locator('a:has-text("Unnamed Condition Set Condition Set")').count(); | ||||
|         //Expect Unnamed Condition Set to be visible in Main View | ||||
|         await expect(page.locator('a:has-text("Unnamed Condition Set Condition Set") >> nth=0')).toBeVisible(); | ||||
|  | ||||
|         // 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,14 @@ | ||||
| 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'); | ||||
|  | ||||
| test.describe('Example Imagery', () => { | ||||
|  | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         page.on('console', msg => console.log(msg.text())) | ||||
|         //Go to baseURL | ||||
|         await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
| @@ -40,32 +41,38 @@ test.describe('Example Imagery', () => { | ||||
|         // Click text=Example Imagery | ||||
|         await page.click('text=Example Imagery'); | ||||
|  | ||||
|         // 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")'); | ||||
|  | ||||
|         // 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') | ||||
|         ]); | ||||
|  | ||||
|         //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'); | ||||
|     }); | ||||
|  | ||||
|     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 bgImageLocator = page.locator(backgroundImageSelector); | ||||
|         const deltaYStep = 100; //equivalent to 1x zoom | ||||
|         await bgImageLocator.hover(); | ||||
|         await bgImageLocator.hover({trial: true}); | ||||
|         const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|         // zoom in | ||||
|         await bgImageLocator.hover(); | ||||
|         await bgImageLocator.hover({trial: true}); | ||||
|         await page.mouse.wheel(0, deltaYStep * 2); | ||||
|         // wait for zoom animation to finish | ||||
|         await bgImageLocator.hover(); | ||||
|         await bgImageLocator.hover({trial: true}); | ||||
|         const imageMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|         // zoom out | ||||
|         await bgImageLocator.hover(); | ||||
|         await bgImageLocator.hover({trial: true}); | ||||
|         await page.mouse.wheel(0, -deltaYStep); | ||||
|         // wait for zoom animation to finish | ||||
|         await bgImageLocator.hover(); | ||||
|         await bgImageLocator.hover({trial: true}); | ||||
|         const imageMouseZoomedOut = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|  | ||||
|         expect(imageMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height); | ||||
| @@ -77,12 +84,14 @@ test.describe('Example Imagery', () => { | ||||
|  | ||||
|     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']; | ||||
|  | ||||
|         const bgImageLocator = page.locator(backgroundImageSelector); | ||||
|         await bgImageLocator.hover({trial: true}); | ||||
|  | ||||
|         const bgImageLocator = await page.locator(backgroundImageSelector); | ||||
|         await bgImageLocator.hover(); | ||||
|         // zoom in | ||||
|         await page.mouse.wheel(0, deltaYStep * 2); | ||||
|         await bgImageLocator.hover(); | ||||
|         await bgImageLocator.hover({trial: true}); | ||||
|         const zoomedBoundingBox = await bgImageLocator.boundingBox(); | ||||
|         const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2; | ||||
|         const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2; | ||||
| @@ -91,63 +100,72 @@ 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'); | ||||
|         await Promise.all(panHotkey.map(x => page.keyboard.up(x))); | ||||
|         const afterRightPanBoundingBox = await bgImageLocator.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'); | ||||
|         await Promise.all(panHotkey.map(x => page.keyboard.up(x))); | ||||
|         const afterLeftPanBoundingBox = await bgImageLocator.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'); | ||||
|         await Promise.all(panHotkey.map(x => page.keyboard.up(x))); | ||||
|         const afterUpPanBoundingBox = await bgImageLocator.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'); | ||||
|         await Promise.all(panHotkey.map(x => page.keyboard.up(x))); | ||||
|         const afterDownPanBoundingBox = await bgImageLocator.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 bgImageLocator = page.locator(backgroundImageSelector); | ||||
|         await bgImageLocator.hover({trial: true}); | ||||
|         const zoomInBtn = page.locator('.t-btn-zoom-in').nth(0); | ||||
|         const zoomOutBtn = page.locator('.t-btn-zoom-out').nth(0); | ||||
|         const initialBoundingBox = await bgImageLocator.boundingBox(); | ||||
|  | ||||
|         await zoomInBtn.click(); | ||||
|         await zoomInBtn.click(); | ||||
|         // wait for zoom animation to finish | ||||
|         await bgImageLocator.hover(); | ||||
|         await bgImageLocator.hover({trial: true}); | ||||
|         const zoomedInBoundingBox = await bgImageLocator.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(); | ||||
|         await bgImageLocator.hover({trial: true}); | ||||
|         const zoomedOutBoundingBox = await bgImageLocator.boundingBox(); | ||||
|         expect(zoomedOutBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height); | ||||
|         expect(zoomedOutBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width); | ||||
| @@ -155,22 +173,28 @@ test.describe('Example Imagery', () => { | ||||
|     }); | ||||
|  | ||||
|     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 bgImageLocator = page.locator(backgroundImageSelector); | ||||
|         // wait for zoom animation to finish | ||||
|         await bgImageLocator.hover({trial: true}); | ||||
|  | ||||
|         const zoomInBtn = page.locator('.t-btn-zoom-in').nth(0); | ||||
|         const zoomResetBtn = page.locator('.t-btn-zoom-reset').nth(0); | ||||
|         const initialBoundingBox = await bgImageLocator.boundingBox(); | ||||
|  | ||||
|         await zoomInBtn.click(); | ||||
|         // wait for zoom animation to finish | ||||
|         await bgImageLocator.hover({trial: true}); | ||||
|         await zoomInBtn.click(); | ||||
|         // wait for zoom animation to finish | ||||
|         await bgImageLocator.hover(); | ||||
|         await bgImageLocator.hover({trial: true}); | ||||
|  | ||||
|         const zoomedInBoundingBox = await bgImageLocator.boundingBox(); | ||||
|         expect.soft(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height); | ||||
|         expect.soft(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width); | ||||
|  | ||||
|         await zoomResetBtn.click(); | ||||
|         await bgImageLocator.hover(); | ||||
|         // wait for zoom animation to finish | ||||
|         await bgImageLocator.hover({trial: true}); | ||||
|  | ||||
|         const resetBoundingBox = await bgImageLocator.boundingBox(); | ||||
|         expect.soft(resetBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height); | ||||
| @@ -180,38 +204,271 @@ test.describe('Example Imagery', () => { | ||||
|         expect(resetBoundingBox.width).toEqual(initialBoundingBox.width); | ||||
|     }); | ||||
|  | ||||
|     //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('Using the zoom features does not pause telemetry', async ({ page }) => { | ||||
|         const bgImageLocator = page.locator(backgroundImageSelector); | ||||
|         const pausePlayButton = page.locator('.c-button.pause-play'); | ||||
|         // wait for zoom animation to finish | ||||
|         await bgImageLocator.hover({trial: true}); | ||||
|  | ||||
|         // 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/); | ||||
|         const zoomInBtn = page.locator('.t-btn-zoom-in').nth(0); | ||||
|         await zoomInBtn.click(); | ||||
|         // wait for zoom animation to finish | ||||
|         await bgImageLocator.hover({trial: true}); | ||||
|  | ||||
|         return expect(pausePlayButton).not.toHaveClass(/is-paused/); | ||||
|     }); | ||||
|  | ||||
| }); | ||||
|  | ||||
| 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'); | ||||
| const backgroundImageSelector = '.c-imagery__main-image__background-image'; | ||||
| test('Example Imagery in Display layout', async ({ page }) => { | ||||
|     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 | ||||
|     // FIXME: Update the value to 5000 ms when this bug is fixed. | ||||
|     // See: https://github.com/nasa/openmct/issues/5265 | ||||
|     await page.locator('input[type="number"]').fill(''); | ||||
|     await page.locator('input[type="number"]').fill('0'); | ||||
|  | ||||
|     // 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'); | ||||
|     const bgImageLocator = page.locator(backgroundImageSelector); | ||||
|     await bgImageLocator.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 bgImageLocator.hover({trial: true}); | ||||
|     const deltaYStep = 100; // equivalent to 1x zoom | ||||
|     await page.mouse.wheel(0, deltaYStep * 2); | ||||
|     const zoomedBoundingBox = await bgImageLocator.boundingBox(); | ||||
|     const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2; | ||||
|     const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2; | ||||
|  | ||||
|     // Wait for zoom animation to finish | ||||
|     await bgImageLocator.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 bgImageLocator.hover({trial: true}); | ||||
|     await page.mouse.wheel(0, deltaYStep * 2); | ||||
|  | ||||
|     // Wait for zoom animation to finish | ||||
|     await bgImageLocator.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 new images still stream in", | ||||
|         timeout: 6 * 1000 | ||||
|     }).toBeGreaterThan(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 | ||||
|     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); | ||||
| }); | ||||
|  | ||||
| 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.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.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('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'); | ||||
| }); | ||||
|  | ||||
| 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'); | ||||
| }); | ||||
|   | ||||
							
								
								
									
										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 | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										264
									
								
								e2e/tests/plugins/notebook/restrictedNotebook.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										264
									
								
								e2e/tests/plugins/notebook/restrictedNotebook.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,264 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 COMMIT_BUTTON_TEXT = 'button:has-text("Commit Entries")'; | ||||
| const SINE_WAVE_GENERATOR = 'text=Unnamed Sine Wave Generator'; | ||||
| const NOTEBOOK_DROP_AREA = '.c-notebook__drag-area'; | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function startAndAddNotebookObject(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') | ||||
|     ]); | ||||
|  | ||||
|     return; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @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'); | ||||
|  | ||||
|     return; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @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(SINE_WAVE_GENERATOR, NOTEBOOK_DROP_AREA); | ||||
|  | ||||
|     return; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function lockPage(page) { | ||||
|     const commitButton = page.locator(COMMIT_BUTTON_TEXT); | ||||
|     await commitButton.click(); | ||||
|  | ||||
|     // confirmation dialog click | ||||
|     await page.locator('text=Lock Page').click(); | ||||
|  | ||||
|     // waiting for mutation of locked page | ||||
|     await new Promise((resolve, reject) => { | ||||
|         setTimeout(resolve, 1000); | ||||
|     }); | ||||
|  | ||||
|     return; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function openContextMenuRestrictedNotebook(page) { | ||||
|     // Click text=Open MCT My Items (This expands the My Items folder to show it's chilren in the tree) | ||||
|     await page.locator('text=Open MCT My Items >> span').nth(3).click(); | ||||
|  | ||||
|     // Click a:has-text("Unnamed CUSTOM_NAME") | ||||
|     await page.locator(`a:has-text("Unnamed ${CUSTOM_NAME}")`).click({ | ||||
|         button: 'right' | ||||
|     }); | ||||
|  | ||||
|     return; | ||||
| } | ||||
|  | ||||
| test.describe('Restricted Notebook', () => { | ||||
|  | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         await startAndAddNotebookObject(page); | ||||
|     }); | ||||
|  | ||||
|     test('Can be renamed', async ({ page }) => { | ||||
|         await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText(`Unnamed ${CUSTOM_NAME}`); | ||||
|     }); | ||||
|  | ||||
|     test('Can be deleted if there are no locked pages', 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 text=Remove | ||||
|         await page.locator('text=Remove').click(); | ||||
|         // Click text=OK | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine?tc.mode=fixed&tc.startBound=1653671067340&tc.endBound=1653672867340&tc.timeSystem=utc&view=grid' }*/), | ||||
|             page.locator('text=OK').click() | ||||
|         ]); | ||||
|  | ||||
|         // has been deleted | ||||
|         expect.soft(await restrictedNotebookTreeObject.count()).toEqual(0); | ||||
|     }); | ||||
|  | ||||
|     test('Can be locked if at least one page has one entry', async ({ page }) => { | ||||
|  | ||||
|         await enterTextEntry(page); | ||||
|  | ||||
|         const commitButton = page.locator(COMMIT_BUTTON_TEXT); | ||||
|         expect.soft(await commitButton.count()).toEqual(1); | ||||
|     }); | ||||
|  | ||||
| }); | ||||
|  | ||||
| test.describe('Restricted Notebook with at least one entry and with the page locked', () => { | ||||
|  | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         await startAndAddNotebookObject(page); | ||||
|         await enterTextEntry(page); | ||||
|         await lockPage(page); | ||||
|  | ||||
|         // open sidebar | ||||
|         await page.locator('button.c-notebook__toggle-nav-button').click(); | ||||
|     }); | ||||
|  | ||||
|     test('Locked page should now be in a locked state', async ({ page }) => { | ||||
|         // 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.soft(menuOptions).not.toContainText('Remove'); | ||||
|  | ||||
|     }); | ||||
|  | ||||
|     test('Can still: add page, rename, add entry, delete unlocked pages', 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(COMMIT_BUTTON_TEXT); | ||||
|         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.soft(await deletedPageElement.count()).toEqual(0); | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| test.describe('Restricted Notebook with a page locked and with an embed', () => { | ||||
|  | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         await startAndAddNotebookObject(page); | ||||
|         await dragAndDropEmbed(page); | ||||
|     }); | ||||
|  | ||||
|     test('Allows embeds to be deleted if page unlocked', 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.soft(embedMenu).toContainText('Remove This Embed'); | ||||
|     }); | ||||
|  | ||||
|     test('Disallows embeds to be deleted if page locked', 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.soft(embedMenu).not.toContainText('Remove This Embed'); | ||||
|     }); | ||||
|  | ||||
| }); | ||||
							
								
								
									
										198
									
								
								e2e/tests/plugins/plot/autoscale.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										198
									
								
								e2e/tests/plugins/plot/autoscale.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. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* | ||||
| Testsuite for plot autoscale. | ||||
| */ | ||||
|  | ||||
| const { test: _test } = require('../../../fixtures.js'); | ||||
| const { expect } = require('@playwright/test'); | ||||
|  | ||||
| // create a new `test` API that will not append platform details to snapshot | ||||
| // file names, only for the tests in this file, so that the same snapshots will | ||||
| // be used for all platforms. | ||||
| const test = _test.extend({ | ||||
|     _autoSnapshotSuffix: [ | ||||
|         async ({}, use, testInfo) => { | ||||
|             testInfo.snapshotSuffix = ''; | ||||
|             await use(); | ||||
|         }, | ||||
|         { auto: true } | ||||
|     ] | ||||
| }); | ||||
|  | ||||
| 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. | ||||
|         await test.setTimeout(120 * 1000); | ||||
|  | ||||
|         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); | ||||
|  | ||||
|         const canvas = page.locator('canvas').nth(1); | ||||
|  | ||||
|         // Make sure that after turning off autoscale, the user selected range values start at the same values the plot had prior. | ||||
|         await Promise.all([ | ||||
|             testYTicks(page, ['-1.00', '-0.50', '0.00', '0.50', '1.00']), | ||||
|             new Promise(r => setTimeout(r, 100)) | ||||
|                 .then(() => canvas.screenshot()) | ||||
|                 .then(shot => expect(shot).toMatchSnapshot('autoscale-canvas-prepan.png', { maxDiffPixels: 40 })) | ||||
|         ]); | ||||
|  | ||||
|         await page.keyboard.down('Alt'); | ||||
|  | ||||
|         await canvas.dragTo(canvas, { | ||||
|             sourcePosition: { | ||||
|                 x: 200, | ||||
|                 y: 200 | ||||
|             }, | ||||
|             targetPosition: { | ||||
|                 x: 400, | ||||
|                 y: 400 | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         await page.keyboard.up('Alt'); | ||||
|  | ||||
|         // Ensure the drag worked. | ||||
|         await Promise.all([ | ||||
|             testYTicks(page, ['0.00', '0.50', '1.00', '1.50', '2.00']), | ||||
|             new Promise(r => setTimeout(r, 100)) | ||||
|                 .then(() => canvas.screenshot()) | ||||
|                 .then(shot => expect(shot).toMatchSnapshot('autoscale-canvas-panned.png', { maxDiffPixels: 40 })) | ||||
|         ]); | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| /** | ||||
|  * @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: 19 KiB | 
							
								
								
									
										312
									
								
								e2e/tests/plugins/plot/logPlot.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										312
									
								
								e2e/tests/plugins/plot/logPlot.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,312 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 }) => { | ||||
|         //This is necessary due to the size of the test suite. | ||||
|         await test.setTimeout(120 * 1000); | ||||
|  | ||||
|         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); | ||||
|         //await testLogPlotPixels(page); | ||||
|  | ||||
|         // FIXME: Get rid of the waitForTimeout() and lint warning exception. | ||||
|         // eslint-disable-next-line playwright/no-wait-for-timeout | ||||
|         await page.waitForTimeout(1 * 1000); | ||||
|  | ||||
|         // refresh page and wait for charts and ticks to load | ||||
|         await page.reload({ waitUntil: 'networkidle'}); | ||||
|         await page.waitForSelector('.gl-plot-chart-area'); | ||||
|         await page.waitForSelector('.gl-plot-y-tick-label'); | ||||
|  | ||||
|         // test log ticks hold up after refresh | ||||
|         await testLogTicks(page); | ||||
|         //await testLogPlotPixels(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) .form-row .c-form-row__controls .form-control .field input').click(); | ||||
|     await page.locator('div:nth-child(5) .form-row .c-form-row__controls .form-control .field input').fill('6'); | ||||
|  | ||||
|     await page.locator('div:nth-child(6) .form-row .c-form-row__controls .form-control .field input').click(); | ||||
|     await page.locator('div:nth-child(6) .form-row .c-form-row__controls .form-control .field input').fill('4'); | ||||
|  | ||||
|     await page.locator('div:nth-child(7) .form-row .c-form-row__controls .form-control .field input').click(); | ||||
|     await page.locator('div:nth-child(7) .form-row .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); | ||||
| } | ||||
							
								
								
									
										101
									
								
								e2e/tests/plugins/telemetryTable/telemetryTable.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								e2e/tests/plugins/telemetryTable/telemetryTable.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,101 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 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(); | ||||
|  | ||||
|         // 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 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(); | ||||
|  | ||||
|         // 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 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 = await page.locator('button.c-button.icon-pause'); | ||||
|         await pauseButton.click(); | ||||
|  | ||||
|         const tableWrapper = await page.locator('div.c-table-wrapper'); | ||||
|         await expect(tableWrapper).toHaveClass(/is-paused/); | ||||
|  | ||||
|         // Arbitrarily change end date to some time in the future | ||||
|         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.setUTCDate(endDate.getUTCDate() + 1); | ||||
|         endDate = endDate.toISOString().replace(/T.*/, ''); | ||||
|  | ||||
|         await endTimeInput.fill(''); | ||||
|         await endTimeInput.fill(endDate); | ||||
|         await page.keyboard.press('Enter'); | ||||
|  | ||||
|         await expect(tableWrapper).not.toHaveClass(/is-paused/); | ||||
|     }); | ||||
| }); | ||||
| @@ -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(); | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										22
									
								
								e2e/tests/recycled_storage.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								e2e/tests/recycled_storage.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| { | ||||
|   "cookies": [], | ||||
|   "origins": [ | ||||
|     { | ||||
|       "origin": "http://localhost:8080", | ||||
|       "localStorage": [ | ||||
|         { | ||||
|           "name": "tcHistory", | ||||
|           "value": "{\"utc\":[{\"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\":\"\"}],\"location\":\"ROOT\",\"persisted\":1652303756008,\"modified\":1652303756007},\"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}}" | ||||
|         }, | ||||
|         { | ||||
|           "name": "mct-tree-expanded", | ||||
|           "value": "[]" | ||||
|         } | ||||
|       ] | ||||
|     } | ||||
|   ] | ||||
| } | ||||
| @@ -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 }) => { | ||||
| 
 | ||||
| @@ -47,7 +47,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 +59,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); | ||||
| @@ -171,3 +173,36 @@ 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'); | ||||
| 
 | ||||
| }); | ||||
							
								
								
									
										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 | ||||
|                     } | ||||
|                 } | ||||
|             ] | ||||
|         }, | ||||
|   | ||||
| @@ -23,7 +23,7 @@ | ||||
| define([ | ||||
|     'uuid' | ||||
| ], function ( | ||||
|     uuid | ||||
|     { v4: uuid } | ||||
| ) { | ||||
|     function WorkerInterface(openmct) { | ||||
|         // eslint-disable-next-line no-undef | ||||
|   | ||||
| @@ -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) | ||||
|                         } | ||||
|                     }); | ||||
| @@ -126,7 +127,8 @@ | ||||
|                 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) | ||||
|             }); | ||||
|         } | ||||
| @@ -154,8 +156,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) { | ||||
|   | ||||
| @@ -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', | ||||
|   | ||||
| @@ -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> | ||||
|   | ||||
							
								
								
									
										73
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										73
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,90 +1,86 @@ | ||||
| { | ||||
|   "name": "openmct", | ||||
|   "version": "2.0.2-SNAPSHOT", | ||||
|   "version": "2.0.5", | ||||
|   "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", | ||||
|     "@percy/cli": "1.2.1", | ||||
|     "@percy/playwright": "1.0.4", | ||||
|     "@playwright/test": "1.21.1", | ||||
|     "@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", | ||||
|     "allure-playwright": "2.0.0-beta.15", | ||||
|     "babel-loader": "8.2.3", | ||||
|     "babel-plugin-istanbul": "6.1.1", | ||||
|     "comma-separated-values": "3.6.4", | ||||
|     "copy-webpack-plugin": "10.2.0", | ||||
|     "core-js": "3.21.1", | ||||
|     "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.13.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.0", | ||||
|     "eslint-plugin-you-dont-need-lodash-underscore": "6.12.0", | ||||
|     "eventemitter3": "1.2.0", | ||||
|     "exports-loader": "0.7.0", | ||||
|     "express": "4.13.1", | ||||
|     "file-saver": "2.0.5", | ||||
|     "git-rev-sync": "3.0.2", | ||||
|     "html2canvas": "1.4.1", | ||||
|     "imports-loader": "0.8.0", | ||||
|     "jasmine-core": "4.0.1", | ||||
|     "jasmine-core": "4.1.1", | ||||
|     "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-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.5.0", | ||||
|     "lighthouse": "9.6.1", | ||||
|     "location-bar": "3.0.1", | ||||
|     "lodash": "4.17.21", | ||||
|     "mini-css-extract-plugin": "2.6.0", | ||||
|     "moment": "2.29.1", | ||||
|     "moment-duration-format": "2.2.2", | ||||
|     "moment": "2.29.3", | ||||
|     "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", | ||||
|     "plotly.js-basic-dist": "2.12.0", | ||||
|     "plotly.js-gl2d-dist": "2.12.0", | ||||
|     "printj": "1.3.1", | ||||
|     "request": "2.88.2", | ||||
|     "resolve-url-loader": "4.0.0", | ||||
|     "sass": "1.49.9", | ||||
|     "resolve-url-loader": "5.0.0", | ||||
|     "sass": "1.52.2", | ||||
|     "sass-loader": "12.6.0", | ||||
|     "sinon": "13.0.1", | ||||
|     "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": "5.3.1", | ||||
|     "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 ./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", | ||||
| @@ -93,10 +89,12 @@ | ||||
|     "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: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:ci": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome smoke branding default condition timeConductor clock exampleImagery persistence performance", | ||||
|     "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-local.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:perf": "npx playwright test --config=e2e/playwright-performance.config.js", | ||||
|     "test:watch": "cross-env 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", | ||||
| @@ -112,6 +110,9 @@ | ||||
|   "engines": { | ||||
|     "node": ">=14.19.1" | ||||
|   }, | ||||
|   "overrides": { | ||||
|     "core-js": "3.21.1" | ||||
|   }, | ||||
|   "browserslist": [ | ||||
|     "Firefox ESR", | ||||
|     "not IE 11", | ||||
|   | ||||
							
								
								
									
										15
									
								
								src/MCT.js
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								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 | ||||
| ) { | ||||
|     /** | ||||
| @@ -236,14 +238,22 @@ define([ | ||||
|         this.priority = api.PriorityAPI; | ||||
|  | ||||
|         this.router = new ApplicationRouter(this); | ||||
|         this.faults = new api.FaultManagementAPI.default(this); | ||||
|         this.forms = new api.FormsAPI.default(this); | ||||
|  | ||||
|         this.branding = BrandingAPI.default; | ||||
|  | ||||
|         // Plugins that are installed by 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 | ||||
|          */ | ||||
|         this.annotation = new api.AnnotationAPI(this); | ||||
|  | ||||
|         // 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()); | ||||
| @@ -379,6 +389,7 @@ define([ | ||||
|     }; | ||||
|  | ||||
|     MCT.prototype.plugins = plugins; | ||||
|     MCT.prototype.components = components.default; | ||||
|  | ||||
|     return MCT; | ||||
| }); | ||||
|   | ||||
							
								
								
									
										275
									
								
								src/api/annotation/AnnotationAPI.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										275
									
								
								src/api/annotation/AnnotationAPI.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,275 @@ | ||||
| /***************************************************************************** | ||||
|  * 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: [], | ||||
|                 contentText, | ||||
|                 targets | ||||
|             }; | ||||
|             existingAnnotation = await this.create(annotationCreationArguments); | ||||
|         } | ||||
|  | ||||
|         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> | ||||
|  | ||||
| @@ -70,7 +66,7 @@ | ||||
|         </button> | ||||
|         <button | ||||
|             tabindex="0" | ||||
|             class="c-button" | ||||
|             class="c-button js-cancel-button" | ||||
|             @click="onDismiss" | ||||
|         > | ||||
|             {{ cancelLabel }} | ||||
| @@ -81,7 +77,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,46 @@ | ||||
| * 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" | ||||
|         @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 +76,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 +105,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 +159,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 +258,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); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -58,7 +58,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 }} | ||||
|   | ||||
| @@ -15,6 +15,7 @@ | ||||
|                 :key="action.name" | ||||
|                 :class="[action.cssClass, action.isDisabled ? 'disabled' : '']" | ||||
|                 :title="action.description" | ||||
|                 :data-testid="action.testId || false" | ||||
|                 @click="action.onItemClicked" | ||||
|                 @mouseover="toggleItemDescription(action)" | ||||
|                 @mouseleave="toggleItemDescription()" | ||||
| @@ -45,6 +46,7 @@ | ||||
|             :key="action.name" | ||||
|             :class="action.cssClass" | ||||
|             :title="action.description" | ||||
|             :data-testid="action.testId || false" | ||||
|             @click="action.onItemClicked" | ||||
|             @mouseover="toggleItemDescription(action)" | ||||
|             @mouseleave="toggleItemDescription()" | ||||
|   | ||||
| @@ -20,7 +20,7 @@ | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import uuid from 'uuid'; | ||||
| import { v4 as uuid } from 'uuid'; | ||||
|  | ||||
| class InMemorySearchProvider { | ||||
|     /** | ||||
| @@ -36,13 +36,13 @@ class InMemorySearchProvider { | ||||
|          */ | ||||
|         this.MAX_CONCURRENT_REQUESTS = 100; | ||||
|         /** | ||||
|         * If max results is not specified in query, use this as default. | ||||
|         */ | ||||
|          * If max results is not specified in query, use this as default. | ||||
|          */ | ||||
|         this.DEFAULT_MAX_RESULTS = 100; | ||||
|  | ||||
|         this.openmct = openmct; | ||||
|  | ||||
|         this.indexedIds = {}; | ||||
|         this.indexedCompositions = {}; | ||||
|         this.indexedTags = {}; | ||||
|         this.idsToIndex = []; | ||||
|         this.pendingIndex = {}; | ||||
|         this.pendingRequests = 0; | ||||
| @@ -51,14 +51,20 @@ class InMemorySearchProvider { | ||||
|         /** | ||||
|          * If we don't have SharedWorkers available (e.g., iOS) | ||||
|          */ | ||||
|         this.localIndexedItems = {}; | ||||
|         this.localIndexedDomainObjects = {}; | ||||
|         this.localIndexedAnnotationsByDomainObject = {}; | ||||
|         this.localIndexedAnnotationsByTag = {}; | ||||
|  | ||||
|         this.pendingQueries = {}; | ||||
|         this.onWorkerMessage = this.onWorkerMessage.bind(this); | ||||
|         this.onWorkerMessageError = this.onWorkerMessageError.bind(this); | ||||
|         this.localSearchForObjects = this.localSearchForObjects.bind(this); | ||||
|         this.localSearchForAnnotations = this.localSearchForAnnotations.bind(this); | ||||
|         this.localSearchForTags = this.localSearchForTags.bind(this); | ||||
|         this.localSearchForNotebookAnnotations = this.localSearchForNotebookAnnotations.bind(this); | ||||
|         this.onAnnotationCreation = this.onAnnotationCreation.bind(this); | ||||
|         this.onerror = this.onWorkerError.bind(this); | ||||
|         this.startIndexing = this.startIndexing.bind(this); | ||||
|         this.onMutationOfIndexedObject = this.onMutationOfIndexedObject.bind(this); | ||||
|  | ||||
|         this.openmct.on('start', this.startIndexing); | ||||
|         this.openmct.on('destroy', () => { | ||||
| @@ -68,18 +74,47 @@ class InMemorySearchProvider { | ||||
|                 this.worker.port.onmessageerror = null; | ||||
|                 this.worker.port.close(); | ||||
|             } | ||||
|  | ||||
|             this.destroyObservers(this.indexedIds); | ||||
|             this.destroyObservers(this.indexedCompositions); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     startIndexing() { | ||||
|         const rootObject = this.openmct.objects.rootProvider.rootObject; | ||||
|  | ||||
|         this.searchTypes = this.openmct.objects.SEARCH_TYPES; | ||||
|  | ||||
|         this.supportedSearchTypes = [this.searchTypes.OBJECTS, this.searchTypes.ANNOTATIONS, this.searchTypes.NOTEBOOK_ANNOTATIONS, this.searchTypes.TAGS]; | ||||
|  | ||||
|         this.scheduleForIndexing(rootObject.identifier); | ||||
|  | ||||
|         this.indexAnnotations(); | ||||
|  | ||||
|         if (typeof SharedWorker !== 'undefined') { | ||||
|             this.worker = this.startSharedWorker(); | ||||
|         } else { | ||||
|             // we must be on iOS | ||||
|         } | ||||
|  | ||||
|         this.openmct.annotation.on('annotationCreated', this.onAnnotationCreation); | ||||
|  | ||||
|     } | ||||
|  | ||||
|     indexAnnotations() { | ||||
|         const theInMemorySearchProvider = this; | ||||
|         Object.values(this.openmct.objects.providers).forEach(objectProvider => { | ||||
|             if (objectProvider.getAllObjects) { | ||||
|                 const allObjects = objectProvider.getAllObjects(); | ||||
|                 if (allObjects) { | ||||
|                     Object.values(allObjects).forEach(domainObject => { | ||||
|                         if (domainObject.type === 'annotation') { | ||||
|                             theInMemorySearchProvider.scheduleForIndexing(domainObject.identifier); | ||||
|                         } | ||||
|                     }); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -95,51 +130,60 @@ class InMemorySearchProvider { | ||||
|         return intermediateResponse; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Query the search provider for results. | ||||
|      * | ||||
|      * @param {String} input the string to search by. | ||||
|      * @param {Number} maxResults max number of results to return. | ||||
|      * @returns {Promise} a promise for a modelResults object. | ||||
|      */ | ||||
|     query(input, maxResults) { | ||||
|         if (!maxResults) { | ||||
|             maxResults = this.DEFAULT_MAX_RESULTS; | ||||
|         } | ||||
|  | ||||
|     search(query, searchType) { | ||||
|         const queryId = uuid(); | ||||
|         const pendingQuery = this.getIntermediateResponse(); | ||||
|         this.pendingQueries[queryId] = pendingQuery; | ||||
|         const searchOptions = { | ||||
|             queryId, | ||||
|             searchType, | ||||
|             query, | ||||
|             maxResults: this.DEFAULT_MAX_RESULTS | ||||
|         }; | ||||
|  | ||||
|         if (this.worker) { | ||||
|             this.dispatchSearch(queryId, input, maxResults); | ||||
|             this.#dispatchSearchToWorker(searchOptions); | ||||
|         } else { | ||||
|             this.localSearch(queryId, input, maxResults); | ||||
|             this.#localQueryFallBack(searchOptions); | ||||
|         } | ||||
|  | ||||
|         return pendingQuery.promise; | ||||
|     } | ||||
|  | ||||
|     #localQueryFallBack({queryId, searchType, query, maxResults}) { | ||||
|         if (searchType === this.searchTypes.OBJECTS) { | ||||
|             return this.localSearchForObjects(queryId, query, maxResults); | ||||
|         } else if (searchType === this.searchTypes.ANNOTATIONS) { | ||||
|             return this.localSearchForAnnotations(queryId, query, maxResults); | ||||
|         } else if (searchType === this.searchTypes.NOTEBOOK_ANNOTATIONS) { | ||||
|             return this.localSearchForNotebookAnnotations(queryId, query, maxResults); | ||||
|         } else if (searchType === this.searchTypes.TAGS) { | ||||
|             return this.localSearchForTags(queryId, query, maxResults); | ||||
|         } else { | ||||
|             throw new Error(`🤷♂️ Unknown search type passed: ${searchType}`); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     supportsSearchType(searchType) { | ||||
|         return this.supportedSearchTypes.includes(searchType); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Handle messages from the worker.  Only really knows how to handle search | ||||
|      * results, which are parsed, transformed into a modelResult object, which | ||||
|      * is used to resolve the corresponding promise. | ||||
|      * Handle messages from the worker. | ||||
|      * @private | ||||
|      */ | ||||
|     async onWorkerMessage(event) { | ||||
|         if (event.data.request !== 'search') { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         const pendingQuery = this.pendingQueries[event.data.queryId]; | ||||
|         const modelResults = { | ||||
|             total: event.data.total | ||||
|         }; | ||||
|         modelResults.hits = await Promise.all(event.data.results.map(async (hit) => { | ||||
|             const identifier = this.openmct.objects.parseKeyString(hit.keyString); | ||||
|             const domainObject = await this.openmct.objects.get(identifier.key); | ||||
|             if (hit && hit.keyString) { | ||||
|                 const identifier = this.openmct.objects.parseKeyString(hit.keyString); | ||||
|                 const domainObject = await this.openmct.objects.get(identifier); | ||||
|  | ||||
|             return domainObject; | ||||
|                 return domainObject; | ||||
|             } | ||||
|         })); | ||||
|  | ||||
|         pendingQuery.resolve(modelResults); | ||||
| @@ -213,29 +257,72 @@ class InMemorySearchProvider { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     onMutationOfIndexedObject(domainObject) { | ||||
|     onAnnotationCreation(annotationObject) { | ||||
|         const provider = this; | ||||
|         provider.index(domainObject.identifier, domainObject); | ||||
|         provider.index(annotationObject); | ||||
|     } | ||||
|  | ||||
|     onNameMutation(domainObject, name) { | ||||
|         const provider = this; | ||||
|  | ||||
|         domainObject.name = name; | ||||
|         provider.index(domainObject); | ||||
|     } | ||||
|  | ||||
|     onTagMutation(domainObject, newTags) { | ||||
|         domainObject.oldTags = domainObject.tags; | ||||
|         domainObject.tags = newTags; | ||||
|         const provider = this; | ||||
|  | ||||
|         provider.index(domainObject); | ||||
|     } | ||||
|  | ||||
|     onCompositionMutation(domainObject, composition) { | ||||
|         const provider = this; | ||||
|         const indexedComposition = domainObject.composition; | ||||
|         const identifiersToIndex = composition | ||||
|             .filter(identifier => !indexedComposition | ||||
|                 .some(indexedIdentifier => this.openmct.objects | ||||
|                     .areIdsEqual([identifier, indexedIdentifier]))); | ||||
|  | ||||
|         identifiersToIndex.forEach(identifier => { | ||||
|             this.openmct.objects.get(identifier).then(objectToIndex => provider.index(objectToIndex)); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Pass an id and model to the worker to be indexed.  If the model has | ||||
|      * composition, schedule those ids for later indexing. | ||||
|      * Pass a domainObject to the worker to be indexed. | ||||
|      * If the object has composition, schedule those ids for later indexing. | ||||
|      * Watch for object changes and re-index object and children if so | ||||
|      * | ||||
|      * @private | ||||
|      * @param id a model id | ||||
|      * @param model a model | ||||
|      * @param domainObject a domainObject | ||||
|      */ | ||||
|     async index(id, domainObject) { | ||||
|     async index(domainObject) { | ||||
|         const provider = this; | ||||
|         const keyString = this.openmct.objects.makeKeyString(id); | ||||
|         const keyString = this.openmct.objects.makeKeyString(domainObject.identifier); | ||||
|  | ||||
|         if (!this.indexedIds[keyString]) { | ||||
|             this.openmct.objects.observe(domainObject, `*`, this.onMutationOfIndexedObject); | ||||
|             this.indexedIds[keyString] = this.openmct.objects.observe( | ||||
|                 domainObject, | ||||
|                 'name', | ||||
|                 this.onNameMutation.bind(this, domainObject) | ||||
|             ); | ||||
|             this.indexedCompositions[keyString] = this.openmct.objects.observe( | ||||
|                 domainObject, | ||||
|                 'composition', | ||||
|                 this.onCompositionMutation.bind(this, domainObject) | ||||
|             ); | ||||
|             if (domainObject.type === 'annotation') { | ||||
|                 this.indexedTags[keyString] = this.openmct.objects.observe( | ||||
|                     domainObject, | ||||
|                     'tags', | ||||
|                     this.onTagMutation.bind(this, domainObject) | ||||
|                 ); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         this.indexedIds[keyString] = true; | ||||
|  | ||||
|         if ((id.key !== 'ROOT')) { | ||||
|         if ((keyString !== 'ROOT')) { | ||||
|             if (this.worker) { | ||||
|                 this.worker.port.postMessage({ | ||||
|                     request: 'index', | ||||
| @@ -247,15 +334,12 @@ class InMemorySearchProvider { | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         const composition = this.openmct.composition.registry.find(foundComposition => { | ||||
|             return foundComposition.appliesTo(domainObject); | ||||
|         }); | ||||
|         const composition = this.openmct.composition.get(domainObject); | ||||
|  | ||||
|         if (composition) { | ||||
|             const childIdentifiers = await composition.load(domainObject); | ||||
|             childIdentifiers.forEach(function (childIdentifier) { | ||||
|                 provider.scheduleForIndexing(childIdentifier); | ||||
|             }); | ||||
|         if (composition !== undefined) { | ||||
|             const children = await composition.load(); | ||||
|  | ||||
|             children.forEach(child => provider.scheduleForIndexing(child.identifier)); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -271,12 +355,12 @@ class InMemorySearchProvider { | ||||
|         const provider = this; | ||||
|  | ||||
|         this.pendingRequests += 1; | ||||
|         const identifier = await this.openmct.objects.parseKeyString(keyString); | ||||
|         const domainObject = await this.openmct.objects.get(identifier.key); | ||||
|         const domainObject = await this.openmct.objects.get(keyString); | ||||
|         delete provider.pendingIndex[keyString]; | ||||
|  | ||||
|         try { | ||||
|             if (domainObject) { | ||||
|                 await provider.index(identifier, domainObject); | ||||
|                 await provider.index(domainObject); | ||||
|             } | ||||
|         } catch (error) { | ||||
|             console.warn('Failed to index domain object ' + keyString, error); | ||||
| @@ -294,26 +378,87 @@ class InMemorySearchProvider { | ||||
|      * @private | ||||
|      * @returns {String} a unique query Id for the query. | ||||
|      */ | ||||
|     dispatchSearch(queryId, searchInput, maxResults) { | ||||
|     #dispatchSearchToWorker({queryId, searchType, query, maxResults}) { | ||||
|         const message = { | ||||
|             request: 'search', | ||||
|             input: searchInput, | ||||
|             request: searchType.toString(), | ||||
|             input: query, | ||||
|             maxResults, | ||||
|             queryId | ||||
|         }; | ||||
|         this.worker.port.postMessage(message); | ||||
|     } | ||||
|  | ||||
|     localIndexTags(keyString, objectToIndex, model) { | ||||
|         // add new tags | ||||
|         model.tags.forEach(tagID => { | ||||
|             if (!this.localIndexedAnnotationsByTag[tagID]) { | ||||
|                 this.localIndexedAnnotationsByTag[tagID] = []; | ||||
|             } | ||||
|  | ||||
|             const existsInIndex = this.localIndexedAnnotationsByTag[tagID].some(indexedObject => { | ||||
|                 return indexedObject.keyString === objectToIndex.keyString; | ||||
|             }); | ||||
|  | ||||
|             if (!existsInIndex) { | ||||
|                 this.localIndexedAnnotationsByTag[tagID].push(objectToIndex); | ||||
|             } | ||||
|  | ||||
|         }); | ||||
|         // remove old tags | ||||
|         if (model.oldTags) { | ||||
|             model.oldTags.forEach(tagIDToRemove => { | ||||
|                 const existsInNewModel = model.tags.includes(tagIDToRemove); | ||||
|                 if (!existsInNewModel && this.localIndexedAnnotationsByTag[tagIDToRemove]) { | ||||
|                     this.localIndexedAnnotationsByTag[tagIDToRemove] = this.localIndexedAnnotationsByTag[tagIDToRemove]. | ||||
|                         filter(annotationToRemove => { | ||||
|                             const shouldKeep = annotationToRemove.keyString !== keyString; | ||||
|  | ||||
|                             return shouldKeep; | ||||
|                         }); | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     localIndexAnnotation(objectToIndex, model) { | ||||
|         Object.keys(model.targets).forEach(targetID => { | ||||
|             if (!this.localIndexedAnnotationsByDomainObject[targetID]) { | ||||
|                 this.localIndexedAnnotationsByDomainObject[targetID] = []; | ||||
|             } | ||||
|  | ||||
|             objectToIndex.targets = model.targets; | ||||
|             objectToIndex.tags = model.tags; | ||||
|             const existsInIndex = this.localIndexedAnnotationsByDomainObject[targetID].some(indexedObject => { | ||||
|                 return indexedObject.keyString === objectToIndex.keyString; | ||||
|             }); | ||||
|  | ||||
|             if (!existsInIndex) { | ||||
|                 this.localIndexedAnnotationsByDomainObject[targetID].push(objectToIndex); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|     * A local version of the same SharedWorker function | ||||
|     * if we don't have SharedWorkers available (e.g., iOS) | ||||
|     */ | ||||
|      * A local version of the same SharedWorker function | ||||
|      * if we don't have SharedWorkers available (e.g., iOS) | ||||
|      */ | ||||
|     localIndexItem(keyString, model) { | ||||
|         this.localIndexedItems[keyString] = { | ||||
|         const objectToIndex = { | ||||
|             type: model.type, | ||||
|             name: model.name, | ||||
|             keyString | ||||
|         }; | ||||
|         if (model && (model.type === 'annotation')) { | ||||
|             if (model.targets && model.targets) { | ||||
|                 this.localIndexAnnotation(objectToIndex, model); | ||||
|             } | ||||
|  | ||||
|             if (model.tags) { | ||||
|                 this.localIndexTags(keyString, objectToIndex, model); | ||||
|             } | ||||
|         } else { | ||||
|             this.localIndexedDomainObjects[keyString] = objectToIndex; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -323,21 +468,21 @@ class InMemorySearchProvider { | ||||
|      * Gets search results from the indexedItems based on provided search | ||||
|      * input. Returns matching results from indexedItems | ||||
|      */ | ||||
|     localSearch(queryId, searchInput, maxResults) { | ||||
|     localSearchForObjects(queryId, searchInput, maxResults) { | ||||
|         // This results dictionary will have domain object ID keys which | ||||
|         // point to the value the domain object's score. | ||||
|         let results; | ||||
|         let results = []; | ||||
|         const input = searchInput.trim().toLowerCase(); | ||||
|         const message = { | ||||
|             request: 'search', | ||||
|             results: {}, | ||||
|             request: 'searchForObjects', | ||||
|             results: [], | ||||
|             total: 0, | ||||
|             queryId | ||||
|         }; | ||||
|  | ||||
|         results = Object.values(this.localIndexedItems).filter((indexedItem) => { | ||||
|         results = Object.values(this.localIndexedDomainObjects).filter((indexedItem) => { | ||||
|             return indexedItem.name.toLowerCase().includes(input); | ||||
|         }); | ||||
|         }) || []; | ||||
|  | ||||
|         message.total = results.length; | ||||
|         message.results = results | ||||
| @@ -347,6 +492,117 @@ class InMemorySearchProvider { | ||||
|         }; | ||||
|         this.onWorkerMessage(eventToReturn); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * A local version of the same SharedWorker function | ||||
|      * if we don't have SharedWorkers available (e.g., iOS) | ||||
|      */ | ||||
|     localSearchForAnnotations(queryId, searchInput, maxResults) { | ||||
|         // This results dictionary will have domain object ID keys which | ||||
|         // point to the value the domain object's score. | ||||
|         let results = []; | ||||
|         const message = { | ||||
|             request: 'searchForAnnotations', | ||||
|             results: [], | ||||
|             total: 0, | ||||
|             queryId | ||||
|         }; | ||||
|  | ||||
|         results = this.localIndexedAnnotationsByDomainObject[searchInput] || []; | ||||
|  | ||||
|         message.total = results.length; | ||||
|         message.results = results | ||||
|             .slice(0, maxResults); | ||||
|         const eventToReturn = { | ||||
|             data: message | ||||
|         }; | ||||
|         this.onWorkerMessage(eventToReturn); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * A local version of the same SharedWorker function | ||||
|      * if we don't have SharedWorkers available (e.g., iOS) | ||||
|      */ | ||||
|     localSearchForTags(queryId, matchingTagKeys, maxResults) { | ||||
|         let results = []; | ||||
|         const message = { | ||||
|             request: 'searchForTags', | ||||
|             results: [], | ||||
|             total: 0, | ||||
|             queryId | ||||
|         }; | ||||
|  | ||||
|         if (matchingTagKeys) { | ||||
|             matchingTagKeys.forEach(matchingTag => { | ||||
|                 const matchingAnnotations = this.localIndexedAnnotationsByTag[matchingTag]; | ||||
|                 if (matchingAnnotations) { | ||||
|                     matchingAnnotations.forEach(matchingAnnotation => { | ||||
|                         const existsInResults = results.some(indexedObject => { | ||||
|                             return matchingAnnotation.keyString === indexedObject.keyString; | ||||
|                         }); | ||||
|                         if (!existsInResults) { | ||||
|                             results.push(matchingAnnotation); | ||||
|                         } | ||||
|                     }); | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         message.total = results.length; | ||||
|         message.results = results | ||||
|             .slice(0, maxResults); | ||||
|         const eventToReturn = { | ||||
|             data: message | ||||
|         }; | ||||
|         this.onWorkerMessage(eventToReturn); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * A local version of the same SharedWorker function | ||||
|      * if we don't have SharedWorkers available (e.g., iOS) | ||||
|      */ | ||||
|     localSearchForNotebookAnnotations(queryId, {entryId, targetKeyString}, maxResults) { | ||||
|         // This results dictionary will have domain object ID keys which | ||||
|         // point to the value the domain object's score. | ||||
|         let results = []; | ||||
|         const message = { | ||||
|             request: 'searchForNotebookAnnotations', | ||||
|             results: [], | ||||
|             total: 0, | ||||
|             queryId | ||||
|         }; | ||||
|  | ||||
|         const matchingAnnotations = this.localIndexedAnnotationsByDomainObject[targetKeyString]; | ||||
|         if (matchingAnnotations) { | ||||
|             results = matchingAnnotations.filter(matchingAnnotation => { | ||||
|                 if (!matchingAnnotation.targets) { | ||||
|                     return false; | ||||
|                 } | ||||
|  | ||||
|                 const target = matchingAnnotation.targets[targetKeyString]; | ||||
|  | ||||
|                 return (target && target.entryId && (target.entryId === entryId)); | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         message.total = results.length; | ||||
|         message.results = results | ||||
|             .slice(0, maxResults); | ||||
|         const eventToReturn = { | ||||
|             data: message | ||||
|         }; | ||||
|         this.onWorkerMessage(eventToReturn); | ||||
|     } | ||||
|  | ||||
|     destroyObservers(observers) { | ||||
|         Object.entries(observers).forEach(([keyString, unobserve]) => { | ||||
|             if (typeof unobserve === 'function') { | ||||
|                 unobserve(); | ||||
|             } | ||||
|  | ||||
|             delete observers[keyString]; | ||||
|         }); | ||||
|     } | ||||
| } | ||||
|  | ||||
| export default InMemorySearchProvider; | ||||
|   | ||||
| @@ -26,16 +26,27 @@ | ||||
| (function () { | ||||
|     // An object composed of domain object IDs and models | ||||
|     // {id: domainObject's ID, name: domainObject's name} | ||||
|     const indexedItems = {}; | ||||
|     const indexedDomainObjects = {}; | ||||
|     const indexedAnnotationsByDomainObject = {}; | ||||
|     const indexedAnnotationsByTag = {}; | ||||
|  | ||||
|     self.onconnect = function (e) { | ||||
|         const port = e.ports[0]; | ||||
|  | ||||
|         port.onmessage = function (event) { | ||||
|             if (event.data.request === 'index') { | ||||
|             const requestType = event.data.request; | ||||
|             if (requestType === 'index') { | ||||
|                 indexItem(event.data.keyString, event.data.model); | ||||
|             } else if (event.data.request === 'search') { | ||||
|                 port.postMessage(search(event.data)); | ||||
|             } else if (requestType === 'OBJECTS') { | ||||
|                 port.postMessage(searchForObjects(event.data)); | ||||
|             } else if (requestType === 'ANNOTATIONS') { | ||||
|                 port.postMessage(searchForAnnotations(event.data)); | ||||
|             } else if (requestType === 'TAGS') { | ||||
|                 port.postMessage(searchForTags(event.data)); | ||||
|             } else if (requestType === 'NOTEBOOK_ANNOTATIONS') { | ||||
|                 port.postMessage(searchForNotebookAnnotations(event.data)); | ||||
|             } else { | ||||
|                 throw new Error(`Unknown request ${event.data.request}`); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
| @@ -48,12 +59,73 @@ | ||||
|         console.error('Error on feed', error); | ||||
|     }; | ||||
|  | ||||
|     function indexAnnotation(objectToIndex, model) { | ||||
|         Object.keys(model.targets).forEach(targetID => { | ||||
|             if (!indexedAnnotationsByDomainObject[targetID]) { | ||||
|                 indexedAnnotationsByDomainObject[targetID] = []; | ||||
|             } | ||||
|  | ||||
|             objectToIndex.targets = model.targets; | ||||
|             objectToIndex.tags = model.tags; | ||||
|             const existsInIndex = indexedAnnotationsByDomainObject[targetID].some(indexedObject => { | ||||
|                 return indexedObject.keyString === objectToIndex.keyString; | ||||
|             }); | ||||
|  | ||||
|             if (!existsInIndex) { | ||||
|                 indexedAnnotationsByDomainObject[targetID].push(objectToIndex); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     function indexTags(keyString, objectToIndex, model) { | ||||
|         // add new tags | ||||
|         model.tags.forEach(tagID => { | ||||
|             if (!indexedAnnotationsByTag[tagID]) { | ||||
|                 indexedAnnotationsByTag[tagID] = []; | ||||
|             } | ||||
|  | ||||
|             const existsInIndex = indexedAnnotationsByTag[tagID].some(indexedObject => { | ||||
|                 return indexedObject.keyString === objectToIndex.keyString; | ||||
|             }); | ||||
|  | ||||
|             if (!existsInIndex) { | ||||
|                 indexedAnnotationsByTag[tagID].push(objectToIndex); | ||||
|             } | ||||
|  | ||||
|         }); | ||||
|         // remove old tags | ||||
|         if (model.oldTags) { | ||||
|             model.oldTags.forEach(tagIDToRemove => { | ||||
|                 const existsInNewModel = model.tags.includes(tagIDToRemove); | ||||
|                 if (!existsInNewModel && indexedAnnotationsByTag[tagIDToRemove]) { | ||||
|                     indexedAnnotationsByTag[tagIDToRemove] = indexedAnnotationsByTag[tagIDToRemove]. | ||||
|                         filter(annotationToRemove => { | ||||
|                             const shouldKeep = annotationToRemove.keyString !== keyString; | ||||
|  | ||||
|                             return shouldKeep; | ||||
|                         }); | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function indexItem(keyString, model) { | ||||
|         indexedItems[keyString] = { | ||||
|         const objectToIndex = { | ||||
|             type: model.type, | ||||
|             name: model.name, | ||||
|             keyString | ||||
|         }; | ||||
|         if (model && (model.type === 'annotation')) { | ||||
|             if (model.targets && model.targets) { | ||||
|                 indexAnnotation(objectToIndex, model); | ||||
|             } | ||||
|  | ||||
|             if (model.tags) { | ||||
|                 indexTags(keyString, objectToIndex, model); | ||||
|             } | ||||
|         } else { | ||||
|             indexedDomainObjects[keyString] = objectToIndex; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -65,21 +137,98 @@ | ||||
|      *           * maxResults: The maximum number of search results desired | ||||
|      *           * queryId: an id identifying this query, will be returned. | ||||
|      */ | ||||
|     function search(data) { | ||||
|         // This results dictionary will have domain object ID keys which | ||||
|         // point to the value the domain object's score. | ||||
|         let results; | ||||
|     function searchForObjects(data) { | ||||
|         let results = []; | ||||
|         const input = data.input.trim().toLowerCase(); | ||||
|         const message = { | ||||
|             request: 'search', | ||||
|             request: 'searchForObjects', | ||||
|             results: [], | ||||
|             total: 0, | ||||
|             queryId: data.queryId | ||||
|         }; | ||||
|  | ||||
|         results = Object.values(indexedDomainObjects).filter((indexedItem) => { | ||||
|             return indexedItem.name.toLowerCase().includes(input); | ||||
|         }) || []; | ||||
|  | ||||
|         message.total = results.length; | ||||
|         message.results = results | ||||
|             .slice(0, data.maxResults); | ||||
|  | ||||
|         return message; | ||||
|     } | ||||
|  | ||||
|     function searchForAnnotations(data) { | ||||
|         let results = []; | ||||
|         const message = { | ||||
|             request: 'searchForAnnotations', | ||||
|             results: [], | ||||
|             total: 0, | ||||
|             queryId: data.queryId | ||||
|         }; | ||||
|  | ||||
|         results = indexedAnnotationsByDomainObject[data.input] || []; | ||||
|  | ||||
|         message.total = results.length; | ||||
|         message.results = results | ||||
|             .slice(0, data.maxResults); | ||||
|  | ||||
|         return message; | ||||
|     } | ||||
|  | ||||
|     function searchForTags(data) { | ||||
|         let results = []; | ||||
|         const message = { | ||||
|             request: 'searchForTags', | ||||
|             results: [], | ||||
|             total: 0, | ||||
|             queryId: data.queryId | ||||
|         }; | ||||
|  | ||||
|         if (data.input) { | ||||
|             data.input.forEach(matchingTag => { | ||||
|                 const matchingAnnotations = indexedAnnotationsByTag[matchingTag]; | ||||
|                 if (matchingAnnotations) { | ||||
|                     matchingAnnotations.forEach(matchingAnnotation => { | ||||
|                         const existsInResults = results.some(indexedObject => { | ||||
|                             return matchingAnnotation.keyString === indexedObject.keyString; | ||||
|                         }); | ||||
|                         if (!existsInResults) { | ||||
|                             results.push(matchingAnnotation); | ||||
|                         } | ||||
|                     }); | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         message.total = results.length; | ||||
|         message.results = results | ||||
|             .slice(0, data.maxResults); | ||||
|  | ||||
|         return message; | ||||
|     } | ||||
|  | ||||
|     function searchForNotebookAnnotations(data) { | ||||
|         let results = []; | ||||
|         const message = { | ||||
|             request: 'searchForNotebookAnnotations', | ||||
|             results: {}, | ||||
|             total: 0, | ||||
|             queryId: data.queryId | ||||
|         }; | ||||
|  | ||||
|         results = Object.values(indexedItems).filter((indexedItem) => { | ||||
|             return indexedItem.name.toLowerCase().includes(input); | ||||
|         }); | ||||
|         const matchingAnnotations = indexedAnnotationsByDomainObject[data.input.targetKeyString]; | ||||
|         if (matchingAnnotations) { | ||||
|             results = matchingAnnotations.filter(matchingAnnotation => { | ||||
|                 if (!matchingAnnotation.targets) { | ||||
|                     return false; | ||||
|                 } | ||||
|  | ||||
|                 const target = matchingAnnotation.targets[data.input.targetKeyString]; | ||||
|  | ||||
|                 return (target && target.entryId && (target.entryId === data.input.entryId)); | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         message.total = results.length; | ||||
|         message.results = results | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -17,13 +17,16 @@ describe("The Object API Search Function", () => { | ||||
|             openmct = createOpenMct(); | ||||
|  | ||||
|             mockObjectProvider = jasmine.createSpyObj("mock object provider", [ | ||||
|                 "search" | ||||
|                 "search", "supportsSearchType" | ||||
|             ]); | ||||
|             anotherMockObjectProvider = jasmine.createSpyObj("another mock object provider", [ | ||||
|                 "search" | ||||
|                 "search", "supportsSearchType" | ||||
|             ]); | ||||
|             openmct.objects.addProvider('objects', mockObjectProvider); | ||||
|             openmct.objects.addProvider('other-objects', anotherMockObjectProvider); | ||||
|             mockObjectProvider.supportsSearchType.and.callFake(() => { | ||||
|                 return true; | ||||
|             }); | ||||
|             mockObjectProvider.search.and.callFake(() => { | ||||
|                 return new Promise(resolve => { | ||||
|                     const mockProviderSearch = { | ||||
| @@ -38,6 +41,9 @@ describe("The Object API Search Function", () => { | ||||
|                     }, MOCK_PROVIDER_SEARCH_DELAY); | ||||
|                 }); | ||||
|             }); | ||||
|             anotherMockObjectProvider.supportsSearchType.and.callFake(() => { | ||||
|                 return true; | ||||
|             }); | ||||
|             anotherMockObjectProvider.search.and.callFake(() => { | ||||
|                 return new Promise(resolve => { | ||||
|                     const anotherMockProviderSearch = { | ||||
| @@ -105,13 +111,18 @@ describe("The Object API Search Function", () => { | ||||
|  | ||||
|         beforeEach((done) => { | ||||
|             openmct = createOpenMct(); | ||||
|             spyOn(openmct.objects.inMemorySearchProvider, "query").and.callThrough(); | ||||
|             spyOn(openmct.objects.inMemorySearchProvider, "localSearch").and.callThrough(); | ||||
|             const defaultObjectProvider = openmct.objects.getProvider({ | ||||
|                 key: '', | ||||
|                 namespace: '' | ||||
|             }); | ||||
|             openmct.objects.addProvider('foo', defaultObjectProvider); | ||||
|             spyOn(openmct.objects.inMemorySearchProvider, "search").and.callThrough(); | ||||
|             spyOn(openmct.objects.inMemorySearchProvider, "localSearchForObjects").and.callThrough(); | ||||
|  | ||||
|             openmct.on('start', async () => { | ||||
|                 mockIdentifier1 = { | ||||
|                     key: 'some-object', | ||||
|                     namespace: 'some-namespace' | ||||
|                     namespace: 'foo' | ||||
|                 }; | ||||
|                 mockDomainObject1 = { | ||||
|                     type: 'clock', | ||||
| @@ -120,7 +131,7 @@ describe("The Object API Search Function", () => { | ||||
|                 }; | ||||
|                 mockIdentifier2 = { | ||||
|                     key: 'some-other-object', | ||||
|                     namespace: 'some-namespace' | ||||
|                     namespace: 'foo' | ||||
|                 }; | ||||
|                 mockDomainObject2 = { | ||||
|                     type: 'clock', | ||||
| @@ -129,16 +140,16 @@ describe("The Object API Search Function", () => { | ||||
|                 }; | ||||
|                 mockIdentifier3 = { | ||||
|                     key: 'yet-another-object', | ||||
|                     namespace: 'some-namespace' | ||||
|                     namespace: 'foo' | ||||
|                 }; | ||||
|                 mockDomainObject3 = { | ||||
|                     type: 'clock', | ||||
|                     name: 'redBear', | ||||
|                     identifier: mockIdentifier3 | ||||
|                 }; | ||||
|                 await openmct.objects.inMemorySearchProvider.index(mockIdentifier1, mockDomainObject1); | ||||
|                 await openmct.objects.inMemorySearchProvider.index(mockIdentifier2, mockDomainObject2); | ||||
|                 await openmct.objects.inMemorySearchProvider.index(mockIdentifier3, mockDomainObject3); | ||||
|                 await openmct.objects.inMemorySearchProvider.index(mockDomainObject1); | ||||
|                 await openmct.objects.inMemorySearchProvider.index(mockDomainObject2); | ||||
|                 await openmct.objects.inMemorySearchProvider.index(mockDomainObject3); | ||||
|                 done(); | ||||
|             }); | ||||
|             openmct.startHeadless(); | ||||
| @@ -150,7 +161,7 @@ describe("The Object API Search Function", () => { | ||||
|  | ||||
|         it("can provide indexing without a provider", () => { | ||||
|             openmct.objects.search('foo'); | ||||
|             expect(openmct.objects.inMemorySearchProvider.query).toHaveBeenCalled(); | ||||
|             expect(openmct.objects.inMemorySearchProvider.search).toHaveBeenCalled(); | ||||
|         }); | ||||
|  | ||||
|         it("can do partial search", async () => { | ||||
| @@ -172,16 +183,22 @@ describe("The Object API Search Function", () => { | ||||
|         }); | ||||
|  | ||||
|         describe("Without Shared Workers", () => { | ||||
|             let sharedWorkerToRestore; | ||||
|             beforeEach(async () => { | ||||
|                 // use local worker | ||||
|                 sharedWorkerToRestore = openmct.objects.inMemorySearchProvider.worker; | ||||
|                 openmct.objects.inMemorySearchProvider.worker = null; | ||||
|                 // reindex locally | ||||
|                 await openmct.objects.inMemorySearchProvider.index(mockIdentifier1, mockDomainObject1); | ||||
|                 await openmct.objects.inMemorySearchProvider.index(mockIdentifier2, mockDomainObject2); | ||||
|                 await openmct.objects.inMemorySearchProvider.index(mockIdentifier3, mockDomainObject3); | ||||
|                 await openmct.objects.inMemorySearchProvider.index(mockDomainObject1); | ||||
|                 await openmct.objects.inMemorySearchProvider.index(mockDomainObject2); | ||||
|                 await openmct.objects.inMemorySearchProvider.index(mockDomainObject3); | ||||
|             }); | ||||
|             afterEach(() => { | ||||
|                 openmct.objects.inMemorySearchProvider.worker = sharedWorkerToRestore; | ||||
|             }); | ||||
|             it("calls local search", () => { | ||||
|                 openmct.objects.search('foo'); | ||||
|                 expect(openmct.objects.inMemorySearchProvider.localSearch).toHaveBeenCalled(); | ||||
|                 expect(openmct.objects.inMemorySearchProvider.localSearchForObjects).toHaveBeenCalled(); | ||||
|             }); | ||||
|  | ||||
|             it("can do partial search", async () => { | ||||
|   | ||||
| @@ -22,12 +22,14 @@ | ||||
|  | ||||
| export default class Transaction { | ||||
|     constructor(objectAPI) { | ||||
|         this.dirtyObjects = new Set(); | ||||
|         this.dirtyObjects = {}; | ||||
|         this.objectAPI = objectAPI; | ||||
|     } | ||||
|  | ||||
|     add(object) { | ||||
|         this.dirtyObjects.add(object); | ||||
|         const key = this.objectAPI.makeKeyString(object.identifier); | ||||
|  | ||||
|         this.dirtyObjects[key] = object; | ||||
|     } | ||||
|  | ||||
|     cancel() { | ||||
| @@ -37,7 +39,8 @@ export default class Transaction { | ||||
|     commit() { | ||||
|         const promiseArray = []; | ||||
|         const save = this.objectAPI.save.bind(this.objectAPI); | ||||
|         this.dirtyObjects.forEach(object => { | ||||
|  | ||||
|         Object.values(this.dirtyObjects).forEach(object => { | ||||
|             promiseArray.push(this.createDirtyObjectPromise(object, save)); | ||||
|         }); | ||||
|  | ||||
| @@ -48,7 +51,9 @@ export default class Transaction { | ||||
|         return new Promise((resolve, reject) => { | ||||
|             action(object) | ||||
|                 .then((success) => { | ||||
|                     this.dirtyObjects.delete(object); | ||||
|                     const key = this.objectAPI.makeKeyString(object.identifier); | ||||
|  | ||||
|                     delete this.dirtyObjects[key]; | ||||
|                     resolve(success); | ||||
|                 }) | ||||
|                 .catch(reject); | ||||
| @@ -57,7 +62,8 @@ export default class Transaction { | ||||
|  | ||||
|     getDirtyObject(identifier) { | ||||
|         let dirtyObject; | ||||
|         this.dirtyObjects.forEach(object => { | ||||
|  | ||||
|         Object.values(this.dirtyObjects).forEach(object => { | ||||
|             const areIdsEqual = this.objectAPI.areIdsEqual(object.identifier, identifier); | ||||
|             if (areIdsEqual) { | ||||
|                 dirtyObject = object; | ||||
| @@ -67,14 +73,11 @@ export default class Transaction { | ||||
|         return dirtyObject; | ||||
|     } | ||||
|  | ||||
|     start() { | ||||
|         this.dirtyObjects = new Set(); | ||||
|     } | ||||
|  | ||||
|     _clear() { | ||||
|         const promiseArray = []; | ||||
|         const refresh = this.objectAPI.refresh.bind(this.objectAPI); | ||||
|         this.dirtyObjects.forEach(object => { | ||||
|  | ||||
|         Object.values(this.dirtyObjects).forEach(object => { | ||||
|             promiseArray.push(this.createDirtyObjectPromise(object, refresh)); | ||||
|         }); | ||||
|  | ||||
|   | ||||
| @@ -34,24 +34,24 @@ describe("Transaction Class", () => { | ||||
|     }); | ||||
|  | ||||
|     it('has no dirty objects', () => { | ||||
|         expect(transaction.dirtyObjects.size).toEqual(0); | ||||
|         expect(Object.keys(transaction.dirtyObjects).length).toEqual(0); | ||||
|     }); | ||||
|  | ||||
|     it('add(), adds object to dirtyObjects', () => { | ||||
|         const mockDomainObjects = createMockDomainObjects(); | ||||
|         transaction.add(mockDomainObjects[0]); | ||||
|         expect(transaction.dirtyObjects.size).toEqual(1); | ||||
|         expect(Object.keys(transaction.dirtyObjects).length).toEqual(1); | ||||
|     }); | ||||
|  | ||||
|     it('cancel(), clears all dirtyObjects', (done) => { | ||||
|         const mockDomainObjects = createMockDomainObjects(3); | ||||
|         mockDomainObjects.forEach(transaction.add.bind(transaction)); | ||||
|  | ||||
|         expect(transaction.dirtyObjects.size).toEqual(3); | ||||
|         expect(Object.keys(transaction.dirtyObjects).length).toEqual(3); | ||||
|  | ||||
|         transaction.cancel() | ||||
|             .then(success => { | ||||
|                 expect(transaction.dirtyObjects.size).toEqual(0); | ||||
|                 expect(Object.keys(transaction.dirtyObjects).length).toEqual(0); | ||||
|             }).finally(done); | ||||
|     }); | ||||
|  | ||||
| @@ -59,12 +59,12 @@ describe("Transaction Class", () => { | ||||
|         const mockDomainObjects = createMockDomainObjects(3); | ||||
|         mockDomainObjects.forEach(transaction.add.bind(transaction)); | ||||
|  | ||||
|         expect(transaction.dirtyObjects.size).toEqual(3); | ||||
|         expect(Object.keys(transaction.dirtyObjects).length).toEqual(3); | ||||
|         spyOn(objectAPI, 'save').and.callThrough(); | ||||
|  | ||||
|         transaction.commit() | ||||
|             .then(success => { | ||||
|                 expect(transaction.dirtyObjects.size).toEqual(0); | ||||
|                 expect(Object.keys(transaction.dirtyObjects).length).toEqual(0); | ||||
|                 expect(objectAPI.save.calls.count()).toEqual(3); | ||||
|             }).finally(done); | ||||
|     }); | ||||
| @@ -73,7 +73,7 @@ describe("Transaction Class", () => { | ||||
|         const mockDomainObjects = createMockDomainObjects(); | ||||
|         transaction.add(mockDomainObjects[0]); | ||||
|  | ||||
|         expect(transaction.dirtyObjects.size).toEqual(1); | ||||
|         expect(Object.keys(transaction.dirtyObjects).length).toEqual(1); | ||||
|         const dirtyObject = transaction.getDirtyObject(mockDomainObjects[0].identifier); | ||||
|  | ||||
|         expect(dirtyObject).toEqual(mockDomainObjects[0]); | ||||
| @@ -82,7 +82,7 @@ describe("Transaction Class", () => { | ||||
|     it('getDirtyObject(), returns empty dirtyObject for no active transaction', () => { | ||||
|         const mockDomainObjects = createMockDomainObjects(); | ||||
|  | ||||
|         expect(transaction.dirtyObjects.size).toEqual(0); | ||||
|         expect(Object.keys(transaction.dirtyObjects).length).toEqual(0); | ||||
|         const dirtyObject = transaction.getDirtyObject(mockDomainObjects[0].identifier); | ||||
|  | ||||
|         expect(dirtyObject).toEqual(undefined); | ||||
|   | ||||
| @@ -7,6 +7,7 @@ | ||||
|     <div class="c-overlay__outer"> | ||||
|         <button | ||||
|             v-if="dismissable" | ||||
|             aria-label="Close" | ||||
|             class="c-click-icon c-overlay__close-button icon-x" | ||||
|             @click="destroy" | ||||
|         ></button> | ||||
|   | ||||
| @@ -512,7 +512,7 @@ define([ | ||||
|     TelemetryAPI.prototype.handleMissingRequestProvider = function (domainObject) { | ||||
|         this.noRequestProviderForAllObjects = this.requestProviders.every(requestProvider => { | ||||
|             const supportsRequest = requestProvider.supportsRequest.apply(requestProvider, arguments); | ||||
|             const hasRequestProvider = Object.hasOwn(requestProvider, 'request'); | ||||
|             const hasRequestProvider = Object.prototype.hasOwnProperty.call(requestProvider, 'request') && typeof requestProvider.request === 'function'; | ||||
|  | ||||
|             return supportsRequest && hasRequestProvider; | ||||
|         }); | ||||
|   | ||||
| @@ -22,11 +22,7 @@ | ||||
|  | ||||
| import _ from 'lodash'; | ||||
| import EventEmitter from 'EventEmitter'; | ||||
|  | ||||
| const ERRORS = { | ||||
|     TIMESYSTEM_KEY: 'All telemetry metadata must have a telemetry value with a key that matches the key of the active time system.', | ||||
|     LOADED: 'Telemetry Collection has already been loaded.' | ||||
| }; | ||||
| import { LOADED_ERROR, TIMESYSTEM_KEY_NOTIFICATION, TIMESYSTEM_KEY_WARNING } from './constants'; | ||||
|  | ||||
| /** Class representing a Telemetry Collection. */ | ||||
|  | ||||
| @@ -61,7 +57,7 @@ export class TelemetryCollection extends EventEmitter { | ||||
|      */ | ||||
|     load() { | ||||
|         if (this.loaded) { | ||||
|             this._error(ERRORS.LOADED); | ||||
|             this._error(LOADED_ERROR); | ||||
|         } | ||||
|  | ||||
|         this._setTimeSystem(this.openmct.time.timeSystem()); | ||||
| @@ -172,6 +168,7 @@ export class TelemetryCollection extends EventEmitter { | ||||
|      * @private | ||||
|      */ | ||||
|     _processNewTelemetry(telemetryData) { | ||||
|         performance.mark('tlm:process:start'); | ||||
|         if (telemetryData === undefined) { | ||||
|             return; | ||||
|         } | ||||
| @@ -266,6 +263,10 @@ export class TelemetryCollection extends EventEmitter { | ||||
|         this.lastBounds = bounds; | ||||
|  | ||||
|         if (isTick) { | ||||
|             if (this.timeKey === undefined) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             // need to check futureBuffer and need to check | ||||
|             // if anything has fallen out of bounds | ||||
|             let startIndex = 0; | ||||
| @@ -305,7 +306,6 @@ export class TelemetryCollection extends EventEmitter { | ||||
|             if (added.length > 0) { | ||||
|                 this.emit('add', added); | ||||
|             } | ||||
|  | ||||
|         } else { | ||||
|             // user bounds change, reset | ||||
|             this._reset(); | ||||
| @@ -325,12 +325,16 @@ export class TelemetryCollection extends EventEmitter { | ||||
|         let domains = this.metadata.valuesForHints(['domain']); | ||||
|         let domain = domains.find((d) => d.key === timeSystem.key); | ||||
|  | ||||
|         if (domain === undefined) { | ||||
|             this._error(ERRORS.TIMESYSTEM_KEY); | ||||
|         if (domain !== undefined) { | ||||
|             // timeKey is used to create a dummy datum used for sorting | ||||
|             this.timeKey = domain.source; | ||||
|         } else { | ||||
|             this.timeKey = undefined; | ||||
|  | ||||
|             this._warn(TIMESYSTEM_KEY_WARNING); | ||||
|             this.openmct.notifications.alert(TIMESYSTEM_KEY_NOTIFICATION); | ||||
|         } | ||||
|  | ||||
|         // timeKey is used to create a dummy datum used for sorting | ||||
|         this.timeKey = domain.source; // this defaults to key if no source is set | ||||
|         let metadataValue = this.metadata.value(timeSystem.key) || { format: timeSystem.key }; | ||||
|         let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue); | ||||
|  | ||||
| @@ -352,6 +356,7 @@ export class TelemetryCollection extends EventEmitter { | ||||
|      * @todo handle subscriptions more granually | ||||
|      */ | ||||
|     _reset() { | ||||
|         performance.mark('tlm:reset'); | ||||
|         this.boundedTelemetry = []; | ||||
|         this.futureBuffer = []; | ||||
|  | ||||
| @@ -400,4 +405,8 @@ export class TelemetryCollection extends EventEmitter { | ||||
|     _error(message) { | ||||
|         throw new Error(message); | ||||
|     } | ||||
|  | ||||
|     _warn(message) { | ||||
|         console.warn(message); | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										101
									
								
								src/api/telemetry/TelemetryCollectionSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								src/api/telemetry/TelemetryCollectionSpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,101 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 { TIMESYSTEM_KEY_WARNING } from './constants'; | ||||
|  | ||||
| describe('Telemetry Collection', () => { | ||||
|     let openmct; | ||||
|     let mockMetadataProvider; | ||||
|     let mockMetadata = {}; | ||||
|     let domainObject; | ||||
|  | ||||
|     beforeEach(done => { | ||||
|         openmct = createOpenMct(); | ||||
|         openmct.on('start', done); | ||||
|  | ||||
|         domainObject = { | ||||
|             identifier: { | ||||
|                 key: 'a', | ||||
|                 namespace: 'b' | ||||
|             }, | ||||
|             type: 'sample-type' | ||||
|         }; | ||||
|  | ||||
|         mockMetadataProvider = { | ||||
|             key: 'mockMetadataProvider', | ||||
|             supportsMetadata() { | ||||
|                 return true; | ||||
|             }, | ||||
|             getMetadata() { | ||||
|                 return mockMetadata; | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         openmct.telemetry.addProvider(mockMetadataProvider); | ||||
|         openmct.startHeadless(); | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         return resetApplicationState(); | ||||
|     }); | ||||
|  | ||||
|     it('Warns if telemetry metadata does not match the active timesystem', () => { | ||||
|         mockMetadata.values = [ | ||||
|             { | ||||
|                 key: 'foo', | ||||
|                 name: 'Bar', | ||||
|                 hints: { | ||||
|                     domain: 1 | ||||
|                 } | ||||
|             } | ||||
|         ]; | ||||
|  | ||||
|         const telemetryCollection = openmct.telemetry.requestCollection(domainObject); | ||||
|         spyOn(telemetryCollection, '_warn'); | ||||
|         telemetryCollection.load(); | ||||
|  | ||||
|         expect(telemetryCollection._warn).toHaveBeenCalledOnceWith(TIMESYSTEM_KEY_WARNING); | ||||
|     }); | ||||
|  | ||||
|     it('Does not warn if telemetry metadata matches the active timesystem', () => { | ||||
|         mockMetadata.values = [ | ||||
|             { | ||||
|                 key: 'utc', | ||||
|                 name: 'Timestamp', | ||||
|                 format: 'utc', | ||||
|                 hints: { | ||||
|                     domain: 1 | ||||
|                 } | ||||
|             } | ||||
|         ]; | ||||
|  | ||||
|         const telemetryCollection = openmct.telemetry.requestCollection(domainObject); | ||||
|         spyOn(telemetryCollection, '_warn'); | ||||
|         telemetryCollection.load(); | ||||
|  | ||||
|         expect(telemetryCollection._warn).not.toHaveBeenCalled(); | ||||
|     }); | ||||
| }); | ||||
| @@ -121,6 +121,18 @@ define([ | ||||
|         return _.sortBy(matchingMetadata, ...iteratees); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * check out of a given metadata has array values | ||||
|      */ | ||||
|     TelemetryMetadataManager.prototype.isArrayValue = function (metadata) { | ||||
|         const regex = /\[\]$/g; | ||||
|         if (!metadata.format && !metadata.formatString) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         return (metadata.format || metadata.formatString).match(regex) !== null; | ||||
|     }; | ||||
|  | ||||
|     TelemetryMetadataManager.prototype.getFilterableValues = function () { | ||||
|         return this.valueMetadatas.filter(metadatum => metadatum.filters && metadatum.filters.length > 0); | ||||
|     }; | ||||
| @@ -138,7 +150,7 @@ define([ | ||||
|             valueMetadata = this.values()[0]; | ||||
|         } | ||||
|  | ||||
|         return valueMetadata.key; | ||||
|         return valueMetadata; | ||||
|     }; | ||||
|  | ||||
|     return TelemetryMetadataManager; | ||||
|   | ||||
| @@ -43,9 +43,23 @@ define([ | ||||
|         }; | ||||
|  | ||||
|         this.valueMetadata = valueMetadata; | ||||
|         this.formatter = formatMap.get(valueMetadata.format) || numberFormatter; | ||||
|  | ||||
|         if (valueMetadata.format === 'enum') { | ||||
|         function getNonArrayValue(value) { | ||||
|             //metadata format could have array formats ex. string[]/number[] | ||||
|             const arrayRegex = /\[\]$/g; | ||||
|             if (value && value.match(arrayRegex)) { | ||||
|                 return value.replace(arrayRegex, ''); | ||||
|             } | ||||
|  | ||||
|             return value; | ||||
|         } | ||||
|  | ||||
|         let valueMetadataFormat = getNonArrayValue(valueMetadata.format); | ||||
|  | ||||
|         //Is there an existing formatter for the format specified? If not, default to number format | ||||
|         this.formatter = formatMap.get(valueMetadataFormat) || numberFormatter; | ||||
|  | ||||
|         if (valueMetadataFormat === 'enum') { | ||||
|             this.formatter = {}; | ||||
|             this.enumerations = valueMetadata.enumerations.reduce(function (vm, e) { | ||||
|                 vm.byValue[e.value] = e.string; | ||||
| @@ -77,13 +91,13 @@ define([ | ||||
|         // Check for formatString support once instead of per format call. | ||||
|         if (valueMetadata.formatString) { | ||||
|             const baseFormat = this.formatter.format; | ||||
|             const formatString = valueMetadata.formatString; | ||||
|             const formatString = getNonArrayValue(valueMetadata.formatString); | ||||
|             this.formatter.format = function (value) { | ||||
|                 return printj.sprintf(formatString, baseFormat.call(this, value)); | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         if (valueMetadata.format === 'string') { | ||||
|         if (valueMetadataFormat === 'string') { | ||||
|             this.formatter.parse = function (value) { | ||||
|                 if (value === undefined) { | ||||
|                     return ''; | ||||
| @@ -108,7 +122,14 @@ define([ | ||||
|  | ||||
|     TelemetryValueFormatter.prototype.parse = function (datum) { | ||||
|         if (_.isObject(datum)) { | ||||
|             return this.formatter.parse(datum[this.valueMetadata.source]); | ||||
|             const objectDatum = datum[this.valueMetadata.source]; | ||||
|             if (Array.isArray(objectDatum)) { | ||||
|                 return objectDatum.map((item) => { | ||||
|                     return this.formatter.parse(item); | ||||
|                 }); | ||||
|             } else { | ||||
|                 return this.formatter.parse(objectDatum); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return this.formatter.parse(datum); | ||||
| @@ -116,7 +137,14 @@ define([ | ||||
|  | ||||
|     TelemetryValueFormatter.prototype.format = function (datum) { | ||||
|         if (_.isObject(datum)) { | ||||
|             return this.formatter.format(datum[this.valueMetadata.source]); | ||||
|             const objectDatum = datum[this.valueMetadata.source]; | ||||
|             if (Array.isArray(objectDatum)) { | ||||
|                 return objectDatum.map((item) => { | ||||
|                     return this.formatter.format(item); | ||||
|                 }); | ||||
|             } else { | ||||
|                 return this.formatter.format(objectDatum); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return this.formatter.format(datum); | ||||
|   | ||||
							
								
								
									
										25
									
								
								src/api/telemetry/constants.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/api/telemetry/constants.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 const TIMESYSTEM_KEY_WARNING = 'All telemetry metadata must have a telemetry value with a key that matches the key of the active time system.'; | ||||
| export const TIMESYSTEM_KEY_NOTIFICATION = 'Telemetry metadata does not match the active time system.'; | ||||
| export const LOADED_ERROR = 'Telemetry Collection has already been loaded.'; | ||||
							
								
								
									
										295
									
								
								src/api/user/StatusAPI.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										295
									
								
								src/api/user/StatusAPI.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,295 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 EventEmitter from "EventEmitter"; | ||||
|  | ||||
| export default class StatusAPI extends EventEmitter { | ||||
|     #userAPI; | ||||
|     #openmct; | ||||
|  | ||||
|     constructor(userAPI, openmct) { | ||||
|         super(); | ||||
|         this.#userAPI = userAPI; | ||||
|         this.#openmct = openmct; | ||||
|  | ||||
|         this.onProviderStatusChange = this.onProviderStatusChange.bind(this); | ||||
|         this.onProviderPollQuestionChange = this.onProviderPollQuestionChange.bind(this); | ||||
|         this.listenToStatusEvents = this.listenToStatusEvents.bind(this); | ||||
|  | ||||
|         this.#openmct.once('destroy', () => { | ||||
|             const provider = this.#userAPI.getProvider(); | ||||
|  | ||||
|             if (typeof provider?.off === 'function') { | ||||
|                 provider.off('statusChange', this.onProviderStatusChange); | ||||
|                 provider.off('pollQuestionChange', this.onProviderPollQuestionChange); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         this.#userAPI.on('providerAdded', this.listenToStatusEvents); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Fetch the currently defined operator status poll question. When presented with a status poll question, all operators will reply with their current status. | ||||
|      * @returns {Promise<PollQuestion>} | ||||
|      */ | ||||
|     getPollQuestion() { | ||||
|         const provider = this.#userAPI.getProvider(); | ||||
|  | ||||
|         if (provider.getPollQuestion) { | ||||
|             return provider.getPollQuestion(); | ||||
|         } else { | ||||
|             this.#userAPI.error("User provider does not support polling questions"); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Set a poll question for operators to respond to. When presented with a status poll question, all operators will reply with their current status. | ||||
|      * @param {String} questionText - The text of the question | ||||
|      * @returns {Promise<Boolean>} true if operation was successful, otherwise false. | ||||
|      */ | ||||
|     async setPollQuestion(questionText) { | ||||
|         const canSetPollQuestion = await this.canSetPollQuestion(); | ||||
|  | ||||
|         if (canSetPollQuestion) { | ||||
|             const provider = this.#userAPI.getProvider(); | ||||
|  | ||||
|             const result = await provider.setPollQuestion(questionText); | ||||
|  | ||||
|             try { | ||||
|                 await this.resetAllStatuses(); | ||||
|             } catch (error) { | ||||
|                 console.warn("Poll question set but unable to clear operator statuses."); | ||||
|                 console.error(error); | ||||
|             } | ||||
|  | ||||
|             return result; | ||||
|         } else { | ||||
|             this.#userAPI.error("User provider does not support setting polling question"); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Can the currently logged in user set the operator status poll question. | ||||
|      * @returns {Promise<Boolean>} | ||||
|      */ | ||||
|     canSetPollQuestion() { | ||||
|         const provider = this.#userAPI.getProvider(); | ||||
|  | ||||
|         if (provider.canSetPollQuestion) { | ||||
|             return provider.canSetPollQuestion(); | ||||
|         } else { | ||||
|             return Promise.resolve(false); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @returns {Promise<Array<Status>>} the complete list of possible states that an operator can reply to a poll question with. | ||||
|      */ | ||||
|     async getPossibleStatuses() { | ||||
|         const provider = this.#userAPI.getProvider(); | ||||
|  | ||||
|         if (provider.getPossibleStatuses) { | ||||
|             const possibleStatuses = await provider.getPossibleStatuses() || []; | ||||
|  | ||||
|             return possibleStatuses.map(status => status); | ||||
|         } else { | ||||
|             this.#userAPI.error("User provider cannot provide statuses"); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @param {import("./UserAPI").Role} role The role to fetch the current status for. | ||||
|      * @returns {Promise<Status>} the current status of the provided role | ||||
|      */ | ||||
|     async getStatusForRole(role) { | ||||
|         const provider = this.#userAPI.getProvider(); | ||||
|  | ||||
|         if (provider.getStatusForRole) { | ||||
|             const status = await provider.getStatusForRole(role); | ||||
|  | ||||
|             return status; | ||||
|         } else { | ||||
|             this.#userAPI.error("User provider does not support role status"); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @param {import("./UserAPI").Role} role | ||||
|      * @returns {Promise<Boolean>} true if the configured UserProvider can provide status for the given role | ||||
|      * @see StatusUserProvider | ||||
|      */ | ||||
|     canProvideStatusForRole(role) { | ||||
|         const provider = this.#userAPI.getProvider(); | ||||
|  | ||||
|         if (provider.canProvideStatusForRole) { | ||||
|             return provider.canProvideStatusForRole(role); | ||||
|         } else { | ||||
|             return false; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @param {import("./UserAPI").Role} role The role to set the status for. | ||||
|      * @param {Status} status The status to set for the provided role | ||||
|      * @returns {Promise<Boolean>} true if operation was successful, otherwise false. | ||||
|      */ | ||||
|     setStatusForRole(role, status) { | ||||
|         const provider = this.#userAPI.getProvider(); | ||||
|  | ||||
|         if (provider.setStatusForRole) { | ||||
|             return provider.setStatusForRole(role, status); | ||||
|         } else { | ||||
|             this.#userAPI.error("User provider does not support setting role status"); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Resets the status of the provided role back to its default status. | ||||
|      * @param {import("./UserAPI").Role} role The role to set the status for. | ||||
|      * @returns {Promise<Boolean>} true if operation was successful, otherwise false. | ||||
|      */ | ||||
|     async resetStatusForRole(role) { | ||||
|         const provider = this.#userAPI.getProvider(); | ||||
|         const defaultStatus = await this.getDefaultStatus(); | ||||
|  | ||||
|         if (provider.setStatusForRole) { | ||||
|             return provider.setStatusForRole(role, defaultStatus); | ||||
|         } else { | ||||
|             this.#userAPI.error("User provider does not support resetting role status"); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Resets the status of all operators to their default status | ||||
|      * @returns {Promise<Boolean>} true if operation was successful, otherwise false. | ||||
|      */ | ||||
|     async resetAllStatuses() { | ||||
|         const allStatusRoles = await this.getAllStatusRoles(); | ||||
|  | ||||
|         return Promise.all(allStatusRoles.map(role => this.resetStatusForRole(role))); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * The default status. This is the status that will be used before the user has selected any status. | ||||
|      * @param {import("./UserAPI").Role} role | ||||
|      * @returns {Promise<Status>} the default operator status if no other has been set. | ||||
|      */ | ||||
|     async getDefaultStatusForRole(role) { | ||||
|         const provider = this.#userAPI.getProvider(); | ||||
|         const defaultStatus = await provider.getDefaultStatusForRole(role); | ||||
|  | ||||
|         return defaultStatus; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * All possible status roles. A status role is a user role that can provide status. In some systems | ||||
|      * this may be all user roles, but there may be cases where some users are not are not polled | ||||
|      * for status if they do not have a real-time operational role. | ||||
|      * | ||||
|      * @returns {Promise<Array<import("./UserAPI").Role>>} the default operator status if no other has been set. | ||||
|      */ | ||||
|     getAllStatusRoles() { | ||||
|         const provider = this.#userAPI.getProvider(); | ||||
|  | ||||
|         if (provider.getAllStatusRoles) { | ||||
|             return provider.getAllStatusRoles(); | ||||
|         } else { | ||||
|             this.#userAPI.error("User provider cannot provide all status roles"); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * The status role of the current user. A user may have multiple roles, but will only have one role | ||||
|      * that provides status at any time. | ||||
|      * @returns {Promise<import("./UserAPI").Role>} the role for which the current user can provide status. | ||||
|      */ | ||||
|     getStatusRoleForCurrentUser() { | ||||
|         const provider = this.#userAPI.getProvider(); | ||||
|  | ||||
|         if (provider.getStatusRoleForCurrentUser) { | ||||
|             return provider.getStatusRoleForCurrentUser(); | ||||
|         } else { | ||||
|             this.#userAPI.error("User provider cannot provide role status for this user"); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @returns {Promise<Boolean>} true if the configured UserProvider can provide status for the currently logged in user, false otherwise. | ||||
|      * @see StatusUserProvider | ||||
|      */ | ||||
|     async canProvideStatusForCurrentUser() { | ||||
|         const provider = this.#userAPI.getProvider(); | ||||
|  | ||||
|         if (provider.getStatusRoleForCurrentUser) { | ||||
|             const activeStatusRole = await this.#userAPI.getProvider().getStatusRoleForCurrentUser(); | ||||
|             const canProvideStatus = await this.canProvideStatusForRole(activeStatusRole); | ||||
|  | ||||
|             return canProvideStatus; | ||||
|         } else { | ||||
|             return false; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Private internal function that cannot be made #private because it needs to be registered as a callback to the user provider | ||||
|      * @private | ||||
|      */ | ||||
|     listenToStatusEvents(provider) { | ||||
|         if (typeof provider.on === 'function') { | ||||
|             provider.on('statusChange', this.onProviderStatusChange); | ||||
|             provider.on('pollQuestionChange', this.onProviderPollQuestionChange); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @private | ||||
|      */ | ||||
|     onProviderStatusChange(newStatus) { | ||||
|         this.emit('statusChange', newStatus); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @private | ||||
|      */ | ||||
|     onProviderPollQuestionChange(pollQuestion) { | ||||
|         this.emit('pollQuestionChange', pollQuestion); | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @typedef {import('./UserProvider')} UserProvider | ||||
|  */ | ||||
| /** | ||||
|  * @typedef {import('./StatusUserProvider')} StatusUserProvider | ||||
|  */ | ||||
| /** | ||||
|  * The PollQuestion type | ||||
|  * @typedef {Object} PollQuestion | ||||
|  * @property {String} question - The question to be presented to users | ||||
|  * @property {Number} timestamp - The time that the poll question was set. | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * The Status type | ||||
|  * @typedef {Object} Status | ||||
|  * @property {String} key - A unique identifier for this status | ||||
|  * @property {Number} label - A human readable label for this status | ||||
|  */ | ||||
							
								
								
									
										81
									
								
								src/api/user/StatusUserProvider.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								src/api/user/StatusUserProvider.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,81 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 UserProvider from "./UserProvider"; | ||||
|  | ||||
| export default class StatusUserProvider extends UserProvider { | ||||
|     /** | ||||
|      * @param {('statusChange'|'pollQuestionChange')} event the name of the event to listen to | ||||
|      * @param {Function} callback a function to invoke when this event occurs | ||||
|      */ | ||||
|     on(event, callback) {} | ||||
|     /** | ||||
|      * @param {('statusChange'|'pollQuestionChange')} event the name of the event to stop listen to | ||||
|      * @param {Function} callback the callback function used to register the listener | ||||
|      */ | ||||
|     off(event, callback) {} | ||||
|     /** | ||||
|      * @returns {import("./StatusAPI").PollQuestion} the current status poll question | ||||
|      */ | ||||
|     async getPollQuestion() {} | ||||
|     /** | ||||
|      * @param {import("./StatusAPI").PollQuestion} pollQuestion a new poll question to set | ||||
|      * @returns {Promise<Boolean>} true if operation was successful, otherwise false | ||||
|      */ | ||||
|     async setPollQuestion(pollQuestion) {} | ||||
|     /** | ||||
|      * @returns {Promise<Boolean>} true if the current user can set the poll question, otherwise false | ||||
|      */ | ||||
|     async canSetPollQuestion() {} | ||||
|     /** | ||||
|      * @returns {Promise<Array<import("./StatusAPI").Status>>} a list of the possible statuses that an operator can be in | ||||
|      */ | ||||
|     async getPossibleStatuses() {} | ||||
|     /** | ||||
|      * @param {import("./UserAPI").Role} role | ||||
|      * @returns {Promise<import("./StatusAPI").Status} | ||||
|      */ | ||||
|     async getStatusForRole(role) {} | ||||
|     /** | ||||
|      * @param {import("./UserAPI").Role} role | ||||
|      * @returns {Promise<import("./StatusAPI").Status} | ||||
|      */ | ||||
|     async getDefaultStatusForRole(role) {} | ||||
|     /** | ||||
|      * @param {import("./UserAPI").Role} role | ||||
|      * @param {*} status | ||||
|      * @returns {Promise<Boolean>} true if operation was successful, otherwise false. | ||||
|      */ | ||||
|     async setStatusForRole(role, status) {} | ||||
|     /** | ||||
|      * @param {import("./UserAPI").Role} role | ||||
|      * @returns {Promise<Boolean} true if the user provider can provide status for the given role | ||||
|      */ | ||||
|     async canProvideStatusForRole(role) {} | ||||
|     /** | ||||
|      * @returns {Promise<Array<import("./UserAPI").Role>>} a list of all available status roles, if user permissions allow it. | ||||
|      */ | ||||
|     async getAllStatusRoles() {} | ||||
|     /** | ||||
|      * @returns {Promise<import("./UserAPI").Role>} the active status role for the currently logged in user | ||||
|      */ | ||||
|     async getStatusRoleForCurrentUser() {} | ||||
| } | ||||
| @@ -25,16 +25,22 @@ import { | ||||
|     MULTIPLE_PROVIDER_ERROR, | ||||
|     NO_PROVIDER_ERROR | ||||
| } from './constants'; | ||||
| import StatusAPI from './StatusAPI'; | ||||
| import User from './User'; | ||||
|  | ||||
| class UserAPI extends EventEmitter { | ||||
|     constructor(openmct) { | ||||
|     /** | ||||
|      * @param {OpenMCT} openmct | ||||
|      * @param {UserAPIConfiguration} config | ||||
|      */ | ||||
|     constructor(openmct, config) { | ||||
|         super(); | ||||
|  | ||||
|         this._openmct = openmct; | ||||
|         this._provider = undefined; | ||||
|  | ||||
|         this.User = User; | ||||
|         this.status = new StatusAPI(this, openmct, config); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -47,14 +53,17 @@ class UserAPI extends EventEmitter { | ||||
|      */ | ||||
|     setProvider(provider) { | ||||
|         if (this.hasProvider()) { | ||||
|             this._error(MULTIPLE_PROVIDER_ERROR); | ||||
|             this.error(MULTIPLE_PROVIDER_ERROR); | ||||
|         } | ||||
|  | ||||
|         this._provider = provider; | ||||
|  | ||||
|         this.emit('providerAdded', this._provider); | ||||
|     } | ||||
|  | ||||
|     getProvider() { | ||||
|         return this._provider; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Return true if the user provider has been set. | ||||
|      * | ||||
| @@ -74,7 +83,7 @@ class UserAPI extends EventEmitter { | ||||
|      * @throws Will throw an error if no user provider is set | ||||
|      */ | ||||
|     getCurrentUser() { | ||||
|         this._noProviderCheck(); | ||||
|         this.noProviderCheck(); | ||||
|  | ||||
|         return this._provider.getCurrentUser(); | ||||
|     } | ||||
| @@ -105,7 +114,7 @@ class UserAPI extends EventEmitter { | ||||
|      * @throws Will throw an error if no user provider is set | ||||
|      */ | ||||
|     hasRole(roleId) { | ||||
|         this._noProviderCheck(); | ||||
|         this.noProviderCheck(); | ||||
|  | ||||
|         return this._provider.hasRole(roleId); | ||||
|     } | ||||
| @@ -116,9 +125,9 @@ class UserAPI extends EventEmitter { | ||||
|      * @private | ||||
|      * @throws Will throw an error if no user provider is set | ||||
|      */ | ||||
|     _noProviderCheck() { | ||||
|     noProviderCheck() { | ||||
|         if (!this.hasProvider()) { | ||||
|             this._error(NO_PROVIDER_ERROR); | ||||
|             this.error(NO_PROVIDER_ERROR); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -129,9 +138,26 @@ class UserAPI extends EventEmitter { | ||||
|      * @param {string} error description of error | ||||
|      * @throws Will throw error passed in | ||||
|      */ | ||||
|     _error(error) { | ||||
|     error(error) { | ||||
|         throw new Error(error); | ||||
|     } | ||||
| } | ||||
|  | ||||
| export default UserAPI; | ||||
| /** | ||||
|  * @typedef {String} Role | ||||
|  */ | ||||
| /** | ||||
|  * @typedef {Object} OpenMCT | ||||
|  */ | ||||
| /** | ||||
|  * @typedef {{statusStyles: Object.<string, StatusStyleDefinition>}} UserAPIConfiguration | ||||
|  */ | ||||
| /** | ||||
|  * @typedef {Object} StatusStyleDefinition | ||||
|  * @property {String} iconClass The icon class to apply to the status indicator when this status is active "icon-circle-slash", | ||||
|  * @property {String} iconClassPoll The icon class to apply to the poll question indicator when this style is active eg. "icon-status-poll-question-mark" | ||||
|  * @property {String} statusClass The class to apply to the indicator when this status is active eg. "s-status-error" | ||||
|  * @property {String} statusBgColor The background color to apply in the status summary section of the poll question popup for this status eg."#9900cc" | ||||
|  * @property {String} statusFgColor The foreground color to apply in the status summary section of the poll question popup for this status eg. "#fff" | ||||
|  */ | ||||
|   | ||||
| @@ -40,11 +40,13 @@ describe("The User API", () => { | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         const activeOverlays = openmct.overlays.activeOverlays; | ||||
|         activeOverlays.forEach(overlay => overlay.dismiss()); | ||||
|  | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     describe('with regard to user providers', () => { | ||||
|  | ||||
|         it('allows you to specify a user provider', () => { | ||||
|             openmct.user.on('providerAdded', (provider) => { | ||||
|                 expect(provider).toBeInstanceOf(ExampleUserProvider); | ||||
|   | ||||
							
								
								
									
										36
									
								
								src/api/user/UserProvider.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								src/api/user/UserProvider.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 UserProvider { | ||||
|     /** | ||||
|      * @returns {Promise<User>} A promise that resolves with the currently logged in user | ||||
|      */ | ||||
|     getCurrentUser() {} | ||||
|     /** | ||||
|      * @returns {Boolean} true if a user is currently logged in, otherwise false | ||||
|      */ | ||||
|     isLoggedIn() {} | ||||
|     /** | ||||
|     * @param {String} role | ||||
|     * @returns {Promise<Boolean>} true if the current user has the given role | ||||
|     */ | ||||
|     hasRole(role) {} | ||||
| } | ||||
							
								
								
									
										103
									
								
								src/api/user/UserStatusAPISpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								src/api/user/UserStatusAPISpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,103 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 User Status API", () => { | ||||
|     let openmct; | ||||
|     let userProvider; | ||||
|     let mockUser; | ||||
|  | ||||
|     beforeEach(() => { | ||||
|         userProvider = jasmine.createSpyObj("userProvider", [ | ||||
|             "setPollQuestion", | ||||
|             "getPollQuestion", | ||||
|             "getCurrentUser", | ||||
|             "getPossibleStatuses", | ||||
|             "getAllStatusRoles", | ||||
|             "canSetPollQuestion", | ||||
|             "isLoggedIn", | ||||
|             "on" | ||||
|         ]); | ||||
|         openmct = createOpenMct(); | ||||
|         mockUser = new openmct.user.User("test-user", "A test user"); | ||||
|         userProvider.getCurrentUser.and.returnValue(Promise.resolve(mockUser)); | ||||
|         userProvider.getPossibleStatuses.and.returnValue(Promise.resolve([])); | ||||
|         userProvider.getAllStatusRoles.and.returnValue(Promise.resolve([])); | ||||
|         userProvider.canSetPollQuestion.and.returnValue(Promise.resolve(false)); | ||||
|         userProvider.isLoggedIn.and.returnValue(true); | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     describe("the poll question", () => { | ||||
|         it('can be set via a user status provider if supported', () => { | ||||
|             openmct.user.setProvider(userProvider); | ||||
|             userProvider.canSetPollQuestion.and.returnValue(Promise.resolve(true)); | ||||
|  | ||||
|             return openmct.user.status.setPollQuestion('This is a poll question').then(() => { | ||||
|                 expect(userProvider.setPollQuestion).toHaveBeenCalledWith('This is a poll question'); | ||||
|             }); | ||||
|         }); | ||||
|         // fit('emits an event when the poll question changes', () => { | ||||
|         //     const pollQuestionChangeCallback = jasmine.createSpy('pollQuestionChangeCallback'); | ||||
|         //     let pollQuestionListener; | ||||
|  | ||||
|         //     userProvider.canSetPollQuestion.and.returnValue(Promise.resolve(true)); | ||||
|         //     userProvider.on.and.callFake((eventName, listener) => { | ||||
|         //         if (eventName === 'pollQuestionChange') { | ||||
|         //             pollQuestionListener = listener; | ||||
|         //         } | ||||
|         //     }); | ||||
|  | ||||
|         //     openmct.user.on('pollQuestionChange', pollQuestionChangeCallback); | ||||
|  | ||||
|         //     openmct.user.setProvider(userProvider); | ||||
|  | ||||
|         //     return openmct.user.status.setPollQuestion('This is a poll question').then(() => { | ||||
|         //         expect(pollQuestionListener).toBeDefined(); | ||||
|         //         pollQuestionListener(); | ||||
|         //         expect(pollQuestionChangeCallback).toHaveBeenCalled(); | ||||
|  | ||||
|         //         const pollQuestion = pollQuestionChangeCallback.calls.mostRecent().args[0]; | ||||
|         //         expect(pollQuestion.question).toBe('This is a poll question'); | ||||
|  | ||||
|         //         openmct.user.off('pollQuestionChange', pollQuestionChangeCallback); | ||||
|         //     }); | ||||
|         // }); | ||||
|         it('cannot be set if the user is not permitted', () => { | ||||
|             openmct.user.setProvider(userProvider); | ||||
|             userProvider.canSetPollQuestion.and.returnValue(Promise.resolve(false)); | ||||
|  | ||||
|             return openmct.user.status.setPollQuestion('This is a poll question').catch((error) => { | ||||
|                 expect(error).toBeInstanceOf(Error); | ||||
|             }).finally(() => { | ||||
|                 expect(userProvider.setPollQuestion).not.toHaveBeenCalled(); | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
| @@ -33,7 +33,7 @@ function replaceDotsWithUnderscores(filename) { | ||||
|  | ||||
| import {saveAs} from 'saveAs'; | ||||
| import html2canvas from 'html2canvas'; | ||||
| import uuid from 'uuid'; | ||||
| import { v4 as uuid } from 'uuid'; | ||||
|  | ||||
| class ImageExporter { | ||||
|     constructor(openmct) { | ||||
| @@ -51,7 +51,7 @@ class ImageExporter { | ||||
|         const overlays = this.openmct.overlays; | ||||
|         const dialog = overlays.dialog({ | ||||
|             iconClass: 'info', | ||||
|             message: 'Caputuring an image', | ||||
|             message: 'Capturing image, please wait...', | ||||
|             buttons: [ | ||||
|                 { | ||||
|                     label: 'Cancel', | ||||
|   | ||||
| @@ -52,7 +52,6 @@ export default (agent, document) => { | ||||
|     if (agent.isMobile()) { | ||||
|         const mediaQuery = window.matchMedia("(orientation: landscape)"); | ||||
|         function eventHandler(event) { | ||||
|             console.log("changed"); | ||||
|             if (event.matches) { | ||||
|                 body.classList.remove("portrait"); | ||||
|                 body.classList.add("landscape"); | ||||
|   | ||||
| @@ -1,4 +1,3 @@ | ||||
|  | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
| @@ -114,14 +113,12 @@ export default { | ||||
|         this.metadata = this.openmct.telemetry.getMetadata(this.domainObject); | ||||
|         this.formats = this.openmct.telemetry.getFormatMap(this.metadata); | ||||
|         this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier); | ||||
|         this.bounds = this.openmct.time.bounds(); | ||||
|  | ||||
|         this.limitEvaluator = this.openmct | ||||
|             .telemetry | ||||
|             .limitEvaluator(this.domainObject); | ||||
|  | ||||
|         this.openmct.time.on('timeSystem', this.updateTimeSystem); | ||||
|         this.openmct.time.on('bounds', this.updateBounds); | ||||
|  | ||||
|         this.timestampKey = this.openmct.time.timeSystem().key; | ||||
|  | ||||
| @@ -135,72 +132,41 @@ export default { | ||||
|  | ||||
|         this.valueKey = this.valueMetadata ? this.valueMetadata.key : undefined; | ||||
|  | ||||
|         this.unsubscribe = this.openmct | ||||
|             .telemetry | ||||
|             .subscribe(this.domainObject, this.setLatestValues); | ||||
|  | ||||
|         this.requestHistory(); | ||||
|         this.telemetryCollection = this.openmct.telemetry.requestCollection(this.domainObject, { | ||||
|             size: 1, | ||||
|             strategy: 'latest' | ||||
|         }); | ||||
|         this.telemetryCollection.on('add', this.setLatestValues); | ||||
|         this.telemetryCollection.on('clear', this.resetValues); | ||||
|         this.telemetryCollection.load(); | ||||
|  | ||||
|         if (this.hasUnits) { | ||||
|             this.setUnit(); | ||||
|         } | ||||
|     }, | ||||
|     destroyed() { | ||||
|         this.unsubscribe(); | ||||
|         this.openmct.time.off('timeSystem', this.updateTimeSystem); | ||||
|         this.openmct.time.off('bounds', this.updateBounds); | ||||
|         this.telemetryCollection.off('add', this.setLatestValues); | ||||
|         this.telemetryCollection.off('clear', this.resetValues); | ||||
|  | ||||
|         this.telemetryCollection.destroy(); | ||||
|     }, | ||||
|     methods: { | ||||
|         updateView() { | ||||
|             if (!this.updatingView) { | ||||
|                 this.updatingView = true; | ||||
|                 requestAnimationFrame(() => { | ||||
|                     let newTimestamp = this.getParsedTimestamp(this.latestDatum); | ||||
|  | ||||
|                     if (this.shouldUpdate(newTimestamp)) { | ||||
|                         this.timestamp = newTimestamp; | ||||
|                         this.datum = this.latestDatum; | ||||
|                     } | ||||
|  | ||||
|                     this.timestamp = this.getParsedTimestamp(this.latestDatum); | ||||
|                     this.datum = this.latestDatum; | ||||
|                     this.updatingView = false; | ||||
|                 }); | ||||
|             } | ||||
|         }, | ||||
|         setLatestValues(datum) { | ||||
|             this.latestDatum = datum; | ||||
|  | ||||
|         setLatestValues(data) { | ||||
|             this.latestDatum = data[data.length - 1]; | ||||
|             this.updateView(); | ||||
|         }, | ||||
|         shouldUpdate(newTimestamp) { | ||||
|             return this.inBounds(newTimestamp) | ||||
|                 && (this.timestamp === undefined || newTimestamp > this.timestamp); | ||||
|         }, | ||||
|         requestHistory() { | ||||
|             this.openmct | ||||
|                 .telemetry | ||||
|                 .request(this.domainObject, { | ||||
|                     start: this.bounds.start, | ||||
|                     end: this.bounds.end, | ||||
|                     size: 1, | ||||
|                     strategy: 'latest' | ||||
|                 }) | ||||
|                 .then((array) => this.setLatestValues(array[array.length - 1])) | ||||
|                 .catch((error) => { | ||||
|                     console.warn('Error fetching data', error); | ||||
|                 }); | ||||
|         }, | ||||
|         updateBounds(bounds, isTick) { | ||||
|             this.bounds = bounds; | ||||
|             if (!isTick) { | ||||
|                 this.resetValues(); | ||||
|                 this.requestHistory(); | ||||
|             } | ||||
|         }, | ||||
|         inBounds(timestamp) { | ||||
|             return timestamp >= this.bounds.start && timestamp <= this.bounds.end; | ||||
|         }, | ||||
|         updateTimeSystem(timeSystem) { | ||||
|             this.resetValues(); | ||||
|             this.timestampKey = timeSystem.key; | ||||
|         }, | ||||
|         updateViewContext() { | ||||
| @@ -241,4 +207,3 @@ export default { | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
|  | ||||
|   | ||||
| @@ -46,6 +46,7 @@ describe("The LAD Table", () => { | ||||
|  | ||||
|     let openmct; | ||||
|     let ladPlugin; | ||||
|     let historicalProvider; | ||||
|     let parent; | ||||
|     let child; | ||||
|     let telemetryCount = 3; | ||||
| @@ -81,6 +82,13 @@ describe("The LAD Table", () => { | ||||
|  | ||||
|         spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({})); | ||||
|  | ||||
|         historicalProvider = { | ||||
|             request: () => { | ||||
|                 return Promise.resolve([]); | ||||
|             } | ||||
|         }; | ||||
|         spyOn(openmct.telemetry, 'findRequestProvider').and.returnValue(historicalProvider); | ||||
|  | ||||
|         openmct.time.bounds({ | ||||
|             start: bounds.start, | ||||
|             end: bounds.end | ||||
| @@ -147,7 +155,7 @@ describe("The LAD Table", () => { | ||||
|         // add another telemetry object as composition in lad table to test multi rows | ||||
|         mockObj.ladTable.composition.push(anotherTelemetryObj.identifier); | ||||
|  | ||||
|         beforeEach(async () => { | ||||
|         beforeEach(async (done) => { | ||||
|             let telemetryRequestResolve; | ||||
|             let telemetryObjectResolve; | ||||
|             let anotherTelemetryObjectResolve; | ||||
| @@ -166,11 +174,12 @@ describe("The LAD Table", () => { | ||||
|                 callBack(); | ||||
|             }); | ||||
|  | ||||
|             openmct.telemetry.request.and.callFake(() => { | ||||
|             historicalProvider.request = () => { | ||||
|                 telemetryRequestResolve(mockTelemetry); | ||||
|  | ||||
|                 return telemetryRequestPromise; | ||||
|             }); | ||||
|             }; | ||||
|  | ||||
|             openmct.objects.get.and.callFake((obj) => { | ||||
|                 if (obj.key === 'telemetry-object') { | ||||
|                     telemetryObjectResolve(mockObj.telemetry); | ||||
| @@ -195,6 +204,8 @@ describe("The LAD Table", () => { | ||||
|  | ||||
|             await Promise.all([telemetryRequestPromise, telemetryObjectPromise, anotherTelemetryObjectPromise]); | ||||
|             await Vue.nextTick(); | ||||
|  | ||||
|             done(); | ||||
|         }); | ||||
|  | ||||
|         it("should show one row per object in the composition", () => { | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user