Compare commits
	
		
			54 Commits
		
	
	
		
			v2.0.4
			...
			temp-sourc
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 1a9c845f84 | ||
|   | c46849b166 | ||
|   | 6c71fa01f5 | ||
|   | c56d458ecb | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | f74a35f45a | ||
|   | dfa2e1ef1e | ||
|   | fa4c58a7cb | ||
|   | 6c642281e7 | ||
|   | 68e46e45c7 | ||
|   | 8cd87ff9d1 | ||
|   | d9ac0182c3 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 7bb108c36b | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 77804cff75 | ||
|   | 2d73296b36 | ||
|   | 405418b9d5 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | f999b9e12b | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 664ba399ea | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | c6078a234a | ||
|   | 17c16eba50 | ||
|   | 9f9c69ee68 | ||
|   | 037886aa01 | ||
|   | 48916564e4 | ||
|   | 1ca5271c3e | ||
|   | 6521b888d6 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 85fce3c456 | ||
|   | 8d577a8958 | ||
|   | 9c8ee09960 | ||
|   | 9568da9d5f | ||
|   | 2aa3b810ba | ||
|   | 1cdbb34e21 | ||
|   | 95299336d0 | ||
|   | b8ff5c7f33 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 9ede023cfa | ||
|   | 308e621b5d | ||
|   | e6b5870234 | ||
|   | 03e7d912be | ||
|   | 09da373d1c | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | b8d9e41c01 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 815e7d169c | ||
|   | 58387e0902 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 0a0826f87e | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | e063442d8c | ||
|   | 6a5823ab5c | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 0493e5ae3c | ||
|   | 24f13b6249 | ||
|   | 221fb4d6bf | ||
|   | 257742b45b | ||
|   | 44edec4f04 | ||
|   | ab4d0dd37f | ||
|   | c089a4760d | ||
|   | b77a4066f2 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 20d7e80502 | ||
|   | d63fec51a7 | ||
|   | d30c4fcb53 | 
| @@ -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: | ||||
| @@ -64,7 +64,7 @@ commands: | ||||
|         - 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: | ||||
| @@ -128,16 +128,30 @@ 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: | ||||
| @@ -150,10 +164,6 @@ workflows: | ||||
|           browser: ChromeHeadless | ||||
|           post-steps: | ||||
|             - upload_code_covio | ||||
|       - unit-test: | ||||
|           name: node16-chrome | ||||
|           node-version: lts/gallium | ||||
|           browser: ChromeHeadless | ||||
|       - unit-test: | ||||
|           name: node18-chrome | ||||
|           node-version: "18" | ||||
| @@ -162,6 +172,8 @@ workflows: | ||||
|           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: | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/workflows/e2e-pr.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/e2e-pr.yml
									
									
									
									
										vendored
									
									
								
							| @@ -30,7 +30,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 | ||||
|       - run: npm run test:e2e:full | ||||
|       - name: Archive test results | ||||
|   | ||||
							
								
								
									
										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,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" | ||||
|             } | ||||
|         } | ||||
|     ] | ||||
| }; | ||||
|   | ||||
							
								
								
									
										27
									
								
								e2e/fixtures.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								e2e/fixtures.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| /* 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'); | ||||
|  | ||||
| exports.test = base.test.extend({ | ||||
|     page: async ({ baseURL, page }, use) => { | ||||
|         const messages = []; | ||||
|         page.on('console', msg => messages.push(`[${msg.type()}] ${msg.text()}`)); | ||||
|         await use(page); | ||||
|         await expect.soft(messages.toString()).not.toContain('[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: 0, | ||||
|     testDir: 'tests/performance/', | ||||
|     timeout: 30 * 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: 'off', | ||||
|         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":{"21338566-d472-4377-aed1-21b79272c8de":{"identifier":{"key":"21338566-d472-4377-aed1-21b79272c8de","namespace":""},"name":"Performance Display Layout","type":"layout","composition":[{"key":"644c2e47-2903-475f-8a4a-6be1588ee02f","namespace":""}],"configuration":{"items":[{"width":32,"height":18,"x":1,"y":1,"identifier":{"key":"644c2e47-2903-475f-8a4a-6be1588ee02f","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"5aeb5a71-3149-41ed-9d8a-d34b0a18b053"}],"layoutGrid":[10,10]},"modified":1652228997384,"location":"mine","persisted":1652228997384},"644c2e47-2903-475f-8a4a-6be1588ee02f":{"identifier":{"key":"644c2e47-2903-475f-8a4a-6be1588ee02f","namespace":""},"name":"Performance Example Imagery","type":"example.imagery","configuration":{"imageLocation":"","imageLoadDelayInMilliSeconds":20000,"imageSamples":[]},"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}},{"name":"Image Download Name","key":"imageDownloadName","format":"imageDownloadName","hints":{"imageDownloadName":1}}]},"modified":1652228997375,"location":"21338566-d472-4377-aed1-21b79272c8de","persisted":1652228997375}},"rootId":"21338566-d472-4377-aed1-21b79272c8de"} | ||||
							
								
								
									
										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('input[type="search"]').click(); | ||||
|         await page.evaluate(() => window.performance.mark("search-available")); | ||||
|         // Fill Search input | ||||
|         await page.locator('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('.c-click-icon').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('input[type="search"]').click(); | ||||
|         // Fill Search input | ||||
|         await page.locator('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('input[type="search"]').click(); | ||||
|         await page.evaluate(() => window.performance.mark("search-available")); | ||||
|         // Fill Search input | ||||
|         await page.locator('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', () => { | ||||
|  | ||||
|   | ||||
| @@ -26,7 +26,8 @@ suite is sharing state between tests which is considered an anti-pattern. Implim | ||||
| 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'); | ||||
|  | ||||
| let conditionSetUrl; | ||||
| let getConditionSetIdentifierFromUrl; | ||||
| @@ -41,6 +42,9 @@ test('Create new Condition Set object and store @localStorage', async ({ page, c | ||||
|     // Click text=Condition Set | ||||
|     await page.click('text=Condition Set'); | ||||
|  | ||||
|     // 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(), | ||||
|   | ||||
| @@ -24,13 +24,15 @@ | ||||
| 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())) | ||||
|         page.on('console', msg => console.log(msg.text())); | ||||
|         //Go to baseURL | ||||
|         await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
| @@ -40,18 +42,24 @@ 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(); | ||||
|         const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox(); | ||||
| @@ -77,9 +85,11 @@ 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 = await page.locator(backgroundImageSelector); | ||||
|         const bgImageLocator = page.locator(backgroundImageSelector); | ||||
|         await bgImageLocator.hover(); | ||||
|  | ||||
|         // zoom in | ||||
|         await page.mouse.wheel(0, deltaYStep * 2); | ||||
|         await bgImageLocator.hover(); | ||||
| @@ -91,50 +101,59 @@ 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); | ||||
|         const bgImageLocator = 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 zoomInBtn = page.locator('.t-btn-zoom-in'); | ||||
|         const zoomOutBtn = page.locator('.t-btn-zoom-out'); | ||||
|         const initialBoundingBox = await bgImageLocator.boundingBox(); | ||||
|  | ||||
|         await zoomInBtn.click(); | ||||
| @@ -155,21 +174,27 @@ test.describe('Example Imagery', () => { | ||||
|     }); | ||||
|  | ||||
|     test('Can use the reset button to reset the image', async ({ page }) => { | ||||
|         const bgImageLocator = await page.locator(backgroundImageSelector); | ||||
|         const bgImageLocator = page.locator(backgroundImageSelector); | ||||
|         // wait for zoom animation to finish | ||||
|         await bgImageLocator.hover(); | ||||
|         const zoomInBtn = await page.locator('.t-btn-zoom-in'); | ||||
|         const zoomResetBtn = await page.locator('.t-btn-zoom-reset'); | ||||
|  | ||||
|         const zoomInBtn = page.locator('.t-btn-zoom-in'); | ||||
|         const zoomResetBtn = page.locator('.t-btn-zoom-reset'); | ||||
|         const initialBoundingBox = await bgImageLocator.boundingBox(); | ||||
|  | ||||
|         await zoomInBtn.click(); | ||||
|         // wait for zoom animation to finish | ||||
|         await bgImageLocator.hover(); | ||||
|         await zoomInBtn.click(); | ||||
|         // wait for zoom animation to finish | ||||
|         await bgImageLocator.hover(); | ||||
|  | ||||
|         const zoomedInBoundingBox = await bgImageLocator.boundingBox(); | ||||
|         expect.soft(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height); | ||||
|         expect.soft(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width); | ||||
|  | ||||
|         await zoomResetBtn.click(); | ||||
|         // wait for zoom animation to finish | ||||
|         await bgImageLocator.hover(); | ||||
|  | ||||
|         const resetBoundingBox = await bgImageLocator.boundingBox(); | ||||
| @@ -180,38 +205,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(); | ||||
|  | ||||
|         // open the time conductor drop down | ||||
|         await page.locator('.c-conductor__controls button.c-mode-button').click(); | ||||
|         // Click local clock | ||||
|         await page.locator('.icon-clock >> text=Local Clock').click(); | ||||
|  | ||||
|         await expect.soft(pausePlayButton).not.toHaveClass(/is-paused/); | ||||
|         const zoomInBtn = page.locator('.t-btn-zoom-in'); | ||||
|         await zoomInBtn.click(); | ||||
|         // wait for zoom animation to finish | ||||
|         await bgImageLocator.hover(); | ||||
|  | ||||
|         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(); | ||||
|  | ||||
|     // 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(); | ||||
|     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(); | ||||
|     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(); | ||||
|     await page.mouse.wheel(0, deltaYStep * 2); | ||||
|  | ||||
|     // Wait for zoom animation to finish | ||||
|     await bgImageLocator.hover(); | ||||
|     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'); | ||||
| }); | ||||
|   | ||||
| @@ -21,10 +21,11 @@ | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* | ||||
| Test for plot autoscale. | ||||
| Testsuite for plot autoscale. | ||||
| */ | ||||
|  | ||||
| const { test: _test, expect } = require('@playwright/test'); | ||||
| 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 | ||||
| @@ -47,7 +48,10 @@ test.use({ | ||||
| }); | ||||
|  | ||||
| test.describe('ExportAsJSON', () => { | ||||
|     test('autoscale off causes no error from undefined user range', async ({ page }) => { | ||||
|     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); | ||||
| @@ -68,14 +72,6 @@ test.describe('ExportAsJSON', () => { | ||||
|                 .then(shot => expect(shot).toMatchSnapshot('autoscale-canvas-prepan.png', { maxDiffPixels: 40 })) | ||||
|         ]); | ||||
|  | ||||
|         let errorCount = 0; | ||||
|  | ||||
|         function onError() { | ||||
|             errorCount++; | ||||
|         } | ||||
|  | ||||
|         page.on('pageerror', onError); | ||||
|  | ||||
|         await page.keyboard.down('Alt'); | ||||
|  | ||||
|         await canvas.dragTo(canvas, { | ||||
| @@ -91,12 +87,6 @@ test.describe('ExportAsJSON', () => { | ||||
|  | ||||
|         await page.keyboard.up('Alt'); | ||||
|  | ||||
|         page.off('pageerror', onError); | ||||
|  | ||||
|         // There would have been an error at this point. So if there isn't, then | ||||
|         // we fixed it. | ||||
|         expect(errorCount).toBe(0); | ||||
|  | ||||
|         // Ensure the drag worked. | ||||
|         await Promise.all([ | ||||
|             testYTicks(page, ['0.00', '0.50', '1.00', '1.50', '2.00']), | ||||
| @@ -134,9 +124,14 @@ async function createSinewaveOverlayPlot(page) { | ||||
|     // add overlay plot with defaults | ||||
|     await page.locator('li:has-text("Overlay Plot")').click(); | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine/a9268c6f-45cc-4bcd-a6a0-50ac4036e396?tc.mode=fixed&tc.startBound=1649305424163&tc.endBound=1649307224163&tc.timeSystem=utc&view=plot-overlay' }*/), | ||||
|         page.locator('text=OK').click() | ||||
|         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(); | ||||
| @@ -148,14 +143,19 @@ async function createSinewaveOverlayPlot(page) { | ||||
|     // add sine wave generator with defaults | ||||
|     await page.locator('li:has-text("Sine Wave Generator")').click(); | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine/a9268c6f-45cc-4bcd-a6a0-50ac4036e396/5cfa5c69-17bc-4a99-9545-4da8125380c5?tc.mode=fixed&tc.startBound=1649305424163&tc.endBound=1649307224163&tc.timeSystem=utc&view=plot-single' }*/), | ||||
|         page.locator('text=OK').click() | ||||
|         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(/*{ url: 'http://localhost:8080/#/browse/mine/a9268c6f-45cc-4bcd-a6a0-50ac4036e396?tc.mode=fixed&tc.startBound=1649305424163&tc.endBound=1649307224163&tc.timeSystem=utc&view=plot-overlay' }*/), | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('text=Unnamed Overlay Plot').first().click() | ||||
|     ]); | ||||
| } | ||||
| @@ -168,11 +168,18 @@ async function turnOffAutoscale(page) { | ||||
|     await page.locator('text=Unnamed Overlay Plot Snapshot >> button').nth(3).click(); | ||||
|  | ||||
|     // uncheck autoscale | ||||
|     await page.locator('text=Y Axis Scaling Auto scale Padding >> input[type="checkbox"]').uncheck(); | ||||
|     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 page.locator('text=Save and Finish Editing').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'}); | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -180,6 +187,7 @@ async function turnOffAutoscale(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) { | ||||
|   | ||||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 18 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 19 KiB | 
| @@ -21,13 +21,18 @@ | ||||
|  *****************************************************************************/ | ||||
| 
 | ||||
| /* | ||||
| Tests to verify log plot functionality. | ||||
| 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, expect } = require('@playwright/test'); | ||||
| const { test } = require('../../../fixtures.js'); | ||||
| const { expect } = require('@playwright/test'); | ||||
| 
 | ||||
| test.describe('Log plot tests', () => { | ||||
|     test.only('Can create a log plot.', async ({ page }) => { | ||||
|     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); | ||||
| @@ -39,17 +44,25 @@ test.describe('Log plot tests', () => { | ||||
|         await testLogTicks(page); | ||||
|         await saveOverlayPlot(page); | ||||
|         await testLogTicks(page); | ||||
|         await testLogPlotPixels(page); | ||||
|         //await testLogPlotPixels(page);
 | ||||
| 
 | ||||
|         // refresh page
 | ||||
|         await page.reload(); | ||||
|         // 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); | ||||
|         //await testLogPlotPixels(page);
 | ||||
|     }); | ||||
| 
 | ||||
|     test.only('Verify that log mode option is reflected in import/export JSON', async ({ 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); | ||||
| @@ -57,7 +70,7 @@ test.describe('Log plot tests', () => { | ||||
| 
 | ||||
|         // TODO ...export, delete the overlay, then import it...
 | ||||
| 
 | ||||
|         await testLogTicks(page); | ||||
|         //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...
 | ||||
| @@ -88,14 +101,18 @@ async function makeOverlayPlot(page) { | ||||
|     await page.locator('button.c-create-button').click(); | ||||
|     await page.locator('li:has-text("Overlay Plot")').click(); | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine/8caf7072-535b-4af6-8394-edd86e3ea35f?tc.mode=fixed&tc.startBound=1648590633191&tc.endBound=1648592433191&tc.timeSystem=utc&view=plot-overlay' }*/), | ||||
|         page.locator('text=OK').click() | ||||
|         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 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(); | ||||
|     await saveOverlayPlot(page); | ||||
| 
 | ||||
|     // create a sinewave generator
 | ||||
| 
 | ||||
| @@ -116,15 +133,20 @@ async function makeOverlayPlot(page) { | ||||
|     // Click OK to make generator
 | ||||
| 
 | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine/8caf7072-535b-4af6-8394-edd86e3ea35f/6e58b26a-8a73-4df6-b3a6-918decc0bbfa?tc.mode=fixed&tc.startBound=1648590633191&tc.endBound=1648592433191&tc.timeSystem=utc&view=plot-single' }*/), | ||||
|         page.locator('text=OK').click() | ||||
|         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(/*{ url: 'http://localhost:8080/#/browse/mine/8caf7072-535b-4af6-8394-edd86e3ea35f?tc.mode=fixed&tc.startBound=1648590633191&tc.endBound=1648592433191&tc.timeSystem=utc&view=plot-overlay' }*/), | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('text=Unnamed Overlay Plot').first().click() | ||||
|     ]); | ||||
| } | ||||
| @@ -133,7 +155,7 @@ async function makeOverlayPlot(page) { | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function testRegularTicks(page) { | ||||
|     const yTicks = page.locator('.gl-plot-y-tick-label'); | ||||
|     const yTicks = await page.locator('.gl-plot-y-tick-label'); | ||||
|     expect(await yTicks.count()).toBe(7); | ||||
|     await expect(yTicks.nth(0)).toHaveText('-2'); | ||||
|     await expect(yTicks.nth(1)).toHaveText('0'); | ||||
| @@ -148,7 +170,7 @@ async function testRegularTicks(page) { | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function testLogTicks(page) { | ||||
|     const yTicks = page.locator('.gl-plot-y-tick-label'); | ||||
|     const yTicks = await page.locator('.gl-plot-y-tick-label'); | ||||
|     expect(await yTicks.count()).toBe(28); | ||||
|     await expect(yTicks.nth(0)).toHaveText('-2.98'); | ||||
|     await expect(yTicks.nth(1)).toHaveText('-2.50'); | ||||
| @@ -186,6 +208,7 @@ async function testLogTicks(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(); | ||||
| } | ||||
| 
 | ||||
| /** | ||||
| @@ -210,17 +233,27 @@ async function disableLogMode(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 page.locator('text=Save and Finish Editing').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, 50)); | ||||
|         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
 | ||||
| @@ -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(); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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,24 @@ 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'); | ||||
| 
 | ||||
| }); | ||||
| @@ -21,7 +21,7 @@ | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import EventEmitter from 'EventEmitter'; | ||||
| import uuid from 'uuid'; | ||||
| import { v4 as uuid } from 'uuid'; | ||||
| import createExampleUser from './exampleUserCreator'; | ||||
|  | ||||
| export default class ExampleUserProvider extends EventEmitter { | ||||
|   | ||||
| @@ -196,6 +196,8 @@ | ||||
|         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> | ||||
|   | ||||
							
								
								
									
										50
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										50
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,31 +1,30 @@ | ||||
| { | ||||
|   "name": "openmct", | ||||
|   "version": "2.0.4", | ||||
|   "version": "2.0.4-SNAPSHOT", | ||||
|   "description": "The Open MCT core platform", | ||||
|   "devDependencies": { | ||||
|     "@babel/eslint-parser": "7.16.3", | ||||
|     "@braintree/sanitize-url": "6.0.0", | ||||
|     "@percy/cli": "1.0.4", | ||||
|     "@percy/playwright": "1.0.2", | ||||
|     "@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", | ||||
|     "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", | ||||
|     "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-playwright": "0.9.0", | ||||
|     "eslint-plugin-vue": "8.5.0", | ||||
|     "eslint-plugin-you-dont-need-lodash-underscore": "6.12.0", | ||||
|     "eventemitter3": "1.2.0", | ||||
| @@ -35,12 +34,12 @@ | ||||
|     "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.18", | ||||
|     "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", | ||||
| @@ -48,32 +47,32 @@ | ||||
|     "karma-sourcemap-loader": "0.3.8", | ||||
|     "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": "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": "5.0.0", | ||||
|     "sass": "1.49.9", | ||||
|     "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.3.0", | ||||
|     "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" | ||||
| @@ -82,8 +81,8 @@ | ||||
|     "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", | ||||
| @@ -92,11 +91,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 notebook persistence performance", | ||||
|     "test:e2e:local": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome", | ||||
|     "test:e2e:debug": "npm run test:e2e:local -- --debug", | ||||
|     "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", | ||||
|   | ||||
| @@ -242,7 +242,6 @@ define([ | ||||
|  | ||||
|         // 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()); | ||||
|   | ||||
| @@ -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 | ||||
| @@ -70,7 +70,7 @@ | ||||
|         </button> | ||||
|         <button | ||||
|             tabindex="0" | ||||
|             class="c-button" | ||||
|             class="c-button js-cancel-button" | ||||
|             @click="onDismiss" | ||||
|         > | ||||
|             {{ cancelLabel }} | ||||
| @@ -81,7 +81,7 @@ | ||||
|  | ||||
| <script> | ||||
| import FormRow from "@/api/forms/components/FormRow.vue"; | ||||
| import uuid from 'uuid'; | ||||
| import { v4 as uuid } from 'uuid'; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
|   | ||||
| @@ -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); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -39,7 +39,7 @@ | ||||
| import toggleMixin from '../../toggle-check-box-mixin'; | ||||
| import ToggleSwitch from '@/ui/components/ToggleSwitch.vue'; | ||||
|  | ||||
| import uuid from 'uuid'; | ||||
| import { v4 as uuid } from 'uuid'; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
|   | ||||
| @@ -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 }} | ||||
| @@ -37,6 +38,7 @@ | ||||
|             :key="action.name" | ||||
|             :class="action.cssClass" | ||||
|             :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 { | ||||
|     /** | ||||
|   | ||||
| @@ -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()); | ||||
| @@ -267,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; | ||||
| @@ -306,7 +306,6 @@ export class TelemetryCollection extends EventEmitter { | ||||
|             if (added.length > 0) { | ||||
|                 this.emit('add', added); | ||||
|             } | ||||
|  | ||||
|         } else { | ||||
|             // user bounds change, reset | ||||
|             this._reset(); | ||||
| @@ -326,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); | ||||
|  | ||||
| @@ -402,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(); | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										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.'; | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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"); | ||||
|   | ||||
| @@ -40,6 +40,14 @@ export default { | ||||
|         BarGraph | ||||
|     }, | ||||
|     inject: ['openmct', 'domainObject', 'path'], | ||||
|     props: { | ||||
|         options: { | ||||
|             type: Object, | ||||
|             default() { | ||||
|                 return {}; | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         this.telemetryObjects = {}; | ||||
|         this.telemetryObjectFormats = {}; | ||||
| @@ -247,7 +255,7 @@ export default { | ||||
|                 } | ||||
|             }); | ||||
| 
 | ||||
|             const trace = { | ||||
|             let trace = { | ||||
|                 key, | ||||
|                 name: telemetryObject.name, | ||||
|                 x: xValues, | ||||
| @@ -255,13 +263,18 @@ export default { | ||||
|                 text: yValues.map(String), | ||||
|                 xAxisMetadata: axisMetadata.xAxisMetadata, | ||||
|                 yAxisMetadata: axisMetadata.yAxisMetadata, | ||||
|                 type: 'bar', | ||||
|                 type: this.options.type ? this.options.type : 'bar', | ||||
|                 marker: { | ||||
|                     color: this.domainObject.configuration.barStyles.series[key].color | ||||
|                 }, | ||||
|                 hoverinfo: 'skip' | ||||
|             }; | ||||
| 
 | ||||
|             if (this.options.type) { | ||||
|                 trace.mode = 'markers'; | ||||
|                 trace.hoverinfo = 'x+y'; | ||||
|             } | ||||
| 
 | ||||
|             this.addTrace(trace, key); | ||||
|         }, | ||||
|         isDataInTimeRange(datum, key) { | ||||
							
								
								
									
										57
									
								
								src/plugins/charts/scatter/ScatterPlotCompositionPolicy.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								src/plugins/charts/scatter/ScatterPlotCompositionPolicy.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2021, 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 { SCATTER_PLOT_KEY } from './scatterPlotConstants'; | ||||
|  | ||||
| export default function ScatterPlotCompositionPolicy(openmct) { | ||||
|     function hasRange(metadata) { | ||||
|         const rangeValues = metadata.valuesForHints(['range']).map((value) => { | ||||
|             return value.source; | ||||
|         }); | ||||
|  | ||||
|         const uniqueRangeValues = new Set(rangeValues); | ||||
|  | ||||
|         return uniqueRangeValues && uniqueRangeValues.size > 1; | ||||
|     } | ||||
|  | ||||
|     function hasScatterPlotTelemetry(domainObject) { | ||||
|         if (!openmct.telemetry.isTelemetryObject(domainObject)) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         let metadata = openmct.telemetry.getMetadata(domainObject); | ||||
|  | ||||
|         return metadata.values().length > 0 && hasRange(metadata); | ||||
|     } | ||||
|  | ||||
|     return { | ||||
|         allow: function (parent, child) { | ||||
|             if (parent.type === SCATTER_PLOT_KEY) { | ||||
|                 if ((child.type === 'conditionSet') || (!hasScatterPlotTelemetry(child))) { | ||||
|                     return false; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             return true; | ||||
|         } | ||||
|     }; | ||||
| } | ||||
							
								
								
									
										146
									
								
								src/plugins/charts/scatter/ScatterPlotForm.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										146
									
								
								src/plugins/charts/scatter/ScatterPlotForm.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,146 @@ | ||||
| /***************************************************************************** | ||||
| * 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"> | ||||
|     <span | ||||
|         class="field control" | ||||
|         :class="model.cssClass" | ||||
|     > | ||||
|         <div | ||||
|             class="c-form--sub-grid" | ||||
|         > | ||||
|             <div class="c-form__row"> | ||||
|                 <span | ||||
|                     class="req-indicator" | ||||
|                     :class="{'req': isRequired}" | ||||
|                 > | ||||
|                 </span> | ||||
|                 <label>Minimum X axis value</label> | ||||
|                 <input | ||||
|                     ref="domainMin" | ||||
|                     v-model.number="domainMin" | ||||
|                     data-field-name="domainMin" | ||||
|                     type="number" | ||||
|                     @input="onChange('domainMin')" | ||||
|                 > | ||||
|             </div> | ||||
|  | ||||
|             <div class="c-form__row"> | ||||
|                 <span | ||||
|                     class="req-indicator" | ||||
|                     :class="{'req': isRequired}" | ||||
|                 > | ||||
|                 </span> | ||||
|                 <label>Maximum X axis value</label> | ||||
|                 <input | ||||
|                     ref="domainMax" | ||||
|                     v-model.number="domainMax" | ||||
|                     data-field-name="domainMax" | ||||
|                     type="number" | ||||
|                     @input="onChange('domainMax')" | ||||
|                 > | ||||
|             </div> | ||||
|  | ||||
|             <div class="c-form__row"> | ||||
|                 <span | ||||
|                     class="req-indicator" | ||||
|                     :class="{'req': isRequired}" | ||||
|                 > | ||||
|                 </span> | ||||
|                 <label>Minimum Y axis value</label> | ||||
|                 <input | ||||
|                     ref="rangeMin" | ||||
|                     v-model.number="rangeMin" | ||||
|                     data-field-name="rangeMin" | ||||
|                     type="number" | ||||
|                     @input="onChange('rangeMin')" | ||||
|                 > | ||||
|             </div> | ||||
|  | ||||
|             <div class="c-form__row"> | ||||
|                 <span | ||||
|                     class="req-indicator" | ||||
|                     :class="{'req': isRequired}" | ||||
|                 > | ||||
|                 </span> | ||||
|                 <label>Maximum Y axis value</label> | ||||
|                 <input | ||||
|                     ref="rangeMax" | ||||
|                     v-model.number="rangeMax" | ||||
|                     data-field-name="rangeMax" | ||||
|                     type="number" | ||||
|                     @input="onChange('rangeMax')" | ||||
|                 > | ||||
|             </div> | ||||
|         </div> | ||||
|     </span> | ||||
| </span> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
|  | ||||
| export default { | ||||
|     props: { | ||||
|         model: { | ||||
|             type: Object, | ||||
|             required: true | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             rangeMax: this.model.value.rangeMax, | ||||
|             rangeMin: this.model.value.rangeMin, | ||||
|             domainMax: this.model.value.domainMax, | ||||
|             domainMin: this.model.value.domainMin | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|         isRequired() { | ||||
|             return [this.rangeMax, this.rangeMin, this.domainMin, this.domainMax].some(value => value !== undefined && value !== ''); | ||||
|         } | ||||
|     }, | ||||
|     methods: { | ||||
|         onChange(property) { | ||||
|             if (this[property] === '') { | ||||
|                 this[property] = undefined; | ||||
|             } | ||||
|  | ||||
|             const data = { | ||||
|                 model: this.model, | ||||
|                 value: { | ||||
|                     rangeMax: this.rangeMax, | ||||
|                     rangeMin: this.rangeMin, | ||||
|                     domainMax: this.domainMax, | ||||
|                     domainMin: this.domainMin | ||||
|                 } | ||||
|             }; | ||||
|  | ||||
|             if (property) { | ||||
|                 this.model.validate(data); | ||||
|             } | ||||
|  | ||||
|             this.$emit('onChange', data); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
							
								
								
									
										346
									
								
								src/plugins/charts/scatter/ScatterPlotView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										346
									
								
								src/plugins/charts/scatter/ScatterPlotView.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,346 @@ | ||||
| <!-- | ||||
|  Open MCT, Copyright (c) 2014-2021, 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> | ||||
| <ScatterPlotWithUnderlay | ||||
|     class="c-plot c-scatter-chart-view" | ||||
|     :data="trace" | ||||
|     :plot-axis-title="plotAxisTitle" | ||||
|     @subscribe="subscribeToAll" | ||||
|     @unsubscribe="removeAllSubscriptions" | ||||
| /> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import ScatterPlotWithUnderlay from './ScatterPlotWithUnderlay.vue'; | ||||
| import _ from 'lodash'; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
|         ScatterPlotWithUnderlay | ||||
|     }, | ||||
|     inject: ['openmct', 'domainObject', 'path'], | ||||
|     data() { | ||||
|         this.telemetryObjects = {}; | ||||
|         this.telemetryObjectFormats = {}; | ||||
|         this.valuesByTimestamp = {}; | ||||
|         this.subscriptions = []; | ||||
|  | ||||
|         return { | ||||
|             trace: [] | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|         plotAxisTitle() { | ||||
|             const { xAxisMetadata = {}, yAxisMetadata = {} } = this.trace[0] || {}; | ||||
|             const xAxisUnit = xAxisMetadata.units ? `(${xAxisMetadata.units})` : ''; | ||||
|             const yAxisUnit = yAxisMetadata.units ? `(${yAxisMetadata.units})` : ''; | ||||
|  | ||||
|             return { | ||||
|                 xAxisTitle: `${xAxisMetadata.name || ''} ${xAxisUnit}`, | ||||
|                 yAxisTitle: `${yAxisMetadata.name || ''} ${yAxisUnit}` | ||||
|             }; | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.setTimeContext(); | ||||
|         this.loadComposition(); | ||||
|         this.reloadTelemetry = this.reloadTelemetry.bind(this); | ||||
|         this.reloadTelemetry = _.debounce(this.reloadTelemetry, 500); | ||||
|         this.unobserve = this.openmct.objects.observe(this.domainObject, 'configuration.axes', this.reloadTelemetry); | ||||
|         this.unobserveUnderlayRanges = this.openmct.objects.observe(this.domainObject, 'configuration.ranges', this.reloadTelemetry); | ||||
|     }, | ||||
|     beforeDestroy() { | ||||
|         this.stopFollowingTimeContext(); | ||||
|  | ||||
|         if (!this.composition) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         this.removeAllSubscriptions(); | ||||
|  | ||||
|         this.composition.off('add', this.addToComposition); | ||||
|         this.composition.off('remove', this.removeTelemetryObject); | ||||
|         if (this.unobserve) { | ||||
|             this.unobserve(); | ||||
|         } | ||||
|  | ||||
|         if (this.unobserveUnderlayRanges) { | ||||
|             this.unobserveUnderlayRanges(); | ||||
|         } | ||||
|     }, | ||||
|     methods: { | ||||
|         setTimeContext() { | ||||
|             this.stopFollowingTimeContext(); | ||||
|  | ||||
|             this.timeContext = this.openmct.time.getContextForView(this.path); | ||||
|             this.followTimeContext(); | ||||
|  | ||||
|         }, | ||||
|         followTimeContext() { | ||||
|             this.timeContext.on('bounds', this.reloadTelemetry); | ||||
|         }, | ||||
|         stopFollowingTimeContext() { | ||||
|             if (this.timeContext) { | ||||
|                 this.timeContext.off('bounds', this.reloadTelemetry); | ||||
|             } | ||||
|         }, | ||||
|         addToComposition(telemetryObject) { | ||||
|             if (Object.values(this.telemetryObjects).length > 0) { | ||||
|                 this.confirmRemoval(telemetryObject); | ||||
|             } else { | ||||
|                 this.addTelemetryObject(telemetryObject); | ||||
|             } | ||||
|         }, | ||||
|         removeFromComposition(telemetryObject) { | ||||
|             let composition = this.domainObject.composition.filter(id => | ||||
|                 !this.openmct.objects.areIdsEqual(id, telemetryObject.identifier) | ||||
|             ); | ||||
|  | ||||
|             this.openmct.objects.mutate(this.domainObject, 'composition', composition); | ||||
|         }, | ||||
|         addTelemetryObject(telemetryObject) { | ||||
|             // grab information we need from the added telmetry object | ||||
|             const key = this.openmct.objects.makeKeyString(telemetryObject.identifier); | ||||
|             this.telemetryObjects[key] = telemetryObject; | ||||
|             const metadata = this.openmct.telemetry.getMetadata(telemetryObject); | ||||
|             this.telemetryObjectFormats[key] = this.openmct.telemetry.getFormatMap(metadata); | ||||
|             this.getDataForTelemetry(key); | ||||
|         }, | ||||
|         confirmRemoval(telemetryObject) { | ||||
|             const dialog = this.openmct.overlays.dialog({ | ||||
|                 iconClass: 'alert', | ||||
|                 message: 'This action will replace the current telemetry source. Do you want to continue?', | ||||
|                 buttons: [ | ||||
|                     { | ||||
|                         label: 'Ok', | ||||
|                         emphasis: true, | ||||
|                         callback: () => { | ||||
|                             const oldTelemetryObject = Object.values(this.telemetryObjects)[0]; | ||||
|                             this.removeFromComposition(oldTelemetryObject); | ||||
|                             this.removeTelemetryObject(oldTelemetryObject.identifier); | ||||
|                             this.valuesByTimestamp = {}; | ||||
|                             this.addTelemetryObject(telemetryObject); | ||||
|                             dialog.dismiss(); | ||||
|                         } | ||||
|                     }, | ||||
|                     { | ||||
|                         label: 'Cancel', | ||||
|                         callback: () => { | ||||
|                             this.removeFromComposition(telemetryObject); | ||||
|                             dialog.dismiss(); | ||||
|                         } | ||||
|                     } | ||||
|                 ] | ||||
|             }); | ||||
|         }, | ||||
|         getTelemetryProcessor(keyString) { | ||||
|             return (telemetry) => { | ||||
|                 //Check that telemetry object has not been removed since telemetry was requested. | ||||
|                 const telemetryObject = this.telemetryObjects[keyString]; | ||||
|                 if (!telemetryObject) { | ||||
|                     return; | ||||
|                 } | ||||
|  | ||||
|                 telemetry.forEach(datum => { | ||||
|                     this.addDataToGraph(telemetryObject, datum); | ||||
|                 }); | ||||
|                 this.updateTrace(telemetryObject); | ||||
|             }; | ||||
|         }, | ||||
|         getAxisMetadata(telemetryObject) { | ||||
|             const metadata = this.openmct.telemetry.getMetadata(telemetryObject); | ||||
|             if (!metadata) { | ||||
|                 return {}; | ||||
|             } | ||||
|  | ||||
|             return metadata.valuesForHints(['range']); | ||||
|         }, | ||||
|         loadComposition() { | ||||
|             this.composition = this.openmct.composition.get(this.domainObject); | ||||
|             this.composition.on('add', this.addToComposition); | ||||
|             this.composition.on('remove', this.removeTelemetryObject); | ||||
|             this.composition.load(); | ||||
|         }, | ||||
|         reloadTelemetry() { | ||||
|             this.valuesByTimestamp = {}; | ||||
|  | ||||
|             Object.keys(this.telemetryObjects).forEach(key => { | ||||
|                 this.getDataForTelemetry(key); | ||||
|             }); | ||||
|         }, | ||||
|         getDataForTelemetry(key) { | ||||
|             const telemetryObject = this.telemetryObjects[key]; | ||||
|             if (!telemetryObject) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             const telemetryProcessor = this.getTelemetryProcessor(key); | ||||
|             const options = this.getOptions(); | ||||
|             this.openmct.telemetry.request(telemetryObject, options).then(telemetryProcessor); | ||||
|             this.subscribeToObject(telemetryObject); | ||||
|         }, | ||||
|         removeTelemetryObject(identifier) { | ||||
|             const key = this.openmct.objects.makeKeyString(identifier); | ||||
|             if (this.telemetryObjects[key]) { | ||||
|                 delete this.telemetryObjects[key]; | ||||
|             } | ||||
|  | ||||
|             if (this.telemetryObjectFormats && this.telemetryObjectFormats[key]) { | ||||
|                 delete this.telemetryObjectFormats[key]; | ||||
|             } | ||||
|  | ||||
|             this.removeSubscription(key); | ||||
|         }, | ||||
|         addDataToGraph(telemetryObject, data) { | ||||
|             const key = this.openmct.objects.makeKeyString(telemetryObject.identifier); | ||||
|  | ||||
|             if (data.message) { | ||||
|                 this.openmct.notifications.alert(data.message); | ||||
|             } | ||||
|  | ||||
|             if (!this.domainObject.configuration.axes.xKey || !this.domainObject.configuration.axes.yKey) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             const timestamp = this.getTimestampForDatum(data, key, telemetryObject); | ||||
|             let valueForTimestamp = this.valuesByTimestamp[timestamp] || {}; | ||||
|  | ||||
|             //populate x values | ||||
|             let metadataKey = this.domainObject.configuration.axes.xKey; | ||||
|             if (data[metadataKey] !== undefined) { | ||||
|                 valueForTimestamp.x = this.format(key, metadataKey, data); | ||||
|             } | ||||
|  | ||||
|             metadataKey = this.domainObject.configuration.axes.yKey; | ||||
|             if (data[metadataKey] !== undefined) { | ||||
|                 valueForTimestamp.y = this.format(key, metadataKey, data); | ||||
|             } | ||||
|  | ||||
|             this.valuesByTimestamp[timestamp] = valueForTimestamp; | ||||
|         }, | ||||
|         updateTrace(telemetryObject) { | ||||
|             const xAndyValues = Object.values(this.valuesByTimestamp); | ||||
|             const xValues = xAndyValues.map(value => value.x); | ||||
|             const yValues = xAndyValues.map(value => value.y); | ||||
|             const axisMetadata = this.getAxisMetadata(telemetryObject); | ||||
|             const xAxisMetadata = axisMetadata.find(metadata => metadata.source === this.domainObject.configuration.axes.xKey); | ||||
|             let yAxisMetadata = {}; | ||||
|             if (this.domainObject.configuration.axes.yKey) { | ||||
|                 yAxisMetadata = axisMetadata.find(metadata => metadata.source === this.domainObject.configuration.axes.yKey); | ||||
|             } | ||||
|  | ||||
|             let trace = { | ||||
|                 key: this.openmct.objects.makeKeyString(this.domainObject.identifier), | ||||
|                 name: this.domainObject.name, | ||||
|                 x: xValues, | ||||
|                 y: yValues, | ||||
|                 text: yValues.map(String), | ||||
|                 xAxisMetadata: xAxisMetadata, | ||||
|                 yAxisMetadata: yAxisMetadata, | ||||
|                 type: 'scatter', | ||||
|                 mode: 'markers', | ||||
|                 marker: { | ||||
|                     color: this.domainObject.configuration.styles.color | ||||
|                 }, | ||||
|                 hoverinfo: 'x+y' | ||||
|             }; | ||||
|  | ||||
|             if (this.domainObject.configuration.ranges !== undefined && this.domainObject.configuration.ranges.domainMin !== undefined && this.domainObject.configuration.ranges.domainMax !== undefined) { | ||||
|                 trace.xaxis = { | ||||
|                     min: this.domainObject.configuration.ranges.domainMin, | ||||
|                     max: this.domainObject.configuration.ranges.domainMax | ||||
|                 }; | ||||
|             } | ||||
|  | ||||
|             if (this.domainObject.configuration.ranges !== undefined && this.domainObject.configuration.ranges.rangeMin !== undefined && this.domainObject.configuration.ranges.rangeMax !== undefined) { | ||||
|                 trace.yaxis = { | ||||
|                     min: this.domainObject.configuration.ranges.rangeMin, | ||||
|                     max: this.domainObject.configuration.ranges.rangeMax | ||||
|                 }; | ||||
|             } | ||||
|  | ||||
|             this.trace = [trace]; | ||||
|         }, | ||||
|         getTimestampForDatum(datum, key, telemetryObject) { | ||||
|             const timeSystemKey = this.timeContext.timeSystem().key; | ||||
|             const metadata = this.openmct.telemetry.getMetadata(telemetryObject); | ||||
|             let metadataValue = metadata.value(timeSystemKey) || { format: timeSystemKey }; | ||||
|  | ||||
|             return this.parse(key, metadataValue.source, datum); | ||||
|         }, | ||||
|         format(telemetryObjectKey, metadataKey, data) { | ||||
|             const formats = this.telemetryObjectFormats[telemetryObjectKey]; | ||||
|  | ||||
|             return formats[metadataKey].format(data); | ||||
|         }, | ||||
|         parse(telemetryObjectKey, metadataKey, datum) { | ||||
|             if (!datum) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             const formats = this.telemetryObjectFormats[telemetryObjectKey]; | ||||
|  | ||||
|             return formats[metadataKey].parse(datum); | ||||
|         }, | ||||
|         getOptions() { | ||||
|             const { start, end } = this.timeContext.bounds(); | ||||
|  | ||||
|             return { | ||||
|                 end, | ||||
|                 start | ||||
|             }; | ||||
|         }, | ||||
|         subscribeToObject(telemetryObject) { | ||||
|             const key = this.openmct.objects.makeKeyString(telemetryObject.identifier); | ||||
|  | ||||
|             this.removeSubscription(key); | ||||
|  | ||||
|             const options = this.getOptions(); | ||||
|             const unsubscribe = this.openmct.telemetry.subscribe(telemetryObject, | ||||
|                 data => this.addDataToGraph(telemetryObject, data) | ||||
|                 , options); | ||||
|  | ||||
|             this.subscriptions.push({ | ||||
|                 key, | ||||
|                 unsubscribe | ||||
|             }); | ||||
|         }, | ||||
|         subscribeToAll() { | ||||
|             const telemetryObjects = Object.values(this.telemetryObjects); | ||||
|             telemetryObjects.forEach(this.subscribeToObject); | ||||
|         }, | ||||
|         removeAllSubscriptions() { | ||||
|             this.subscriptions.forEach(subscription => subscription.unsubscribe()); | ||||
|             this.subscriptions = []; | ||||
|         }, | ||||
|         removeSubscription(key) { | ||||
|             const found = this.subscriptions.findIndex(subscription => subscription.key === key); | ||||
|             if (found > -1) { | ||||
|                 this.subscriptions[found].unsubscribe(); | ||||
|                 this.subscriptions.splice(found, 1); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|  | ||||
| </script> | ||||
							
								
								
									
										79
									
								
								src/plugins/charts/scatter/ScatterPlotViewProvider.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								src/plugins/charts/scatter/ScatterPlotViewProvider.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,79 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2021, 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 ScatterPlotView from './ScatterPlotView.vue'; | ||||
| import { SCATTER_PLOT_KEY, SCATTER_PLOT_VIEW, TIME_STRIP_KEY } from './scatterPlotConstants.js'; | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| export default function ScatterPlotViewProvider(openmct) { | ||||
|     function isCompactView(objectPath) { | ||||
|         let isChildOfTimeStrip = objectPath.find(object => object.type === TIME_STRIP_KEY); | ||||
|  | ||||
|         return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath); | ||||
|     } | ||||
|  | ||||
|     return { | ||||
|         key: SCATTER_PLOT_VIEW, | ||||
|         name: 'Scatter Plot', | ||||
|         cssClass: 'icon-telemetry', | ||||
|         canView(domainObject, objectPath) { | ||||
|             return domainObject && domainObject.type === SCATTER_PLOT_KEY; | ||||
|         }, | ||||
|  | ||||
|         canEdit(domainObject, objectPath) { | ||||
|             return domainObject && domainObject.type === SCATTER_PLOT_KEY; | ||||
|         }, | ||||
|  | ||||
|         view: function (domainObject, objectPath) { | ||||
|             let component; | ||||
|  | ||||
|             return { | ||||
|                 show: function (element) { | ||||
|                     let isCompact = isCompactView(objectPath); | ||||
|                     component = new Vue({ | ||||
|                         el: element, | ||||
|                         components: { | ||||
|                             ScatterPlotView | ||||
|                         }, | ||||
|                         provide: { | ||||
|                             openmct, | ||||
|                             domainObject, | ||||
|                             path: objectPath | ||||
|                         }, | ||||
|                         data() { | ||||
|                             return { | ||||
|                                 options: { | ||||
|                                     compact: isCompact | ||||
|                                 } | ||||
|                             }; | ||||
|                         }, | ||||
|                         template: '<scatter-plot-view :options="options"></scatter-plot-view>' | ||||
|                     }); | ||||
|                 }, | ||||
|                 destroy: function () { | ||||
|                     component.$destroy(); | ||||
|                     component = undefined; | ||||
|                 } | ||||
|             }; | ||||
|         } | ||||
|     }; | ||||
| } | ||||
							
								
								
									
										393
									
								
								src/plugins/charts/scatter/ScatterPlotWithUnderlay.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										393
									
								
								src/plugins/charts/scatter/ScatterPlotWithUnderlay.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,393 @@ | ||||
| <template> | ||||
| <div | ||||
|     ref="plotWrapper" | ||||
|     class="has-local-controls" | ||||
|     :class="{ 's-unsynced' : isZoomed }" | ||||
| > | ||||
|     <div | ||||
|         v-if="isZoomed" | ||||
|         class="l-state-indicators" | ||||
|     > | ||||
|         <span | ||||
|             class="l-state-indicators__alert-no-lad t-object-alert t-alert-unsynced icon-alert-triangle" | ||||
|             title="This plot is not currently displaying the latest data. Reset pan/zoom to view latest data." | ||||
|         ></span> | ||||
|     </div> | ||||
|     <div | ||||
|         ref="plot" | ||||
|         class="c-scatter-chart" | ||||
|     ></div> | ||||
|     <div | ||||
|         ref="localControl" | ||||
|         class="gl-plot__local-controls h-local-controls h-local-controls--overlay-content c-local-controls--show-on-hover" | ||||
|     > | ||||
|         <button | ||||
|             v-if="data.length" | ||||
|             class="c-button icon-reset" | ||||
|             :disabled="!isZoomed" | ||||
|             title="Reset pan/zoom" | ||||
|             @click="reset()" | ||||
|         > | ||||
|         </button> | ||||
|     </div> | ||||
| </div> | ||||
| </template> | ||||
| <script> | ||||
| import Plotly from 'plotly-basic'; | ||||
|  | ||||
| const MULTI_AXES_X_PADDING_PERCENT = { | ||||
|     LEFT: 8, | ||||
|     RIGHT: 94 | ||||
| }; | ||||
|  | ||||
| import { getValidatedData } from "@/plugins/plan/util"; | ||||
|  | ||||
| const PATH_COLORS = ['blue', 'red', 'green']; | ||||
| const MARKER_COLOR = 'white'; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct', 'domainObject'], | ||||
|     props: { | ||||
|         data: { | ||||
|             type: Array, | ||||
|             default() { | ||||
|                 return []; | ||||
|             } | ||||
|         }, | ||||
|         plotAxisTitle: { | ||||
|             type: Object, | ||||
|             default() { | ||||
|                 return {}; | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             isZoomed: false, | ||||
|             yAxisRange: { | ||||
|                 min: '', | ||||
|                 max: '' | ||||
|             }, | ||||
|             xAxisRange: { | ||||
|                 min: '', | ||||
|                 max: '' | ||||
|             } | ||||
|         }; | ||||
|     }, | ||||
|     watch: { | ||||
|         data: { | ||||
|             immediate: false, | ||||
|             handler: 'updateData' | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.getUnderlayPlotData(); | ||||
|  | ||||
|         Plotly.newPlot(this.$refs.plot, Array.from(this.data.concat(this.getShapes(this.shapesData))), this.getLayout(), { | ||||
|             responsive: true, | ||||
|             displayModeBar: false | ||||
|         }); | ||||
|         this.registerListeners(); | ||||
|  | ||||
|         this.$refs.plot.on('plotly_relayout', this.zoom); | ||||
|     }, | ||||
|     beforeDestroy() { | ||||
|         if (this.$refs.plot && this.$refs.plot.off) { | ||||
|             this.$refs.plot.off('plotly_relayout', this.zoom); | ||||
|         } | ||||
|  | ||||
|         if (this.plotResizeObserver) { | ||||
|             this.plotResizeObserver.unobserve(this.$refs.plotWrapper); | ||||
|             clearTimeout(this.resizeTimer); | ||||
|         } | ||||
|  | ||||
|         if (this.unlistenUnderlay) { | ||||
|             this.unlistenUnderlay(); | ||||
|         } | ||||
|  | ||||
|         if (this.unlistenUnderlayRanges) { | ||||
|             this.unlistenUnderlayRanges(); | ||||
|         } | ||||
|  | ||||
|         if (this.unobserveColorChanges) { | ||||
|             this.unobserveColorChanges(); | ||||
|         } | ||||
|     }, | ||||
|     methods: { | ||||
|         getUnderlayPlotData() { | ||||
|             if (this.domainObject.selectFile) { | ||||
|                 this.shapesData = getValidatedData(this.domainObject); | ||||
|             } else { | ||||
|                 this.shapesData = []; | ||||
|             } | ||||
|         }, | ||||
|         observeForUnderlayPlotChanges() { | ||||
|             this.getUnderlayPlotData(); | ||||
|             this.updateData(); | ||||
|         }, | ||||
|         getAxisMinMax() { | ||||
|             if (!this.data.length) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             // For now, use x and y axes min, max values only if an underlay is available | ||||
|             if (this.shapesData.length && this.data[0].xaxis) { | ||||
|                 this.xAxisRange = this.data[0].xaxis; | ||||
|             } | ||||
|  | ||||
|             if (this.shapesData.length && this.data[0].yaxis) { | ||||
|                 this.yAxisRange = this.data[0].yaxis; | ||||
|             } | ||||
|         }, | ||||
|         getLayout() { | ||||
|             this.getAxisMinMax(); | ||||
|  | ||||
|             const yAxesMeta = this.getYAxisMeta(); | ||||
|             const primaryYaxis = this.getYaxisLayout(yAxesMeta['1']); | ||||
|             const xAxisDomain = this.getXAxisDomain(yAxesMeta); | ||||
|  | ||||
|             const shapes = this.shapesData.map((shapeData, index) => { | ||||
|                 if (!shapeData.x || !shapeData.y | ||||
|                 || !shapeData.x.length || !shapeData.y.length | ||||
|                 || shapeData.x.length !== shapeData.y.length) { | ||||
|                     return ""; | ||||
|                 } | ||||
|  | ||||
|                 let path = `M ${shapeData.x[0]},${shapeData.y[0]}`; | ||||
|                 shapeData.x.forEach((point, shapeIndex) => { | ||||
|                     if (shapeIndex > 0) { | ||||
|                         path = `${path} L${point},${shapeData.y[shapeIndex]}`; | ||||
|                     } | ||||
|                 }); | ||||
|  | ||||
|                 return { | ||||
|                     path, | ||||
|                     type: 'path', | ||||
|                     line: { | ||||
|                         color: PATH_COLORS[index] | ||||
|                     }, | ||||
|                     opacity: 0.5 | ||||
|                 }; | ||||
|             }); | ||||
|  | ||||
|             return { | ||||
|                 autosize: true, | ||||
|                 showlegend: false, | ||||
|                 textposition: 'auto', | ||||
|                 font: { | ||||
|                     family: 'Helvetica Neue, Helvetica, Arial, sans-serif', | ||||
|                     size: '12px', | ||||
|                     color: '#666' | ||||
|                 }, | ||||
|                 xaxis: { | ||||
|                     domain: xAxisDomain, | ||||
|                     range: [this.xAxisRange.min, this.xAxisRange.max], | ||||
|                     title: this.plotAxisTitle.xAxisTitle, | ||||
|                     automargin: true | ||||
|                 }, | ||||
|                 yaxis: primaryYaxis, | ||||
|                 margin: { | ||||
|                     l: 5, | ||||
|                     r: 5, | ||||
|                     t: 5, | ||||
|                     b: 0 | ||||
|                 }, | ||||
|                 paper_bgcolor: 'transparent', | ||||
|                 plot_bgcolor: 'transparent', | ||||
|                 shapes, | ||||
|                 layer: 'below' | ||||
|             }; | ||||
|         }, | ||||
|         getYAxisMeta() { | ||||
|             const yAxisMeta = {}; | ||||
|  | ||||
|             this.data.forEach(datum => { | ||||
|                 const yAxisMetadata = datum.yAxisMetadata; | ||||
|                 const range = '1'; | ||||
|                 const side = 'left'; | ||||
|                 const name = yAxisMetadata.name; | ||||
|                 const unit = yAxisMetadata.units; | ||||
|  | ||||
|                 yAxisMeta[range] = { | ||||
|                     range, | ||||
|                     side, | ||||
|                     name, | ||||
|                     unit | ||||
|                 }; | ||||
|             }); | ||||
|  | ||||
|             return yAxisMeta; | ||||
|         }, | ||||
|         getXAxisDomain(yAxisMeta) { | ||||
|             let leftPaddingPerc = 0; | ||||
|             let rightPaddingPerc = 100; | ||||
|             let rightSide = yAxisMeta && Object.values(yAxisMeta).filter((axisMeta => axisMeta.side === 'right')); | ||||
|             let leftSide = yAxisMeta && Object.values(yAxisMeta).filter((axisMeta => axisMeta.side === 'left')); | ||||
|             if (yAxisMeta && rightSide.length > 1) { | ||||
|                 rightPaddingPerc = MULTI_AXES_X_PADDING_PERCENT.RIGHT; | ||||
|             } | ||||
|  | ||||
|             if (yAxisMeta && leftSide.length > 1) { | ||||
|                 leftPaddingPerc = MULTI_AXES_X_PADDING_PERCENT.LEFT; | ||||
|             } | ||||
|  | ||||
|             return [leftPaddingPerc / 100, rightPaddingPerc / 100]; | ||||
|         }, | ||||
|         getYaxisLayout(yAxisMeta) { | ||||
|             if (!yAxisMeta) { | ||||
|                 return {}; | ||||
|             } | ||||
|  | ||||
|             const { name, range, side = 'left', unit } = yAxisMeta; | ||||
|             const title = `${name} ${unit ? '(' + unit + ')' : ''}`; | ||||
|             const yaxis = { | ||||
|                 automargin: true, | ||||
|                 title | ||||
|             }; | ||||
|  | ||||
|             let yRange = this.yAxisRange; | ||||
|             if (range === '1') { | ||||
|                 yaxis.range = [yRange.min, yRange.max]; | ||||
|  | ||||
|                 return yaxis; | ||||
|             } | ||||
|  | ||||
|             yaxis.range = [yRange.min, yRange.max]; | ||||
|             yaxis.anchor = side.toLowerCase() === 'left' | ||||
|                 ? 'free' | ||||
|                 : 'x'; | ||||
|             yaxis.showline = side.toLowerCase() === 'left'; | ||||
|             yaxis.side = side.toLowerCase(); | ||||
|             yaxis.overlaying = 'y'; | ||||
|             yaxis.position = 0.01; | ||||
|  | ||||
|             return yaxis; | ||||
|         }, | ||||
|         registerListeners() { | ||||
|             this.unobserveColorChanges = this.openmct.objects.observe(this.domainObject, 'configuration.styles.color', this.updateColors); | ||||
|             this.unlistenUnderlay = this.openmct.objects.observe(this.domainObject, 'selectFile', this.observeForUnderlayPlotChanges); | ||||
|             this.unlistenUnderlayRanges = this.openmct.objects.observe(this.domainObject, 'configuration.ranges', this.updateData); | ||||
|             this.resizeTimer = false; | ||||
|             if (window.ResizeObserver) { | ||||
|                 this.plotResizeObserver = new ResizeObserver(() => { | ||||
|                     // debounce and trigger window resize so that plotly can resize the plot | ||||
|                     clearTimeout(this.resizeTimer); | ||||
|                     this.resizeTimer = setTimeout(() => { | ||||
|                         window.dispatchEvent(new Event('resize')); | ||||
|                     }, 250); | ||||
|                 }); | ||||
|                 this.plotResizeObserver.observe(this.$refs.plotWrapper); | ||||
|             } | ||||
|         }, | ||||
|         updateColors() { | ||||
|             const colors = []; | ||||
|             const indices = []; | ||||
|             this.data.forEach((item, index) => { | ||||
|                 const colorExists = this.domainObject.configuration.styles.color; | ||||
|                 indices.push(index); | ||||
|                 if (colorExists) { | ||||
|                     colors.push(this.domainObject.configuration.styles.color); | ||||
|                 } else { | ||||
|                     colors.push(item.marker.color); | ||||
|                 } | ||||
|             }); | ||||
|             const plotUpdate = { | ||||
|                 'marker.color': colors | ||||
|             }; | ||||
|  | ||||
|             Plotly.restyle(this.$refs.plot, plotUpdate, indices); | ||||
|         }, | ||||
|         reset() { | ||||
|             this.isZoomed = false; | ||||
|  | ||||
|             this.updatePlot(); | ||||
|             this.$emit('subscribe'); | ||||
|         }, | ||||
|         updateData() { | ||||
|             this.updatePlot(); | ||||
|         }, | ||||
|         updateLocalControlPosition() { | ||||
|             const localControl = this.$refs.localControl; | ||||
|             localControl.style.display = 'none'; | ||||
|  | ||||
|             const plot = this.$refs.plot; | ||||
|             const bgLayer = this.$el.querySelector('.bglayer'); | ||||
|  | ||||
|             const plotBoundingRect = plot.getBoundingClientRect(); | ||||
|             const bgLayerBoundingRect = bgLayer.getBoundingClientRect(); | ||||
|  | ||||
|             const top = bgLayerBoundingRect.top - plotBoundingRect.top + 5; | ||||
|             const left = bgLayerBoundingRect.left - plotBoundingRect.left + 5; | ||||
|  | ||||
|             localControl.style.top = `${top}px`; | ||||
|             localControl.style.left = `${left}px`; | ||||
|             localControl.style.display = 'block'; | ||||
|         }, | ||||
|         updatePlot() { | ||||
|             if (!this.$refs || !this.$refs.plot || this.isZoomed) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             Plotly.react(this.$refs.plot, Array.from(this.data.concat(this.getShapes(this.shapesData))), this.getLayout()); | ||||
|         }, | ||||
|         zoom(eventData) { | ||||
|             const autorange = eventData['xaxis.autorange']; | ||||
|             const { autosize } = eventData; | ||||
|  | ||||
|             if (autosize || autorange) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             this.isZoomed = true; | ||||
|             this.$emit('unsubscribe'); | ||||
|         }, | ||||
|         getShapes() { | ||||
|             let markerData = { | ||||
|                 x: [], | ||||
|                 y: [] | ||||
|             }; | ||||
|             const shapes = this.shapesData.map((shapeData, index) => { | ||||
|                 if (!shapeData.x || !shapeData.y | ||||
|               || !shapeData.x.length || !shapeData.y.length | ||||
|               || shapeData.x.length !== shapeData.y.length) { | ||||
|                     return ""; | ||||
|                 } | ||||
|  | ||||
|                 let text = []; | ||||
|                 shapeData.x.forEach((point) => { | ||||
|                     text.push(`${parseFloat(point).toPrecision(2)}`); | ||||
|                 }); | ||||
|  | ||||
|                 markerData.x = markerData.x.concat(shapeData.x); | ||||
|                 markerData.y = markerData.y.concat(shapeData.y); | ||||
|  | ||||
|                 return { | ||||
|                     x: shapeData.x, | ||||
|                     y: shapeData.y, | ||||
|                     mode: 'text', | ||||
|                     text, | ||||
|                     textfont: { | ||||
|                         family: 'Helvetica Neue, Helvetica, Arial, sans-serif', | ||||
|                         size: '12px', | ||||
|                         color: PATH_COLORS[index] | ||||
|                     }, | ||||
|                     opacity: 0.5 | ||||
|                 }; | ||||
|             }); | ||||
|  | ||||
|             shapes.push({ | ||||
|                 x: markerData.x, | ||||
|                 y: markerData.y, | ||||
|                 mode: "markers", | ||||
|                 marker: { | ||||
|                     size: 6, | ||||
|                     color: MARKER_COLOR | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             return shapes; | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
|  | ||||
							
								
								
									
										64
									
								
								src/plugins/charts/scatter/inspector/PlotOptions.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								src/plugins/charts/scatter/inspector/PlotOptions.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | ||||
| <!-- | ||||
|  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> | ||||
| <div> | ||||
|     <div v-if="canEdit"> | ||||
|         <plot-options-edit /> | ||||
|     </div> | ||||
|     <div v-else> | ||||
|         <plot-options-browse /> | ||||
|     </div> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import PlotOptionsBrowse from "./PlotOptionsBrowse.vue"; | ||||
| import PlotOptionsEdit from "./PlotOptionsEdit.vue"; | ||||
| export default { | ||||
|     components: { | ||||
|         PlotOptionsBrowse, | ||||
|         PlotOptionsEdit | ||||
|     }, | ||||
|     inject: ['openmct', 'domainObject'], | ||||
|     data() { | ||||
|         return { | ||||
|             isEditing: this.openmct.editor.isEditing() | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|         canEdit() { | ||||
|             return this.isEditing && !this.domainObject.locked; | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.openmct.editor.on('isEditing', this.setEditState); | ||||
|     }, | ||||
|     beforeDestroy() { | ||||
|         this.openmct.editor.off('isEditing', this.setEditState); | ||||
|     }, | ||||
|     methods: { | ||||
|         setEditState(isEditing) { | ||||
|             this.isEditing = isEditing; | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
							
								
								
									
										153
									
								
								src/plugins/charts/scatter/inspector/PlotOptionsBrowse.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										153
									
								
								src/plugins/charts/scatter/inspector/PlotOptionsBrowse.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,153 @@ | ||||
| <!-- | ||||
|  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> | ||||
| <div class="js-plot-options-browse grid-properties"> | ||||
|     <ul class="l-inspector-part"> | ||||
|         <h2 title="Object view settings">Settings</h2> | ||||
|         <li class="grid-row"> | ||||
|             <div | ||||
|                 class="grid-cell label" | ||||
|                 title="X axis selection" | ||||
|             >X Axis</div> | ||||
|             <div class="grid-cell value">{{ xKeyLabel }}</div> | ||||
|         </li> | ||||
|         <li class="grid-row"> | ||||
|             <div | ||||
|                 class="grid-cell label" | ||||
|                 title="Y axis selection" | ||||
|             >Y Axis</div> | ||||
|             <div class="grid-cell value">{{ yKeyLabel }}</div> | ||||
|         </li> | ||||
|         <ColorSwatch | ||||
|             :current-color="currentColor" | ||||
|             edit-title="Manually set the color for this plot" | ||||
|             view-title="The marker color for this plot" | ||||
|             short-label="Color" | ||||
|         /> | ||||
|     </ul> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import ColorSwatch from "../../../../ui/color/ColorSwatch.vue"; | ||||
| import Color from "../../../../ui/color/Color"; | ||||
| import ColorPalette from "../../../../ui/color/ColorPalette"; | ||||
|  | ||||
| export default { | ||||
|     components: { ColorSwatch }, | ||||
|     inject: ['openmct', 'domainObject'], | ||||
|     data() { | ||||
|         return { | ||||
|             xKeyLabel: '', | ||||
|             yKeyLabel: '', | ||||
|             currentColor: undefined | ||||
|         }; | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.plotSeries = []; | ||||
|         this.colorPalette = new ColorPalette(); | ||||
|         this.initColor(); | ||||
|         this.composition = this.openmct.composition.get(this.domainObject); | ||||
|         this.registerListeners(); | ||||
|         this.composition.load(); | ||||
|     }, | ||||
|     beforeDestroy() { | ||||
|         this.stopListening(); | ||||
|     }, | ||||
|     methods: { | ||||
|         initColor() { | ||||
|         // this is called before the plot is initialized | ||||
|             if (!this.domainObject.configuration.styles || !this.domainObject.configuration.styles.color) { | ||||
|                 const color = this.colorPalette.getNextColor().asHexString(); | ||||
|                 this.domainObject.configuration.styles = { | ||||
|                     color | ||||
|                 }; | ||||
|             } | ||||
|  | ||||
|             this.currentColor = this.domainObject.configuration.styles.color; | ||||
|             const colorObject = Color.fromHexString(this.currentColor); | ||||
|  | ||||
|             this.colorPalette.remove(colorObject); | ||||
|         }, | ||||
|         registerListeners() { | ||||
|             this.composition.on('add', this.addSeries); | ||||
|             this.composition.on('remove', this.removeSeries); | ||||
|             this.unobserve = this.openmct.objects.observe(this.domainObject, 'configuration.axes', this.setAxesLabels); | ||||
|         }, | ||||
|         stopListening() { | ||||
|             this.composition.off('add', this.addSeries); | ||||
|             this.composition.off('remove', this.removeSeries); | ||||
|             if (this.unobserve) { | ||||
|                 this.unobserve(); | ||||
|             } | ||||
|         }, | ||||
|         addSeries(series, index) { | ||||
|             this.$set(this.plotSeries, this.plotSeries.length, series); | ||||
|             this.setAxesLabels(); | ||||
|         }, | ||||
|         removeSeries(series) { | ||||
|             const index = this.plotSeries.findIndex(plotSeries => this.openmct.objects.areIdsEqual(series.identifier, plotSeries.identifier)); | ||||
|             if (index !== undefined) { | ||||
|                 this.$delete(this.plotSeries, index); | ||||
|                 this.setAxesLabels(); | ||||
|             } | ||||
|         }, | ||||
|         setAxesLabels() { | ||||
|             let xKeyOptions = []; | ||||
|             let yKeyOptions = []; | ||||
|             if (this.plotSeries.length <= 0) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             const series = this.plotSeries[0]; | ||||
|             const metadataValues = this.openmct.telemetry.getMetadata(series).valuesForHints(['range']); | ||||
|  | ||||
|             metadataValues.forEach((metadataValue) => { | ||||
|                 xKeyOptions.push({ | ||||
|                     name: metadataValue.name || metadataValue.key, | ||||
|                     value: metadataValue.source || metadataValue.key | ||||
|                 }); | ||||
|                 yKeyOptions.push({ | ||||
|                     name: metadataValue.name || metadataValue.key, | ||||
|                     value: metadataValue.source || metadataValue.key | ||||
|                 }); | ||||
|             }); | ||||
|             let xKeyOptionIndex; | ||||
|             let yKeyOptionIndex; | ||||
|  | ||||
|             if (this.domainObject.configuration.axes.xKey) { | ||||
|                 xKeyOptionIndex = xKeyOptions.findIndex(option => option.value === this.domainObject.configuration.axes.xKey); | ||||
|                 if (xKeyOptionIndex > -1) { | ||||
|                     this.xKeyLabel = xKeyOptions[xKeyOptionIndex].name; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             if (metadataValues.length > 1 && this.domainObject.configuration.axes.yKey) { | ||||
|                 yKeyOptionIndex = yKeyOptions.findIndex(option => option.value === this.domainObject.configuration.axes.yKey); | ||||
|                 if (yKeyOptionIndex > -1) { | ||||
|                     this.yKeyLabel = yKeyOptions[yKeyOptionIndex].name; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
							
								
								
									
										262
									
								
								src/plugins/charts/scatter/inspector/PlotOptionsEdit.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										262
									
								
								src/plugins/charts/scatter/inspector/PlotOptionsEdit.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,262 @@ | ||||
| <!-- | ||||
|  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> | ||||
| <div class="js-plot-options-edit grid-properties"> | ||||
|     <ul class="l-inspector-part"> | ||||
|         <h2 title="Object view settings">Settings</h2> | ||||
|         <li class="grid-row"> | ||||
|             <div | ||||
|                 class="grid-cell label" | ||||
|                 title="X axis selection." | ||||
|             >X Axis</div> | ||||
|             <div class="grid-cell value"> | ||||
|                 <select | ||||
|                     v-model="xKey" | ||||
|                     @change="updateForm('xKey')" | ||||
|                 > | ||||
|                     <option | ||||
|                         v-for="option in xKeyOptions" | ||||
|                         :key="`xKey-${option.value}`" | ||||
|                         :value="option.value" | ||||
|                         :selected="option.value == xKey" | ||||
|                     > | ||||
|                         {{ option.name }} | ||||
|                     </option> | ||||
|                 </select> | ||||
|             </div> | ||||
|         </li> | ||||
|         <li class="grid-row"> | ||||
|             <div | ||||
|                 class="grid-cell label" | ||||
|                 title="Y axis selection." | ||||
|             >Y Axis</div> | ||||
|             <div class="grid-cell value"> | ||||
|                 <select | ||||
|                     v-model="yKey" | ||||
|                     @change="updateForm('yKey')" | ||||
|                 > | ||||
|                     <option | ||||
|                         v-for="option in yKeyOptions" | ||||
|                         :key="`yKey-${option.value}`" | ||||
|                         :value="option.value" | ||||
|                         :selected="option.value == yKey" | ||||
|                     > | ||||
|                         {{ option.name }} | ||||
|                     </option> | ||||
|                 </select> | ||||
|             </div> | ||||
|         </li> | ||||
|         <ColorSwatch | ||||
|             :current-color="currentColor" | ||||
|             title="Manually set the line and marker color for this plot." | ||||
|             edit-title="Manually set the line and marker color for this plot." | ||||
|             view-title="The line and marker color for this plot." | ||||
|             short-label="Color" | ||||
|             @colorSet="setColor" | ||||
|         /> | ||||
|     </ul> | ||||
| </div> | ||||
| </template> | ||||
| <script> | ||||
| import Color from "../../../../ui/color/Color"; | ||||
| import ColorPalette from "../../../../ui/color/ColorPalette"; | ||||
| import ColorSwatch from "../../../../ui/color/ColorSwatch.vue"; | ||||
|  | ||||
| export default { | ||||
|     components: { ColorSwatch }, | ||||
|     inject: ['openmct', 'domainObject'], | ||||
|     data() { | ||||
|         return { | ||||
|             xKey: undefined, | ||||
|             yKey: undefined, | ||||
|             xKeyOptions: [], | ||||
|             yKeyOptions: [], | ||||
|             currentColor: undefined | ||||
|         }; | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.plotSeries = []; | ||||
|         this.colorPalette = new ColorPalette(); | ||||
|         this.initColor(); | ||||
|         this.composition = this.openmct.composition.get(this.domainObject); | ||||
|         this.registerListeners(); | ||||
|         this.composition.load(); | ||||
|     }, | ||||
|     beforeDestroy() { | ||||
|         this.stopListening(); | ||||
|     }, | ||||
|     methods: { | ||||
|         initColor() { | ||||
|         // this is called before the plot is initialized | ||||
|             if (!this.domainObject.configuration.styles || !this.domainObject.configuration.styles.color) { | ||||
|                 const color = this.colorPalette.getNextColor().asHexString(); | ||||
|                 this.domainObject.configuration.styles = { | ||||
|                     color | ||||
|                 }; | ||||
|             } | ||||
|  | ||||
|             this.currentColor = this.domainObject.configuration.styles.color; | ||||
|             const colorObject = Color.fromHexString(this.currentColor); | ||||
|  | ||||
|             this.colorPalette.remove(colorObject); | ||||
|         }, | ||||
|         setColor(chosenColor) { | ||||
|             this.currentColor = chosenColor.asHexString(); | ||||
|             this.openmct.objects.mutate( | ||||
|                 this.domainObject, | ||||
|                 `configuration.styles.color`, | ||||
|                 this.currentColor | ||||
|             ); | ||||
|         }, | ||||
|         registerListeners() { | ||||
|             this.composition.on('add', this.addSeries); | ||||
|             this.composition.on('remove', this.removeSeries); | ||||
|             this.unobserve = this.openmct.objects.observe(this.domainObject, 'configuration.axes', this.setupOptions); | ||||
|         }, | ||||
|         stopListening() { | ||||
|             this.composition.off('add', this.addSeries); | ||||
|             this.composition.off('remove', this.removeSeries); | ||||
|             if (this.unobserve) { | ||||
|                 this.unobserve(); | ||||
|             } | ||||
|         }, | ||||
|         addSeries(series, index) { | ||||
|             this.$set(this.plotSeries, this.plotSeries.length, series); | ||||
|             this.setupOptions(); | ||||
|         }, | ||||
|         removeSeries(seriesIdentifier) { | ||||
|             const index = this.plotSeries.findIndex(plotSeries => this.openmct.objects.areIdsEqual(seriesIdentifier, plotSeries.identifier)); | ||||
|             if (index >= 0) { | ||||
|                 this.$delete(this.plotSeries, index); | ||||
|                 this.setupOptions(); | ||||
|             } | ||||
|         }, | ||||
|         setupOptions() { | ||||
|             this.xKeyOptions = []; | ||||
|             this.yKeyOptions = []; | ||||
|             if (this.plotSeries.length <= 0) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             let update = false; | ||||
|             const series = this.plotSeries[0]; | ||||
|             const metadataValues = this.openmct.telemetry.getMetadata(series).valuesForHints(['range']); | ||||
|             metadataValues.forEach((metadataValue) => { | ||||
|                 this.xKeyOptions.push({ | ||||
|                     name: metadataValue.name || metadataValue.key, | ||||
|                     value: metadataValue.source || metadataValue.key | ||||
|                 }); | ||||
|                 this.yKeyOptions.push({ | ||||
|                     name: metadataValue.name || metadataValue.key, | ||||
|                     value: metadataValue.source || metadataValue.key | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             let xKeyOptionIndex; | ||||
|             let yKeyOptionIndex; | ||||
|  | ||||
|             if (this.domainObject.configuration.axes.xKey) { | ||||
|                 xKeyOptionIndex = this.xKeyOptions.findIndex(option => option.value === this.domainObject.configuration.axes.xKey); | ||||
|                 if (xKeyOptionIndex > -1) { | ||||
|                     this.xKey = this.xKeyOptions[xKeyOptionIndex].value; | ||||
|                 } else { | ||||
|                     this.xKey = undefined; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             if (this.xKey === undefined) { | ||||
|                 update = true; | ||||
|                 xKeyOptionIndex = 0; | ||||
|                 this.xKey = this.xKeyOptions[xKeyOptionIndex].value; | ||||
|             } | ||||
|  | ||||
|             if (metadataValues.length > 1) { | ||||
|                 if (this.domainObject.configuration.axes.yKey) { | ||||
|                     yKeyOptionIndex = this.yKeyOptions.findIndex(option => option.value === this.domainObject.configuration.axes.yKey); | ||||
|                     if (yKeyOptionIndex > -1 && yKeyOptionIndex !== xKeyOptionIndex) { | ||||
|                         this.yKey = this.yKeyOptions[yKeyOptionIndex].value; | ||||
|                     } else { | ||||
|                         this.yKey = undefined; | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 if (this.yKey === undefined) { | ||||
|                     update = true; | ||||
|                     yKeyOptionIndex = this.yKeyOptions.findIndex((option, index) => index !== xKeyOptionIndex); | ||||
|                     this.yKey = this.yKeyOptions[yKeyOptionIndex].value; | ||||
|                 } | ||||
|  | ||||
|                 this.yKeyOptions = this.yKeyOptions.map((option, index) => { | ||||
|                     if (index === xKeyOptionIndex) { | ||||
|                         option.name = `${option.name} (swap)`; | ||||
|                         option.swap = yKeyOptionIndex; | ||||
|                     } else { | ||||
|                         option.name = option.name.replace(' (swap)', ''); | ||||
|                         option.swap = undefined; | ||||
|                     } | ||||
|  | ||||
|                     return option; | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|             this.xKeyOptions = this.xKeyOptions.map((option, index) => { | ||||
|                 if (index === yKeyOptionIndex) { | ||||
|                     option.name = `${option.name} (swap)`; | ||||
|                     option.swap = xKeyOptionIndex; | ||||
|                 } else { | ||||
|                     option.name = option.name.replace(' (swap)', ''); | ||||
|                     option.swap = undefined; | ||||
|                 } | ||||
|  | ||||
|                 return option; | ||||
|             }); | ||||
|  | ||||
|             if (update === true) { | ||||
|                 this.saveConfiguration(); | ||||
|             } | ||||
|         }, | ||||
|         updateForm(property) { | ||||
|             if (property === 'xKey') { | ||||
|                 const xKeyOption = this.xKeyOptions.find(option => option.value === this.xKey); | ||||
|                 if (xKeyOption.swap !== undefined) { | ||||
|                     //swap | ||||
|                     this.yKey = this.xKeyOptions[xKeyOption.swap].value; | ||||
|                 } | ||||
|             } else if (property === 'yKey') { | ||||
|                 const yKeyOption = this.yKeyOptions.find(option => option.value === this.yKey); | ||||
|                 if (yKeyOption.swap !== undefined) { | ||||
|                 //swap | ||||
|                     this.xKey = this.yKeyOptions[yKeyOption.swap].value; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             this.saveConfiguration(); | ||||
|         }, | ||||
|         saveConfiguration() { | ||||
|             this.openmct.objects.mutate(this.domainObject, `configuration.axes`, { | ||||
|                 xKey: this.xKey, | ||||
|                 yKey: this.yKey | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
| @@ -0,0 +1,48 @@ | ||||
| import { SCATTER_PLOT_INSPECTOR_KEY, SCATTER_PLOT_KEY } from '../scatterPlotConstants'; | ||||
| import Vue from 'vue'; | ||||
| import PlotOptions from "./PlotOptions.vue"; | ||||
|  | ||||
| export default function ScatterPlotInspectorViewProvider(openmct) { | ||||
|     return { | ||||
|         key: SCATTER_PLOT_INSPECTOR_KEY, | ||||
|         name: 'Bar Graph Inspector View', | ||||
|         canView: function (selection) { | ||||
|             if (selection.length === 0 || selection[0].length === 0) { | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             let object = selection[0][0].context.item; | ||||
|  | ||||
|             return object | ||||
|                 && object.type === SCATTER_PLOT_KEY; | ||||
|         }, | ||||
|         view: function (selection) { | ||||
|             let component; | ||||
|  | ||||
|             return { | ||||
|                 show: function (element) { | ||||
|                     component = new Vue({ | ||||
|                         el: element, | ||||
|                         components: { | ||||
|                             PlotOptions | ||||
|                         }, | ||||
|                         provide: { | ||||
|                             openmct, | ||||
|                             domainObject: selection[0][0].context.item | ||||
|                         }, | ||||
|                         template: '<plot-options></plot-options>' | ||||
|                     }); | ||||
|                 }, | ||||
|                 destroy: function () { | ||||
|                     if (component) { | ||||
|                         component.$destroy(); | ||||
|                         component = undefined; | ||||
|                     } | ||||
|                 } | ||||
|             }; | ||||
|         }, | ||||
|         priority: function () { | ||||
|             return 1; | ||||
|         } | ||||
|     }; | ||||
| } | ||||
							
								
								
									
										127
									
								
								src/plugins/charts/scatter/plugin.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								src/plugins/charts/scatter/plugin.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,127 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 { SCATTER_PLOT_KEY } from './scatterPlotConstants.js'; | ||||
| import ScatterPlotViewProvider from './ScatterPlotViewProvider'; | ||||
| import ScatterPlotInspectorViewProvider from './inspector/ScatterPlotInspectorViewProvider'; | ||||
| import ScatterPlotCompositionPolicy from './ScatterPlotCompositionPolicy'; | ||||
| import Vue from "vue"; | ||||
| import ScatterPlotForm from "./ScatterPlotForm.vue"; | ||||
|  | ||||
| export default function () { | ||||
|     return function install(openmct) { | ||||
|         openmct.forms.addNewFormControl('scatter-plot-form-control', getScatterPlotFormControl(openmct)); | ||||
|  | ||||
|         openmct.types.addType(SCATTER_PLOT_KEY, { | ||||
|             key: SCATTER_PLOT_KEY, | ||||
|             name: "Scatter Plot", | ||||
|             cssClass: "icon-plot-scatter", | ||||
|             description: "View data as a scatter plot.", | ||||
|             creatable: true, | ||||
|             initialize: function (domainObject) { | ||||
|                 domainObject.composition = []; | ||||
|                 domainObject.configuration = { | ||||
|                     styles: {}, | ||||
|                     axes: {}, | ||||
|                     ranges: {} | ||||
|                 }; | ||||
|             }, | ||||
|             form: [ | ||||
|                 { | ||||
|                     name: 'Underlay data (JSON file)', | ||||
|                     key: 'selectFile', | ||||
|                     control: 'file-input', | ||||
|                     text: 'Select File...', | ||||
|                     type: 'application/json', | ||||
|                     removable: true, | ||||
|                     hideFromInspector: true, | ||||
|                     property: [ | ||||
|                         "selectFile" | ||||
|                     ] | ||||
|                 }, | ||||
|                 { | ||||
|                     name: "Underlay ranges", | ||||
|                     control: "scatter-plot-form-control", | ||||
|                     cssClass: "l-input", | ||||
|                     key: "scatterPlotForm", | ||||
|                     required: false, | ||||
|                     hideFromInspector: false, | ||||
|                     property: [ | ||||
|                         "configuration", | ||||
|                         "ranges" | ||||
|                     ], | ||||
|                     validate: ({ value }, callback) => { | ||||
|                         const { rangeMin, rangeMax, domainMin, domainMax } = value; | ||||
|                         const valid = { | ||||
|                             rangeMin, | ||||
|                             rangeMax, | ||||
|                             domainMin, | ||||
|                             domainMax | ||||
|                         }; | ||||
|  | ||||
|                         if (callback) { | ||||
|                             callback(valid); | ||||
|                         } | ||||
|  | ||||
|                         const values = Object.values(valid); | ||||
|                         const hasAllValues = values.every(rangeValue => rangeValue !== undefined); | ||||
|                         const hasNoValues = values.every(rangeValue => rangeValue === undefined); | ||||
|  | ||||
|                         return hasAllValues || hasNoValues; | ||||
|                     } | ||||
|                 } | ||||
|             ], | ||||
|             priority: 891 | ||||
|         }); | ||||
|  | ||||
|         openmct.objectViews.addProvider(new ScatterPlotViewProvider(openmct)); | ||||
|  | ||||
|         openmct.inspectorViews.addProvider(new ScatterPlotInspectorViewProvider(openmct)); | ||||
|  | ||||
|         openmct.composition.addPolicy(new ScatterPlotCompositionPolicy(openmct).allow); | ||||
|     }; | ||||
|  | ||||
|     function getScatterPlotFormControl(openmct) { | ||||
|         return { | ||||
|             show(element, model, onChange) { | ||||
|                 const rowComponent = new Vue({ | ||||
|                     el: element, | ||||
|                     components: { | ||||
|                         ScatterPlotForm | ||||
|                     }, | ||||
|                     provide: { | ||||
|                         openmct | ||||
|                     }, | ||||
|                     data() { | ||||
|                         return { | ||||
|                             model, | ||||
|                             onChange | ||||
|                         }; | ||||
|                     }, | ||||
|                     template: `<scatter-plot-form :model="model" @onChange="onChange"></scatter-plot-form>` | ||||
|                 }); | ||||
|  | ||||
|                 return rowComponent; | ||||
|             } | ||||
|         }; | ||||
|     } | ||||
| } | ||||
|  | ||||
							
								
								
									
										421
									
								
								src/plugins/charts/scatter/pluginSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										421
									
								
								src/plugins/charts/scatter/pluginSpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,421 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 Vue from "vue"; | ||||
| import ScatterPlotPlugin from "./plugin"; | ||||
| import ScatterPlot from './ScatterPlotView.vue'; | ||||
| import EventEmitter from "EventEmitter"; | ||||
| import { SCATTER_PLOT_VIEW, SCATTER_PLOT_KEY } from './scatterPlotConstants'; | ||||
|  | ||||
| describe("the plugin", function () { | ||||
|     let element; | ||||
|     let child; | ||||
|     let openmct; | ||||
|     let telemetryPromise; | ||||
|     let telemetryPromiseResolve; | ||||
|     let mockObjectPath; | ||||
|  | ||||
|     beforeEach((done) => { | ||||
|         mockObjectPath = [ | ||||
|             { | ||||
|                 name: 'mock folder', | ||||
|                 type: 'fake-folder', | ||||
|                 identifier: { | ||||
|                     key: 'mock-folder', | ||||
|                     namespace: '' | ||||
|                 } | ||||
|             } | ||||
|         ]; | ||||
|         const testTelemetry = [ | ||||
|             { | ||||
|                 'utc': 1, | ||||
|                 'some-key': 'some-value 1', | ||||
|                 'some-other-key': 'some-other-value 1' | ||||
|             }, | ||||
|             { | ||||
|                 'utc': 2, | ||||
|                 'some-key': 'some-value 2', | ||||
|                 'some-other-key': 'some-other-value 2' | ||||
|             }, | ||||
|             { | ||||
|                 'utc': 3, | ||||
|                 'some-key': 'some-value 3', | ||||
|                 'some-other-key': 'some-other-value 3' | ||||
|             } | ||||
|         ]; | ||||
|  | ||||
|         openmct = createOpenMct(); | ||||
|  | ||||
|         telemetryPromise = new Promise((resolve) => { | ||||
|             telemetryPromiseResolve = resolve; | ||||
|         }); | ||||
|  | ||||
|         spyOn(openmct.telemetry, 'request').and.callFake(() => { | ||||
|             telemetryPromiseResolve(testTelemetry); | ||||
|  | ||||
|             return telemetryPromise; | ||||
|         }); | ||||
|  | ||||
|         openmct.install(new ScatterPlotPlugin()); | ||||
|  | ||||
|         element = document.createElement("div"); | ||||
|         element.style.width = "640px"; | ||||
|         element.style.height = "480px"; | ||||
|         child = document.createElement("div"); | ||||
|         child.style.width = "640px"; | ||||
|         child.style.height = "480px"; | ||||
|         element.appendChild(child); | ||||
|         document.body.appendChild(element); | ||||
|  | ||||
|         spyOn(window, 'ResizeObserver').and.returnValue({ | ||||
|             observe() {}, | ||||
|             unobserve() {}, | ||||
|             disconnect() {} | ||||
|         }); | ||||
|  | ||||
|         openmct.time.timeSystem("utc", { | ||||
|             start: 0, | ||||
|             end: 4 | ||||
|         }); | ||||
|  | ||||
|         openmct.types.addType("test-object", { | ||||
|             creatable: true | ||||
|         }); | ||||
|  | ||||
|         openmct.on("start", done); | ||||
|         openmct.startHeadless(); | ||||
|     }); | ||||
|  | ||||
|     afterEach((done) => { | ||||
|         openmct.time.timeSystem('utc', { | ||||
|             start: 0, | ||||
|             end: 1 | ||||
|         }); | ||||
|         resetApplicationState(openmct).then(done).catch(done); | ||||
|     }); | ||||
|  | ||||
|     describe("The scatter plot view", () => { | ||||
|         let testDomainObject; | ||||
|         let scatterPlotObject; | ||||
|         // eslint-disable-next-line no-unused-vars | ||||
|         let component; | ||||
|         let mockComposition; | ||||
|  | ||||
|         beforeEach(async () => { | ||||
|             scatterPlotObject = { | ||||
|                 identifier: { | ||||
|                     namespace: "", | ||||
|                     key: "test-plot" | ||||
|                 }, | ||||
|                 type: "telemetry.plot.scatter-plot", | ||||
|                 name: "Test Scatter Plot", | ||||
|                 configuration: { | ||||
|                     axes: {}, | ||||
|                     styles: {} | ||||
|                 } | ||||
|             }; | ||||
|  | ||||
|             testDomainObject = { | ||||
|                 identifier: { | ||||
|                     namespace: "", | ||||
|                     key: "test-object" | ||||
|                 }, | ||||
|                 type: "test-object", | ||||
|                 name: "Test Object", | ||||
|                 telemetry: { | ||||
|                     values: [{ | ||||
|                         key: "utc", | ||||
|                         format: "utc", | ||||
|                         name: "Time", | ||||
|                         hints: { | ||||
|                             domain: 1 | ||||
|                         } | ||||
|                     }, { | ||||
|                         key: "some-key", | ||||
|                         name: "Some attribute", | ||||
|                         hints: { | ||||
|                             range: 1 | ||||
|                         } | ||||
|                     }, { | ||||
|                         key: "some-other-key", | ||||
|                         name: "Another attribute", | ||||
|                         hints: { | ||||
|                             range: 2 | ||||
|                         } | ||||
|                     }] | ||||
|                 } | ||||
|             }; | ||||
|  | ||||
|             mockComposition = new EventEmitter(); | ||||
|             mockComposition.load = () => { | ||||
|                 mockComposition.emit('add', testDomainObject); | ||||
|  | ||||
|                 return [testDomainObject]; | ||||
|             }; | ||||
|  | ||||
|             spyOn(openmct.composition, 'get').and.returnValue(mockComposition); | ||||
|  | ||||
|             let viewContainer = document.createElement("div"); | ||||
|             child.append(viewContainer); | ||||
|             component = new Vue({ | ||||
|                 el: viewContainer, | ||||
|                 components: { | ||||
|                     ScatterPlot | ||||
|                 }, | ||||
|                 provide: { | ||||
|                     openmct: openmct, | ||||
|                     domainObject: scatterPlotObject, | ||||
|                     composition: openmct.composition.get(scatterPlotObject) | ||||
|                 }, | ||||
|                 template: "<ScatterPlot></ScatterPlot>" | ||||
|             }); | ||||
|  | ||||
|             await Vue.nextTick(); | ||||
|         }); | ||||
|  | ||||
|         it("provides a scatter plot view", () => { | ||||
|             const applicableViews = openmct.objectViews.get(scatterPlotObject, mockObjectPath); | ||||
|             const plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === SCATTER_PLOT_VIEW); | ||||
|             expect(plotViewProvider).toBeDefined(); | ||||
|         }); | ||||
|  | ||||
|         it("Renders plotly scatter plot", () => { | ||||
|             let scatterPlotElement = element.querySelectorAll(".plotly"); | ||||
|             expect(scatterPlotElement.length).toBe(1); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe("the scatter plot objects", () => { | ||||
|         const mockObject = { | ||||
|             name: 'A very nice scatter plot', | ||||
|             key: SCATTER_PLOT_KEY, | ||||
|             creatable: true | ||||
|         }; | ||||
|  | ||||
|         it('defines a scatter plot object type with the correct key', () => { | ||||
|             const objectDef = openmct.types.get(SCATTER_PLOT_KEY).definition; | ||||
|             expect(objectDef.key).toEqual(mockObject.key); | ||||
|         }); | ||||
|  | ||||
|         it('is creatable', () => { | ||||
|             const objectDef = openmct.types.get(SCATTER_PLOT_KEY).definition; | ||||
|             expect(objectDef.creatable).toEqual(mockObject.creatable); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe("The scatter plot composition policy", () => { | ||||
|         it("allows composition for telemetry that contain at least 2 ranges", () => { | ||||
|             const parent = { | ||||
|                 "composition": [], | ||||
|                 "configuration": { | ||||
|                     axes: {}, | ||||
|                     styles: {} | ||||
|                 }, | ||||
|                 "name": "Some Scatter Plot", | ||||
|                 "type": "telemetry.plot.scatter-plot", | ||||
|                 "location": "mine", | ||||
|                 "modified": 1631005183584, | ||||
|                 "persisted": 1631005183502, | ||||
|                 "identifier": { | ||||
|                     "namespace": "", | ||||
|                     "key": "b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9" | ||||
|                 } | ||||
|             }; | ||||
|             const testTelemetryObject = { | ||||
|                 identifier: { | ||||
|                     namespace: "", | ||||
|                     key: "test-object" | ||||
|                 }, | ||||
|                 type: "test-object", | ||||
|                 name: "Test Object", | ||||
|                 telemetry: { | ||||
|                     values: [{ | ||||
|                         key: "some-key", | ||||
|                         name: "Some attribute", | ||||
|                         hints: { | ||||
|                             domain: 1 | ||||
|                         } | ||||
|                     }, { | ||||
|                         key: "some-other-key", | ||||
|                         name: "Another attribute", | ||||
|                         hints: { | ||||
|                             range: 1 | ||||
|                         } | ||||
|                     }, { | ||||
|                         key: "some-other-key2", | ||||
|                         name: "Another attribute2", | ||||
|                         hints: { | ||||
|                             range: 2 | ||||
|                         } | ||||
|                     }] | ||||
|                 } | ||||
|             }; | ||||
|             const composition = openmct.composition.get(parent); | ||||
|             expect(() => { | ||||
|                 composition.add(testTelemetryObject); | ||||
|             }).not.toThrow(); | ||||
|             expect(parent.composition.length).toBe(1); | ||||
|         }); | ||||
|  | ||||
|         it("disallows composition for telemetry that don't contain at least 2 range hints", () => { | ||||
|             const parent = { | ||||
|                 "composition": [], | ||||
|                 "configuration": { | ||||
|                     axes: {}, | ||||
|                     styles: {} | ||||
|                 }, | ||||
|                 "name": "Some Scatter Plot", | ||||
|                 "type": "telemetry.plot.scatter-plot", | ||||
|                 "location": "mine", | ||||
|                 "modified": 1631005183584, | ||||
|                 "persisted": 1631005183502, | ||||
|                 "identifier": { | ||||
|                     "namespace": "", | ||||
|                     "key": "b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9" | ||||
|                 } | ||||
|             }; | ||||
|             const testTelemetryObject = { | ||||
|                 identifier: { | ||||
|                     namespace: "", | ||||
|                     key: "test-object" | ||||
|                 }, | ||||
|                 type: "test-object", | ||||
|                 name: "Test Object", | ||||
|                 telemetry: { | ||||
|                     values: [{ | ||||
|                         key: "some-key", | ||||
|                         name: "Some attribute", | ||||
|                         hints: { | ||||
|                             domain: 1 | ||||
|                         } | ||||
|                     }, { | ||||
|                         key: "some-other-key", | ||||
|                         name: "Another attribute", | ||||
|                         hints: { | ||||
|                             range: 1 | ||||
|                         } | ||||
|                     }] | ||||
|                 } | ||||
|             }; | ||||
|             const composition = openmct.composition.get(parent); | ||||
|             expect(() => { | ||||
|                 composition.add(testTelemetryObject); | ||||
|             }).toThrow(); | ||||
|             expect(parent.composition.length).toBe(0); | ||||
|         }); | ||||
|     }); | ||||
|     describe('the inspector view', () => { | ||||
|         let mockComposition; | ||||
|         let testDomainObject; | ||||
|         let selection; | ||||
|         let plotInspectorView; | ||||
|         let viewContainer; | ||||
|         let optionsElement; | ||||
|         beforeEach(async () => { | ||||
|             testDomainObject = { | ||||
|                 identifier: { | ||||
|                     namespace: "", | ||||
|                     key: "test-object" | ||||
|                 }, | ||||
|                 type: "test-object", | ||||
|                 name: "Test Object", | ||||
|                 telemetry: { | ||||
|                     values: [{ | ||||
|                         key: "utc", | ||||
|                         format: "utc", | ||||
|                         name: "Time", | ||||
|                         hints: { | ||||
|                             domain: 1 | ||||
|                         } | ||||
|                     }, { | ||||
|                         key: "some-key", | ||||
|                         name: "Some attribute", | ||||
|                         hints: { | ||||
|                             range: 1 | ||||
|                         } | ||||
|                     }, { | ||||
|                         key: "some-other-key", | ||||
|                         name: "Another attribute", | ||||
|                         hints: { | ||||
|                             range: 2 | ||||
|                         } | ||||
|                     }] | ||||
|                 } | ||||
|             }; | ||||
|  | ||||
|             selection = [ | ||||
|                 [ | ||||
|                     { | ||||
|                         context: { | ||||
|                             item: { | ||||
|                                 id: "test-object", | ||||
|                                 identifier: { | ||||
|                                     key: "test-object", | ||||
|                                     namespace: '' | ||||
|                                 }, | ||||
|                                 type: "telemetry.plot.scatter-plot", | ||||
|                                 configuration: { | ||||
|                                     axes: {}, | ||||
|                                     styles: { | ||||
|                                     } | ||||
|                                 }, | ||||
|                                 composition: [ | ||||
|                                     { | ||||
|                                         key: '~Some~foo.scatter' | ||||
|                                     } | ||||
|                                 ] | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 ] | ||||
|             ]; | ||||
|  | ||||
|             mockComposition = new EventEmitter(); | ||||
|             mockComposition.load = () => { | ||||
|                 mockComposition.emit('add', testDomainObject); | ||||
|  | ||||
|                 return [testDomainObject]; | ||||
|             }; | ||||
|  | ||||
|             spyOn(openmct.composition, 'get').and.returnValue(mockComposition); | ||||
|  | ||||
|             viewContainer = document.createElement('div'); | ||||
|             child.append(viewContainer); | ||||
|  | ||||
|             const applicableViews = openmct.inspectorViews.get(selection); | ||||
|             plotInspectorView = applicableViews[0]; | ||||
|             plotInspectorView.show(viewContainer); | ||||
|  | ||||
|             await Vue.nextTick(); | ||||
|             optionsElement = element.querySelector('.c-scatter-plot-options'); | ||||
|         }); | ||||
|  | ||||
|         afterEach(() => { | ||||
|             plotInspectorView.destroy(); | ||||
|         }); | ||||
|  | ||||
|         it('it renders the options', () => { | ||||
|             expect(optionsElement).toBeDefined(); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										4
									
								
								src/plugins/charts/scatter/scatterPlotConstants.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/plugins/charts/scatter/scatterPlotConstants.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| export const SCATTER_PLOT_VIEW = 'scatter-plot.view'; | ||||
| export const SCATTER_PLOT_KEY = 'telemetry.plot.scatter-plot'; | ||||
| export const SCATTER_PLOT_INSPECTOR_KEY = 'telemetry.plot.scatter-plot.inspector'; | ||||
| export const TIME_STRIP_KEY = 'time-strip'; | ||||
| @@ -21,7 +21,7 @@ | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import EventEmitter from 'EventEmitter'; | ||||
| import uuid from 'uuid'; | ||||
| import { v4 as uuid } from 'uuid'; | ||||
| import TelemetryCriterion from "./criterion/TelemetryCriterion"; | ||||
| import { evaluateResults } from './utils/evaluator'; | ||||
| import { getLatestTimestamp } from './utils/time'; | ||||
|   | ||||
| @@ -22,7 +22,7 @@ | ||||
|  | ||||
| import Condition from "./Condition"; | ||||
| import { getLatestTimestamp } from './utils/time'; | ||||
| import uuid from "uuid"; | ||||
| import { v4 as uuid } from 'uuid'; | ||||
| import EventEmitter from 'EventEmitter'; | ||||
|  | ||||
| export default class ConditionManager extends EventEmitter { | ||||
|   | ||||
| @@ -214,7 +214,7 @@ | ||||
| import Criterion from './Criterion.vue'; | ||||
| import ConditionDescription from "./ConditionDescription.vue"; | ||||
| import { TRIGGER, TRIGGER_LABEL } from "@/plugins/condition/utils/constants"; | ||||
| import uuid from 'uuid'; | ||||
| import { v4 as uuid } from 'uuid'; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
|   | ||||
| @@ -23,7 +23,7 @@ import ConditionSetViewProvider from './ConditionSetViewProvider.js'; | ||||
| import ConditionSetCompositionPolicy from "./ConditionSetCompositionPolicy"; | ||||
| import ConditionSetMetadataProvider from './ConditionSetMetadataProvider'; | ||||
| import ConditionSetTelemetryProvider from './ConditionSetTelemetryProvider'; | ||||
| import uuid from "uuid"; | ||||
| import { v4 as uuid } from 'uuid'; | ||||
|  | ||||
| export default function ConditionPlugin() { | ||||
|  | ||||
|   | ||||
| @@ -73,7 +73,7 @@ | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import uuid from 'uuid'; | ||||
| import { v4 as uuid } from 'uuid'; | ||||
| import SubobjectView from './SubobjectView.vue'; | ||||
| import TelemetryView from './TelemetryView.vue'; | ||||
| import BoxView from './BoxView.vue'; | ||||
|   | ||||
| @@ -20,7 +20,7 @@ | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import uuid from 'uuid'; | ||||
| import { v4 as uuid } from 'uuid'; | ||||
|  | ||||
| /** | ||||
|  * This class encapsulates the process of  duplicating/copying a domain object | ||||
|   | ||||
| @@ -22,7 +22,7 @@ | ||||
| import JSONExporter from '/src/exporters/JSONExporter.js'; | ||||
|  | ||||
| import _ from 'lodash'; | ||||
| import uuid from "uuid"; | ||||
| import { v4 as uuid } from 'uuid'; | ||||
|  | ||||
| export default class ExportAsJSONAction { | ||||
|     constructor(openmct) { | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import uuid from 'uuid'; | ||||
| import { v4 as uuid } from 'uuid'; | ||||
|  | ||||
| class Container { | ||||
|     constructor(size) { | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import uuid from 'uuid'; | ||||
| import { v4 as uuid } from 'uuid'; | ||||
|  | ||||
| class Frame { | ||||
|     constructor(domainObjectIdentifier, size) { | ||||
|   | ||||
| @@ -23,7 +23,7 @@ | ||||
| import PropertiesAction from './PropertiesAction'; | ||||
| import CreateWizard from './CreateWizard'; | ||||
|  | ||||
| import uuid from 'uuid'; | ||||
| import { v4 as uuid } from 'uuid'; | ||||
|  | ||||
| export default class CreateAction extends PropertiesAction { | ||||
|     constructor(openmct, type, parentDomainObject) { | ||||
|   | ||||
							
								
								
									
										128
									
								
								src/plugins/formActions/CreateActionSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								src/plugins/formActions/CreateActionSpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,128 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 CreateAction from './CreateAction'; | ||||
|  | ||||
| import { | ||||
|     createOpenMct, | ||||
|     resetApplicationState | ||||
| } from 'utils/testing'; | ||||
|  | ||||
| import { debounce } from 'lodash'; | ||||
|  | ||||
| let parentObject; | ||||
| let parentObjectPath; | ||||
| let unObserve; | ||||
|  | ||||
| describe("The create action plugin", () => { | ||||
|     let openmct; | ||||
|  | ||||
|     const TYPES = [ | ||||
|         'clock', | ||||
|         'conditionWidget', | ||||
|         'conditionWidget', | ||||
|         'example.imagery', | ||||
|         'example.state-generator', | ||||
|         'flexible-layout', | ||||
|         'folder', | ||||
|         'generator', | ||||
|         'hyperlink', | ||||
|         'LadTable', | ||||
|         'LadTableSet', | ||||
|         'layout', | ||||
|         'mmgis', | ||||
|         'notebook', | ||||
|         'plan', | ||||
|         'table', | ||||
|         'tabs', | ||||
|         'telemetry-mean', | ||||
|         'telemetry.plot.bar-graph', | ||||
|         'telemetry.plot.overlay', | ||||
|         'telemetry.plot.stacked', | ||||
|         'time-strip', | ||||
|         'timer', | ||||
|         'webpage' | ||||
|     ]; | ||||
|  | ||||
|     beforeEach((done) => { | ||||
|         openmct = createOpenMct(); | ||||
|  | ||||
|         openmct.on('start', done); | ||||
|         openmct.startHeadless(); | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     describe('creates new objects for a', () => { | ||||
|         beforeEach(() => { | ||||
|             parentObject = { | ||||
|                 name: 'mock folder', | ||||
|                 type: 'folder', | ||||
|                 identifier: { | ||||
|                     key: 'mock-folder', | ||||
|                     namespace: '' | ||||
|                 }, | ||||
|                 composition: [] | ||||
|             }; | ||||
|             parentObjectPath = [parentObject]; | ||||
|  | ||||
|             spyOn(openmct.objects, 'save'); | ||||
|             openmct.objects.save.and.callThrough(); | ||||
|             spyOn(openmct.forms, 'showForm'); | ||||
|             openmct.forms.showForm.and.callFake(formStructure => { | ||||
|                 return Promise.resolve({ | ||||
|                     name: 'test', | ||||
|                     notes: 'test notes', | ||||
|                     location: parentObjectPath | ||||
|                 }); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         afterEach(() => { | ||||
|             parentObject = null; | ||||
|             unObserve(); | ||||
|         }); | ||||
|  | ||||
|         TYPES.forEach(type => { | ||||
|             it(`type ${type}`, (done) => { | ||||
|                 function callback(newObject) { | ||||
|                     const composition = newObject.composition; | ||||
|  | ||||
|                     openmct.objects.get(composition[0]) | ||||
|                         .then(object => { | ||||
|                             expect(object.type).toEqual(type); | ||||
|                             expect(object.location).toEqual(openmct.objects.makeKeyString(parentObject.identifier)); | ||||
|  | ||||
|                             done(); | ||||
|                         }); | ||||
|                 } | ||||
|  | ||||
|                 const deBouncedCallback = debounce(callback, 300); | ||||
|                 unObserve = openmct.objects.observe(parentObject, '*', deBouncedCallback); | ||||
|  | ||||
|                 const createAction = new CreateAction(openmct, type, parentObject); | ||||
|                 createAction.invoke(); | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
| @@ -45,7 +45,7 @@ export default class EditPropertiesAction extends PropertiesAction { | ||||
|     } | ||||
|  | ||||
|     invoke(objectPath) { | ||||
|         this._showEditForm(objectPath); | ||||
|         return this._showEditForm(objectPath); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -86,7 +86,7 @@ export default class EditPropertiesAction extends PropertiesAction { | ||||
|         const formStructure = createWizard.getFormStructure(false); | ||||
|         formStructure.title = 'Edit ' + this.domainObject.name; | ||||
|  | ||||
|         this.openmct.forms.showForm(formStructure) | ||||
|         return this.openmct.forms.showForm(formStructure) | ||||
|             .then(this._onSave.bind(this)); | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										222
									
								
								src/plugins/formActions/pluginSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										222
									
								
								src/plugins/formActions/pluginSpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,222 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 { | ||||
|     createMouseEvent, | ||||
|     createOpenMct, | ||||
|     resetApplicationState | ||||
| } from 'utils/testing'; | ||||
|  | ||||
| import { debounce } from 'lodash'; | ||||
|  | ||||
| describe('EditPropertiesAction plugin', () => { | ||||
|     let editPropertiesAction; | ||||
|     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); | ||||
|  | ||||
|         editPropertiesAction = openmct.actions.getAction('properties'); | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         editPropertiesAction = null; | ||||
|  | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     it('editPropertiesAction exists', () => { | ||||
|         expect(editPropertiesAction.key).toEqual('properties'); | ||||
|     }); | ||||
|  | ||||
|     it('edit properties action applies to only persistable objects', () => { | ||||
|         spyOn(openmct.objects, 'isPersistable').and.returnValue(true); | ||||
|  | ||||
|         const domainObject = { | ||||
|             name: 'mock folder', | ||||
|             type: 'folder', | ||||
|             identifier: { | ||||
|                 key: 'mock-folder', | ||||
|                 namespace: '' | ||||
|             }, | ||||
|             composition: [] | ||||
|         }; | ||||
|         const isApplicableTo = editPropertiesAction.appliesTo([domainObject]); | ||||
|         expect(isApplicableTo).toBe(true); | ||||
|     }); | ||||
|  | ||||
|     it('edit properties action does not apply to non persistable objects', () => { | ||||
|         spyOn(openmct.objects, 'isPersistable').and.returnValue(false); | ||||
|  | ||||
|         const domainObject = { | ||||
|             name: 'mock folder', | ||||
|             type: 'folder', | ||||
|             identifier: { | ||||
|                 key: 'mock-folder', | ||||
|                 namespace: '' | ||||
|             }, | ||||
|             composition: [] | ||||
|         }; | ||||
|         const isApplicableTo = editPropertiesAction.appliesTo([domainObject]); | ||||
|         expect(isApplicableTo).toBe(false); | ||||
|     }); | ||||
|  | ||||
|     it('edit properties action when invoked shows form', (done) => { | ||||
|         const domainObject = { | ||||
|             name: 'mock folder', | ||||
|             notes: 'mock notes', | ||||
|             type: 'folder', | ||||
|             identifier: { | ||||
|                 key: 'mock-folder', | ||||
|                 namespace: '' | ||||
|             }, | ||||
|             modified: 1643065068597, | ||||
|             persisted: 1643065068600, | ||||
|             composition: [] | ||||
|         }; | ||||
|  | ||||
|         const deBouncedFormChange = debounce(handleFormPropertyChange, 500); | ||||
|         openmct.forms.on('onFormPropertyChange', deBouncedFormChange); | ||||
|  | ||||
|         function handleFormPropertyChange(data) { | ||||
|             const form = document.querySelector('.js-form'); | ||||
|             const title = form.querySelector('input'); | ||||
|             expect(title.value).toEqual(domainObject.name); | ||||
|  | ||||
|             const notes = form.querySelector('textArea'); | ||||
|             expect(notes.value).toEqual(domainObject.notes); | ||||
|  | ||||
|             const buttons = form.querySelectorAll('button'); | ||||
|             expect(buttons[0].textContent.trim()).toEqual('OK'); | ||||
|             expect(buttons[1].textContent.trim()).toEqual('Cancel'); | ||||
|  | ||||
|             const clickEvent = createMouseEvent('click'); | ||||
|             buttons[1].dispatchEvent(clickEvent); | ||||
|  | ||||
|             openmct.forms.off('onFormPropertyChange', deBouncedFormChange); | ||||
|         } | ||||
|  | ||||
|         editPropertiesAction.invoke([domainObject]) | ||||
|             .catch(() => { | ||||
|                 done(); | ||||
|             }); | ||||
|     }); | ||||
|  | ||||
|     it('edit properties action saves changes', (done) => { | ||||
|         const oldName = 'mock folder'; | ||||
|         const newName = 'renamed mock folder'; | ||||
|         const domainObject = { | ||||
|             name: oldName, | ||||
|             notes: 'mock notes', | ||||
|             type: 'folder', | ||||
|             identifier: { | ||||
|                 key: 'mock-folder', | ||||
|                 namespace: '' | ||||
|             }, | ||||
|             modified: 1643065068597, | ||||
|             persisted: 1643065068600, | ||||
|             composition: [] | ||||
|         }; | ||||
|         let unObserve; | ||||
|  | ||||
|         function callback(newObject) { | ||||
|             expect(newObject.name).not.toEqual(oldName); | ||||
|             expect(newObject.name).toEqual(newName); | ||||
|  | ||||
|             unObserve(); | ||||
|             done(); | ||||
|         } | ||||
|  | ||||
|         const deBouncedCallback = debounce(callback, 300); | ||||
|         unObserve = openmct.objects.observe(domainObject, '*', deBouncedCallback); | ||||
|  | ||||
|         let changed = false; | ||||
|         const deBouncedFormChange = debounce(handleFormPropertyChange, 500); | ||||
|         openmct.forms.on('onFormPropertyChange', deBouncedFormChange); | ||||
|  | ||||
|         function handleFormPropertyChange(data) { | ||||
|             const form = document.querySelector('.js-form'); | ||||
|             const title = form.querySelector('input'); | ||||
|             const notes = form.querySelector('textArea'); | ||||
|  | ||||
|             const buttons = form.querySelectorAll('button'); | ||||
|             expect(buttons[0].textContent.trim()).toEqual('OK'); | ||||
|             expect(buttons[1].textContent.trim()).toEqual('Cancel'); | ||||
|  | ||||
|             if (!changed) { | ||||
|                 expect(title.value).toEqual(domainObject.name); | ||||
|                 expect(notes.value).toEqual(domainObject.notes); | ||||
|  | ||||
|                 // change input field value and dispatch event for it | ||||
|                 title.focus(); | ||||
|                 title.value = newName; | ||||
|                 title.dispatchEvent(new Event('input')); | ||||
|                 title.blur(); | ||||
|  | ||||
|                 changed = true; | ||||
|             } else { | ||||
|                 // click ok to save form changes | ||||
|                 const clickEvent = createMouseEvent('click'); | ||||
|                 buttons[0].dispatchEvent(clickEvent); | ||||
|  | ||||
|                 openmct.forms.off('onFormPropertyChange', deBouncedFormChange); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         editPropertiesAction.invoke([domainObject]); | ||||
|     }); | ||||
|  | ||||
|     it('edit properties action discards changes', (done) => { | ||||
|         const name = 'mock folder'; | ||||
|         const domainObject = { | ||||
|             name, | ||||
|             notes: 'mock notes', | ||||
|             type: 'folder', | ||||
|             identifier: { | ||||
|                 key: 'mock-folder', | ||||
|                 namespace: '' | ||||
|             }, | ||||
|             modified: 1643065068597, | ||||
|             persisted: 1643065068600, | ||||
|             composition: [] | ||||
|         }; | ||||
|  | ||||
|         editPropertiesAction.invoke([domainObject]) | ||||
|             .catch(() => { | ||||
|                 expect(domainObject.name).toEqual(name); | ||||
|  | ||||
|                 done(); | ||||
|             }); | ||||
|  | ||||
|         const form = document.querySelector('.js-form'); | ||||
|         const buttons = form.querySelectorAll('button'); | ||||
|         const clickEvent = createMouseEvent('click'); | ||||
|         buttons[1].dispatchEvent(clickEvent); | ||||
|     }); | ||||
| }); | ||||
| @@ -85,14 +85,13 @@ | ||||
|             class="c-dial__bg" | ||||
|             viewBox="0 0 10 10" | ||||
|         > | ||||
|  | ||||
|             <g | ||||
|                 v-if="limitLow !== null && dialLowLimitDeg < getLimitDegree('low', 'max')" | ||||
|                 v-if="isDialLowLimit" | ||||
|                 class="c-dial__limit-low" | ||||
|                 :style="`transform: rotate(${dialLowLimitDeg}deg)`" | ||||
|             > | ||||
|                 <rect | ||||
|                     v-if="dialLowLimitDeg >= getLimitDegree('low', 'q1')" | ||||
|                     v-if="isDialLowLimitLow" | ||||
|                     class="c-dial__low-limit__low" | ||||
|                     x="5" | ||||
|                     y="5" | ||||
| @@ -100,7 +99,7 @@ | ||||
|                     height="5" | ||||
|                 /> | ||||
|                 <rect | ||||
|                     v-if="dialLowLimitDeg >= getLimitDegree('low', 'q2')" | ||||
|                     v-if="isDialLowLimitMid" | ||||
|                     class="c-dial__low-limit__mid" | ||||
|                     x="5" | ||||
|                     y="0" | ||||
| @@ -108,7 +107,7 @@ | ||||
|                     height="5" | ||||
|                 /> | ||||
|                 <rect | ||||
|                     v-if="dialLowLimitDeg >= getLimitDegree('low', 'q3')" | ||||
|                     v-if="isDialLowLimitHigh" | ||||
|                     class="c-dial__low-limit__high" | ||||
|                     x="0" | ||||
|                     y="0" | ||||
| @@ -118,12 +117,12 @@ | ||||
|             </g> | ||||
|  | ||||
|             <g | ||||
|                 v-if="limitHigh !== null && dialHighLimitDeg < getLimitDegree('high', 'max')" | ||||
|                 v-if="isDialHighLimit" | ||||
|                 class="c-dial__limit-high" | ||||
|                 :style="`transform: rotate(${dialHighLimitDeg}deg)`" | ||||
|             > | ||||
|                 <rect | ||||
|                     v-if="dialHighLimitDeg <= getLimitDegree('high', 'max')" | ||||
|                     v-if="isDialHighLimitLow" | ||||
|                     class="c-dial__high-limit__low" | ||||
|                     x="0" | ||||
|                     y="5" | ||||
| @@ -131,7 +130,7 @@ | ||||
|                     height="5" | ||||
|                 /> | ||||
|                 <rect | ||||
|                     v-if="dialHighLimitDeg <= getLimitDegree('high', 'q2')" | ||||
|                     v-if="isDialHighLimitMid" | ||||
|                     class="c-dial__high-limit__mid" | ||||
|                     x="0" | ||||
|                     y="0" | ||||
| @@ -139,7 +138,7 @@ | ||||
|                     height="5" | ||||
|                 /> | ||||
|                 <rect | ||||
|                     v-if="dialHighLimitDeg <= getLimitDegree('high', 'q3')" | ||||
|                     v-if="isDialHighLimitHigh" | ||||
|                     class="c-dial__high-limit__high" | ||||
|                     x="5" | ||||
|                     y="0" | ||||
| @@ -159,7 +158,7 @@ | ||||
|                 :style="`transform: rotate(${degValueFilledDial}deg)`" | ||||
|             > | ||||
|                 <rect | ||||
|                     v-if="degValue >= getLimitDegree('low', 'q1')" | ||||
|                     v-if="isDialFilledValueLow" | ||||
|                     class="c-dial__filled-value__low" | ||||
|                     x="5" | ||||
|                     y="5" | ||||
| @@ -167,7 +166,7 @@ | ||||
|                     height="5" | ||||
|                 /> | ||||
|                 <rect | ||||
|                     v-if="degValue >= getLimitDegree('low', 'q2')" | ||||
|                     v-if="isDialFilledValueMid" | ||||
|                     class="c-dial__filled-value__mid" | ||||
|                     x="5" | ||||
|                     y="0" | ||||
| @@ -175,7 +174,7 @@ | ||||
|                     height="5" | ||||
|                 /> | ||||
|                 <rect | ||||
|                     v-if="degValue >= getLimitDegree('low', 'q3')" | ||||
|                     v-if="isDialFilledValueHigh" | ||||
|                     class="c-dial__filled-value__high" | ||||
|                     x="0" | ||||
|                     y="0" | ||||
| @@ -216,13 +215,13 @@ | ||||
|                     ></div> | ||||
|  | ||||
|                     <div | ||||
|                         v-if="limitHigh !== null && meterHighLimitPerc > 0" | ||||
|                         v-if="isMeterLimitHigh" | ||||
|                         class="c-meter__limit-high" | ||||
|                         :style="`height: ${meterHighLimitPerc}%`" | ||||
|                     ></div> | ||||
|  | ||||
|                     <div | ||||
|                         v-if="limitLow !== null && meterLowLimitPerc > 0" | ||||
|                         v-if="isMeterLimitLow" | ||||
|                         class="c-meter__limit-low" | ||||
|                         :style="`height: ${meterLowLimitPerc}%`" | ||||
|                     ></div> | ||||
| @@ -235,13 +234,13 @@ | ||||
|                     ></div> | ||||
|  | ||||
|                     <div | ||||
|                         v-if="limitHigh !== null && meterHighLimitPerc > 0" | ||||
|                         v-if="isMeterLimitHigh" | ||||
|                         class="c-meter__limit-high" | ||||
|                         :style="`width: ${meterHighLimitPerc}%`" | ||||
|                     ></div> | ||||
|  | ||||
|                     <div | ||||
|                         v-if="limitLow !== null && meterLowLimitPerc > 0" | ||||
|                         v-if="isMeterLimitLow" | ||||
|                         class="c-meter__limit-low" | ||||
|                         :style="`width: ${meterLowLimitPerc}%`" | ||||
|                     ></div> | ||||
| @@ -275,6 +274,7 @@ | ||||
| import { DIAL_VALUE_DEG_OFFSET, getLimitDegree } from '../gauge-limit-util'; | ||||
|  | ||||
| const LIMIT_PADDING_IN_PERCENT = 10; | ||||
| const DEFAULT_CURRENT_VALUE = '--'; | ||||
|  | ||||
| export default { | ||||
|     name: 'Gauge', | ||||
| @@ -283,7 +283,7 @@ export default { | ||||
|         let gaugeController = this.domainObject.configuration.gaugeController; | ||||
|  | ||||
|         return { | ||||
|             curVal: 0, | ||||
|             curVal: DEFAULT_CURRENT_VALUE, | ||||
|             digits: 3, | ||||
|             precision: gaugeController.precision, | ||||
|             displayMinMax: gaugeController.isDisplayMinMax, | ||||
| @@ -319,6 +319,45 @@ export default { | ||||
|  | ||||
|             return VIEWBOX_STR.replace('X', this.digits * DIGITS_RATIO); | ||||
|         }, | ||||
|         isDialLowLimit() { | ||||
|             return this.limitLow.length > 0 && this.dialLowLimitDeg < getLimitDegree('low', 'max'); | ||||
|         }, | ||||
|         isDialLowLimitLow() { | ||||
|             return this.dialLowLimitDeg >= getLimitDegree('low', 'q1'); | ||||
|         }, | ||||
|         isDialLowLimitMid() { | ||||
|             return this.dialLowLimitDeg >= getLimitDegree('low', 'q2'); | ||||
|         }, | ||||
|         isDialLowLimitHigh() { | ||||
|             return this.dialLowLimitDeg >= getLimitDegree('low', 'q3'); | ||||
|         }, | ||||
|         isDialHighLimit() { | ||||
|             return this.limitHigh.length > 0 && this.dialHighLimitDeg < getLimitDegree('high', 'max'); | ||||
|         }, | ||||
|         isDialHighLimitLow() { | ||||
|             return this.dialHighLimitDeg <= getLimitDegree('high', 'max'); | ||||
|         }, | ||||
|         isDialHighLimitMid() { | ||||
|             return this.dialHighLimitDeg <= getLimitDegree('high', 'q2'); | ||||
|         }, | ||||
|         isDialHighLimitHigh() { | ||||
|             return this.dialHighLimitDeg <= getLimitDegree('high', 'q3'); | ||||
|         }, | ||||
|         isDialFilledValueLow() { | ||||
|             return this.degValue >= getLimitDegree('low', 'q1'); | ||||
|         }, | ||||
|         isDialFilledValueMid() { | ||||
|             return this.degValue >= getLimitDegree('low', 'q2'); | ||||
|         }, | ||||
|         isDialFilledValueHigh() { | ||||
|             return this.degValue >= getLimitDegree('low', 'q3'); | ||||
|         }, | ||||
|         isMeterLimitHigh() { | ||||
|             return this.limitHigh.length > 0 && this.meterHighLimitPerc > 0; | ||||
|         }, | ||||
|         isMeterLimitLow() { | ||||
|             return this.limitLow.length > 0 && this.meterLowLimitPerc > 0; | ||||
|         }, | ||||
|         typeDial() { | ||||
|             return this.matchGaugeType('dial'); | ||||
|         }, | ||||
| @@ -459,13 +498,14 @@ export default { | ||||
|                 this.unsubscribe = null; | ||||
|             } | ||||
|  | ||||
|             this.metadata = null; | ||||
|             this.curVal = DEFAULT_CURRENT_VALUE; | ||||
|             this.formats = null; | ||||
|             this.valueKey = null; | ||||
|             this.limitHigh = null; | ||||
|             this.limitLow = null; | ||||
|             this.limitHigh = ''; | ||||
|             this.limitLow = ''; | ||||
|             this.metadata = null; | ||||
|             this.rangeHigh = null; | ||||
|             this.rangeLow = null; | ||||
|             this.valueKey = null; | ||||
|         }, | ||||
|         request(domainObject = this.telemetryObject) { | ||||
|             this.metadata = this.openmct.telemetry.getMetadata(domainObject); | ||||
| @@ -518,13 +558,20 @@ export default { | ||||
|             } else if (telemetryLimit.WATCH) { | ||||
|                 limits = telemetryLimit.WATCH; | ||||
|             } else { | ||||
|                 this.openmct.notifications.error('No limits definition for given telemetry'); | ||||
|                 this.openmct.notifications.error('No limits definition for given telemetry, hiding low and high limits'); | ||||
|                 this.displayMinMax = false; | ||||
|                 this.limitHigh = ''; | ||||
|                 this.limitLow = ''; | ||||
|  | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             this.limitHigh = this.round(limits.high[this.valueKey]); | ||||
|             this.limitLow = this.round(limits.low[this.valueKey]); | ||||
|             this.rangeHigh = this.round(this.limitHigh + this.limitHigh * LIMIT_PADDING_IN_PERCENT / 100); | ||||
|             this.rangeLow = this.round(this.limitLow - Math.abs(this.limitLow * LIMIT_PADDING_IN_PERCENT / 100)); | ||||
|  | ||||
|             this.displayMinMax = this.domainObject.configuration.gaugeController.isDisplayMinMax; | ||||
|         }, | ||||
|         updateValue(datum) { | ||||
|             this.datum = datum; | ||||
|   | ||||
| @@ -60,7 +60,7 @@ | ||||
|         > | ||||
|             <div class="c-image-controls__input icon-brightness"> | ||||
|                 <input | ||||
|                     v-model="filters.brightness" | ||||
|                     v-model="filters.contrast" | ||||
|                     type="range" | ||||
|                     min="0" | ||||
|                     max="500" | ||||
| @@ -69,7 +69,7 @@ | ||||
|             </div> | ||||
|             <div class="c-image-controls__input icon-contrast"> | ||||
|                 <input | ||||
|                     v-model="filters.contrast" | ||||
|                     v-model="filters.brightness" | ||||
|                     type="range" | ||||
|                     min="0" | ||||
|                     max="500" | ||||
| @@ -107,7 +107,10 @@ export default { | ||||
|             type: Number, | ||||
|             required: true | ||||
|         }, | ||||
|         imageUrl: String | ||||
|         imageUrl: { | ||||
|             type: String, | ||||
|             default: '' | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
| @@ -174,7 +177,7 @@ export default { | ||||
|             this.$emit('filtersUpdated', this.filters); | ||||
|         }, | ||||
|         handleResetFilters() { | ||||
|             this.filters = {...DEFAULT_FILTER_VALUES}; | ||||
|             this.filters = DEFAULT_FILTER_VALUES; | ||||
|             this.notifyFiltersChanged(); | ||||
|         }, | ||||
|         limitZoomRange(factor) { | ||||
|   | ||||
| @@ -55,7 +55,7 @@ | ||||
|             <div | ||||
|                 v-if="zoomFactor > 1" | ||||
|                 class="c-imagery__hints" | ||||
|             >Alt-drag to pan</div> | ||||
|             >{{ formatImageAltText }}</div> | ||||
|             <div | ||||
|                 ref="focusedImageWrapper" | ||||
|                 class="image-wrapper" | ||||
| @@ -97,7 +97,8 @@ | ||||
|                         'transform': `scale(${zoomFactor}) translate(${imageTranslateX}px, ${imageTranslateY}px)`, | ||||
|                         'transition': `${!pan && animateZoom ? 'transform 250ms ease-in' : 'initial'}`, | ||||
|                         'width': `${sizedImageWidth}px`, | ||||
|                         'height': `${sizedImageHeight}px` | ||||
|                         'height': `${sizedImageHeight}px`, | ||||
|  | ||||
|                     }" | ||||
|                 ></div> | ||||
|                 <Compass | ||||
| @@ -381,9 +382,6 @@ export default { | ||||
|         formattedDuration() { | ||||
|             let result = 'N/A'; | ||||
|             let negativeAge = -1; | ||||
|             if (!Number.isInteger(this.numericDuration)) { | ||||
|                 return result; | ||||
|             } | ||||
|  | ||||
|             if (this.numericDuration > TWENTYFOUR_HOURS) { | ||||
|                 negativeAge *= (this.numericDuration / TWENTYFOUR_HOURS); | ||||
| @@ -490,6 +488,16 @@ export default { | ||||
|                 width: this.sizedImageWidth, | ||||
|                 height: this.sizedImageHeight | ||||
|             }; | ||||
|         }, | ||||
|         formatImageAltText() { | ||||
|             const regexLinux = /Linux/; | ||||
|             const navigator = window.navigator.userAgent; | ||||
|  | ||||
|             if (regexLinux.test(navigator)) { | ||||
|                 return 'Ctrl+Alt drag to pan'; | ||||
|             } | ||||
|  | ||||
|             return 'Alt drag to pan'; | ||||
|         } | ||||
|     }, | ||||
|     watch: { | ||||
| @@ -836,10 +844,8 @@ export default { | ||||
|             let currentTime = this.timeContext.clock() && this.timeContext.clock().currentValue(); | ||||
|             if (currentTime === undefined) { | ||||
|                 this.numericDuration = currentTime; | ||||
|             } else if (Number.isInteger(this.parsedSelectedTime)) { | ||||
|                 this.numericDuration = currentTime - this.parsedSelectedTime; | ||||
|             } else { | ||||
|                 this.numericDuration = undefined; | ||||
|                 this.numericDuration = currentTime - this.parsedSelectedTime; | ||||
|             } | ||||
|         }, | ||||
|         resetAgeCSS() { | ||||
| @@ -881,10 +887,6 @@ export default { | ||||
|             this.imageTranslateY = 0; | ||||
|         }, | ||||
|         handlePanZoomUpdate({ newScaleFactor, screenClientX, screenClientY }) { | ||||
|             if (!this.isPaused) { | ||||
|                 this.paused(true); | ||||
|             } | ||||
|  | ||||
|             if (!(screenClientX || screenClientY)) { | ||||
|                 return this.updatePanZoom(newScaleFactor, 0, 0); | ||||
|             } | ||||
| @@ -1034,9 +1036,6 @@ export default { | ||||
|         }, | ||||
|         wheelZoom(e) { | ||||
|             e.preventDefault(); | ||||
|             if (!this.isPaused) { | ||||
|                 this.paused(true); | ||||
|             } | ||||
|  | ||||
|             this.$refs.imageControls.wheelZoom(e); | ||||
|         }, | ||||
|   | ||||
| @@ -63,7 +63,6 @@ | ||||
|             background-position: center; | ||||
|             background-repeat: no-repeat; | ||||
|             background-size: contain; | ||||
|             height: 100%; //fallback value | ||||
|         } | ||||
|         &__image { | ||||
|             height: 100%; | ||||
|   | ||||
| @@ -70,18 +70,22 @@ export default { | ||||
|                 this.timeContext.off('timeSystem', this.timeSystemChange); | ||||
|             } | ||||
|         }, | ||||
|         isDatumValid(datum) { | ||||
|             //TODO: Add a check to see if there are duplicate images (identical image timestamp and url subsequently) | ||||
|             if (!datum) { | ||||
|         datumIsNotValid(datum) { | ||||
|             if (this.imageHistory.length === 0) { | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             const datumURL = this.formatImageUrl(datum); | ||||
|             const lastHistoryURL = this.formatImageUrl(this.imageHistory.slice(-1)[0]); | ||||
|  | ||||
|             // datum is not valid if it matches the last datum in history, | ||||
|             // or it is before the last datum in the history | ||||
|             const datumTimeCheck = this.parseTime(datum); | ||||
|             const bounds = this.timeContext.bounds(); | ||||
|             const historyTimeCheck = this.parseTime(this.imageHistory.slice(-1)[0]); | ||||
|             const matchesLast = (datumTimeCheck === historyTimeCheck) && (datumURL === lastHistoryURL); | ||||
|             const isStale = datumTimeCheck < historyTimeCheck; | ||||
|  | ||||
|             const isOutOfBounds = datumTimeCheck < bounds.start || datumTimeCheck > bounds.end; | ||||
|  | ||||
|             return !isOutOfBounds; | ||||
|             return matchesLast || isStale; | ||||
|         }, | ||||
|         formatImageUrl(datum) { | ||||
|             if (!datum) { | ||||
| @@ -128,19 +132,25 @@ export default { | ||||
|             return this.requestHistory(); | ||||
|         }, | ||||
|         async requestHistory() { | ||||
|             let bounds = this.timeContext.bounds(); | ||||
|             this.requestCount++; | ||||
|             const requestId = this.requestCount; | ||||
|             const bounds = this.timeContext.bounds(); | ||||
|             this.imageHistory = []; | ||||
|  | ||||
|             const data = await this.openmct.telemetry | ||||
|             let data = await this.openmct.telemetry | ||||
|                 .request(this.domainObject, bounds) || []; | ||||
|             // wait until new request resolves to do comparison | ||||
|             if (this.requestCount !== requestId) { | ||||
|                 return this.imageHistory = []; | ||||
|             } | ||||
|  | ||||
|             const imagery = data.filter(this.isDatumValid).map(this.normalizeDatum); | ||||
|             this.imageHistory = imagery; | ||||
|             if (this.requestCount === requestId) { | ||||
|                 let imagery = []; | ||||
|                 data.forEach((datum) => { | ||||
|                     let image = this.normalizeDatum(datum); | ||||
|                     if (image) { | ||||
|                         imagery.push(image); | ||||
|                     } | ||||
|                 }); | ||||
|                 //this is to optimize anything that reacts to imageHistory length | ||||
|                 this.imageHistory = imagery; | ||||
|             } | ||||
|         }, | ||||
|         clearData(domainObjectToClear) { | ||||
|             // global clearData button is accepted therefore no truthy check on inputted param | ||||
| @@ -158,8 +168,6 @@ export default { | ||||
|             // splice array to encourage garbage collection | ||||
|             this.imageHistory.splice(0, this.imageHistory.length); | ||||
|  | ||||
|             // requesting history effectively clears imageHistory array | ||||
|             return this.requestHistory(); | ||||
|         }, | ||||
|         timeSystemChange() { | ||||
|             this.timeSystem = this.timeContext.timeSystem(); | ||||
| @@ -172,29 +180,27 @@ export default { | ||||
|                 .subscribe(this.domainObject, (datum) => { | ||||
|                     let parsedTimestamp = this.parseTime(datum); | ||||
|                     let bounds = this.timeContext.bounds(); | ||||
|                     if (!(parsedTimestamp >= bounds.start && parsedTimestamp <= bounds.end)) { | ||||
|                         return; | ||||
|                     } | ||||
|  | ||||
|                     if (this.isDatumValid(datum)) { | ||||
|                         this.imageHistory.push(this.normalizeDatum(datum)); | ||||
|                     if (parsedTimestamp >= bounds.start && parsedTimestamp <= bounds.end) { | ||||
|                         let image = this.normalizeDatum(datum); | ||||
|                         if (image) { | ||||
|                             this.imageHistory.push(image); | ||||
|                         } | ||||
|                     } | ||||
|                 }); | ||||
|         }, | ||||
|         normalizeDatum(datum) { | ||||
|             if (this.datumIsNotValid(datum)) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             const formattedTime = this.formatTime(datum); | ||||
|             const url = this.formatImageUrl(datum); | ||||
|             const time = this.parseTime(formattedTime); | ||||
|             const imageDownloadName = this.getImageDownloadName(datum); | ||||
|             let image = { ...datum }; | ||||
|             image.formattedTime = this.formatTime(datum); | ||||
|             image.url = this.formatImageUrl(datum); | ||||
|             image.time = this.parseTime(image.formattedTime); | ||||
|             image.imageDownloadName = this.getImageDownloadName(datum); | ||||
|  | ||||
|             return { | ||||
|                 ...datum, | ||||
|                 formattedTime, | ||||
|                 url, | ||||
|                 time, | ||||
|                 imageDownloadName | ||||
|             }; | ||||
|             return image; | ||||
|         }, | ||||
|         getFormatter(key) { | ||||
|             let metadataValue = this.metadata.value(key) || { format: key }; | ||||
|   | ||||
| @@ -84,6 +84,7 @@ describe("The Imagery View Layouts", () => { | ||||
|     let telemetryPromise; | ||||
|     let telemetryPromiseResolve; | ||||
|     let cleanupFirst; | ||||
|     let isClearDataTriggered; | ||||
|  | ||||
|     let openmct; | ||||
|     let parent; | ||||
| @@ -192,12 +193,20 @@ describe("The Imagery View Layouts", () => { | ||||
|         cleanupFirst = []; | ||||
|  | ||||
|         openmct = createOpenMct(); | ||||
|         openmct.time.timeSystem('utc', { | ||||
|             start: START - (5 * ONE_MINUTE), | ||||
|             end: START + (5 * ONE_MINUTE) | ||||
|         }); | ||||
|  | ||||
|         telemetryPromise = new Promise((resolve) => { | ||||
|             telemetryPromiseResolve = resolve; | ||||
|         }); | ||||
|  | ||||
|         spyOn(openmct.telemetry, 'request').and.callFake(() => { | ||||
|             if (isClearDataTriggered) { | ||||
|                 return []; | ||||
|             } | ||||
|  | ||||
|             telemetryPromiseResolve(imageTelemetry); | ||||
|  | ||||
|             return telemetryPromise; | ||||
| @@ -316,93 +325,44 @@ describe("The Imagery View Layouts", () => { | ||||
|         expect(imageryView).toBeDefined(); | ||||
|     }); | ||||
|  | ||||
|     describe("Clear data action for imagery", () => { | ||||
|     describe("imagery view", () => { | ||||
|         let applicableViews; | ||||
|         let imageryViewProvider; | ||||
|         let imageryView; | ||||
|         let componentView; | ||||
|         let clearDataPlugin; | ||||
|         let clearDataAction; | ||||
|  | ||||
|         beforeEach(() => { | ||||
|             openmct.time.timeSystem('utc', { | ||||
|                 start: START - (5 * ONE_MINUTE), | ||||
|                 end: START + (5 * ONE_MINUTE) | ||||
|             }); | ||||
|  | ||||
|             applicableViews = openmct.objectViews.get(imageryObject, [imageryObject]); | ||||
|             imageryViewProvider = applicableViews.find(viewProvider => viewProvider.key === imageryKey); | ||||
|             imageryView = imageryViewProvider.view(imageryObject, [imageryObject]); | ||||
|             imageryView.show(child); | ||||
|             componentView = imageryView._getInstance().$children[0]; | ||||
|  | ||||
|             clearDataPlugin = new ClearDataPlugin( | ||||
|                 ['example.imagery'], | ||||
|                 {indicator: true} | ||||
|             ); | ||||
|             openmct.install(clearDataPlugin); | ||||
|             clearDataAction = openmct.actions.getAction('clear-data-action'); | ||||
|  | ||||
|             return Vue.nextTick(); | ||||
|         }); | ||||
|  | ||||
|         it('clear data action is installed', () => { | ||||
|             expect(clearDataAction).toBeDefined(); | ||||
|         }); | ||||
|  | ||||
|         it('on clearData action should clear data for object is selected', (done) => { | ||||
|             // force show the thumbnails | ||||
|             componentView.forceShowThumbnails = true; | ||||
|             Vue.nextTick(() => { | ||||
|                 let clearDataResolve; | ||||
|                 let telemetryRequestPromise = new Promise((resolve) => { | ||||
|                     clearDataResolve = resolve; | ||||
|                 }); | ||||
|                 expect(parent.querySelectorAll('.c-imagery__thumb').length).not.toBe(0); | ||||
|  | ||||
|                 openmct.objectViews.on('clearData', (_domainObject) => { | ||||
|                     return Vue.nextTick(() => { | ||||
|                         expect(parent.querySelectorAll('.c-imagery__thumb').length).toBe(0); | ||||
|  | ||||
|                         clearDataResolve(); | ||||
|                     }); | ||||
|                 }); | ||||
|                 clearDataAction.invoke(imageryObject); | ||||
|  | ||||
|                 telemetryRequestPromise.then(() => { | ||||
|                     done(); | ||||
|                 }); | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe("imagery view", () => { | ||||
|         let applicableViews; | ||||
|         let imageryViewProvider; | ||||
|         let imageryView; | ||||
|  | ||||
|         beforeEach(() => { | ||||
|             openmct.time.timeSystem('utc', { | ||||
|                 start: START - (5 * ONE_MINUTE), | ||||
|                 end: START + (5 * ONE_MINUTE) | ||||
|             }); | ||||
|  | ||||
|             applicableViews = openmct.objectViews.get(imageryObject, [imageryObject]); | ||||
|             imageryViewProvider = applicableViews.find(viewProvider => viewProvider.key === imageryKey); | ||||
|             imageryView = imageryViewProvider.view(imageryObject, [imageryObject]); | ||||
|             imageryView.show(child); | ||||
|  | ||||
|             imageryView._getInstance().$children[0].forceShowThumbnails = true; | ||||
|  | ||||
|             return Vue.nextTick(); | ||||
|         }); | ||||
|         afterEach(() => { | ||||
|             isClearDataTriggered = false; | ||||
|             // openmct.time.stopClock(); | ||||
|             // openmct.router.removeListener('change:hash', resolveFunction); | ||||
|             // imageryView.destroy(); | ||||
|         }); | ||||
|  | ||||
|         it("on mount should show the the most recent image", () => { | ||||
|         it("on mount should show the the most recent image", (done) => { | ||||
|             //Looks like we need Vue.nextTick here so that computed properties settle down | ||||
|             return Vue.nextTick(() => { | ||||
|             Vue.nextTick(() => { | ||||
|                 const imageInfo = getImageInfo(parent); | ||||
|  | ||||
|                 expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 1].timeId)).not.toEqual(-1); | ||||
|                 done(); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
| @@ -438,7 +398,7 @@ describe("The Imagery View Layouts", () => { | ||||
|  | ||||
|         it("should show that an image is not new", (done) => { | ||||
|             Vue.nextTick(() => { | ||||
|                 const target = imageTelemetry[4].url; | ||||
|                 const target = imageTelemetry[2].url; | ||||
|                 parent.querySelectorAll(`img[src='${target}']`)[0].click(); | ||||
|  | ||||
|                 Vue.nextTick(() => { | ||||
| @@ -560,6 +520,25 @@ describe("The Imagery View Layouts", () => { | ||||
|             expect(imageSizeAfter.width).toBeLessThan(imageSizeBefore.width); | ||||
|             done(); | ||||
|         }); | ||||
|  | ||||
|         it('clear data action is installed', () => { | ||||
|             expect(clearDataAction).toBeDefined(); | ||||
|         }); | ||||
|  | ||||
|         it('on clearData action should clear data for object is selected', async (done) => { | ||||
|             // force show the thumbnails | ||||
|             imageryView._getInstance().$children[0].forceShowThumbnails = true; | ||||
|             await Vue.nextTick(); | ||||
|             expect(parent.querySelectorAll('.c-imagery__thumb').length).not.toBe(0); | ||||
|             openmct.objectViews.on('clearData', async (_domainObject) => { | ||||
|                 await Vue.nextTick(); | ||||
|                 expect(parent.querySelectorAll('.c-imagery__thumb').length).toBe(0); | ||||
|                 done(); | ||||
|             }); | ||||
|             // stubbed telemetry data will return empty array when true | ||||
|             isClearDataTriggered = true; | ||||
|             clearDataAction.invoke(imageryObject); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe("imagery time strip view", () => { | ||||
|   | ||||
| @@ -21,7 +21,7 @@ | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import objectUtils from 'objectUtils'; | ||||
| import uuid from "uuid"; | ||||
| import { v4 as uuid } from 'uuid'; | ||||
|  | ||||
| export default class ImportAsJSONAction { | ||||
|     constructor(openmct) { | ||||
|   | ||||
| @@ -91,6 +91,7 @@ describe("The Move Action plugin", () => { | ||||
|     }); | ||||
|  | ||||
|     describe("when moving an object to a new parent and removing from the old parent", () => { | ||||
|         let unObserve; | ||||
|         beforeEach((done) => { | ||||
|             openmct.router.path = []; | ||||
|  | ||||
| @@ -104,7 +105,7 @@ describe("The Move Action plugin", () => { | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             openmct.objects.observe(parentObject, '*', (newObject) => { | ||||
|             unObserve = openmct.objects.observe(parentObject, '*', (newObject) => { | ||||
|                 done(); | ||||
|             }); | ||||
|  | ||||
| @@ -113,6 +114,10 @@ describe("The Move Action plugin", () => { | ||||
|             moveAction.invoke([childObject, parentObject]); | ||||
|         }); | ||||
|  | ||||
|         afterEach(() => { | ||||
|             unObserve(); | ||||
|         }); | ||||
|  | ||||
|         it("the child object's identifier should be in the new parent's composition", () => { | ||||
|             let newParentChild = anotherParentObject.composition[0]; | ||||
|             expect(newParentChild).toEqual(childObject.identifier); | ||||
|   | ||||
| @@ -177,7 +177,7 @@ export default { | ||||
|         SearchResults, | ||||
|         Sidebar | ||||
|     }, | ||||
|     inject: ['openmct', 'snapshotContainer'], | ||||
|     inject: ['agent', 'openmct', 'snapshotContainer'], | ||||
|     props: { | ||||
|         domainObject: { | ||||
|             type: Object, | ||||
| @@ -455,12 +455,9 @@ export default { | ||||
|                 - tablet portrait | ||||
|                 - in a layout frame (within .c-so-view) | ||||
|             */ | ||||
|             const classList = document.querySelector('body').classList; | ||||
|             const isPhone = Array.from(classList).includes('phone'); | ||||
|             const isTablet = Array.from(classList).includes('tablet'); | ||||
|             // address in https://github.com/nasa/openmct/issues/4875 | ||||
|             // eslint-disable-next-line compat/compat | ||||
|             const isPortrait = window.screen.orientation.type.includes('portrait'); | ||||
|             const isPhone = this.agent.isPhone(); | ||||
|             const isTablet = this.agent.isTablet(); | ||||
|             const isPortrait = this.agent.isPortrait(); | ||||
|             const isInLayout = Boolean(this.$el.closest('.c-so-view')); | ||||
|             const sidebarCoversEntries = (isPhone || (isTablet && isPortrait) || isInLayout); | ||||
|             this.sidebarCoversEntries = sidebarCoversEntries; | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user