Compare commits
	
		
			18 Commits
		
	
	
		
			fix-couchd
			...
			release/2.
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | e2863595d7 | ||
|   | 7cad3c01ff | ||
|   | ae1b7520bf | ||
|   | 2a165a4549 | ||
|   | 6abd395605 | ||
|   | 610f78b6fb | ||
|   | 663f42ad2e | ||
|   | f80a3c13c1 | ||
|   | a94ec344ea | ||
|   | e75befafbd | ||
|   | d7d06b59ea | ||
|   | 8b4a55a7ec | ||
|   | 2519e601d7 | ||
|   | b7b205621b | ||
|   | 3d2d932323 | ||
|   | ce28dd2b9f | ||
|   | 286a533dad | ||
|   | 378a4ca282 | 
| @@ -2,7 +2,7 @@ version: 2.1 | ||||
| executors: | ||||
|   pw-focal-development: | ||||
|     docker: | ||||
|       - image: mcr.microsoft.com/playwright:v1.23.0-focal | ||||
|       - image: mcr.microsoft.com/playwright:v1.19.2-focal | ||||
|     environment: | ||||
|       NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed | ||||
| parameters: | ||||
| @@ -12,7 +12,7 @@ parameters: | ||||
|     type: boolean | ||||
| commands: | ||||
|   build_and_install: | ||||
|     description: "All steps used to build and install. Will use cache if found" | ||||
|     description: "All steps used to build and install. Will not work on node10" | ||||
|     parameters: | ||||
|       node-version: | ||||
|         type: string | ||||
| @@ -23,7 +23,7 @@ commands: | ||||
|       - node/install: | ||||
|           install-npm: true | ||||
|           node-version: << parameters.node-version >> | ||||
|       - run: npm install --prefer-offline --no-audit --progress=false | ||||
|       - run: npm install | ||||
|   restore_cache_cmd: | ||||
|     description: "Custom command for restoring cache with the ability to bust cache. When BUST_CACHE is set to true, jobs will not restore cache" | ||||
|     parameters: | ||||
| @@ -31,7 +31,7 @@ commands: | ||||
|         type: string | ||||
|     steps: | ||||
|       - when: | ||||
|           condition: | ||||
|           condition:  | ||||
|             equal: [false, << pipeline.parameters.BUST_CACHE >> ] | ||||
|           steps: | ||||
|             - restore_cache: | ||||
| @@ -41,7 +41,7 @@ commands: | ||||
|     parameters: | ||||
|       node-version: | ||||
|         type: string | ||||
|     steps: | ||||
|     steps:     | ||||
|       - save_cache: | ||||
|           key: deps-{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}-{{ checksum ".circleci/config.yml" }} | ||||
|           paths: | ||||
| @@ -58,17 +58,13 @@ commands: | ||||
|           ls -latR >> /tmp/artifacts/dir.txt | ||||
|       - store_artifacts: | ||||
|           path: /tmp/artifacts/ | ||||
|   generate_e2e_code_cov_report: | ||||
|    description: "Generate e2e code coverage artifacts and publish to codecov.io. Needed to that we can ignore the exit code status of the npm run test" | ||||
|    parameters: | ||||
|       suite: | ||||
|         type: string | ||||
|    steps: | ||||
|     - run: npm run cov:e2e:report  | ||||
|     - run: npm run cov:e2e:<<parameters.suite>>:publish        | ||||
|   upload_code_covio: | ||||
|     description: "Command to upload code coverage reports to codecov.io" | ||||
|     steps: | ||||
|         - run: curl -Os https://uploader.codecov.io/latest/linux/codecov;chmod +x codecov;./codecov  | ||||
| orbs: | ||||
|   node: circleci/node@4.9.0 | ||||
|   browser-tools: circleci/browser-tools@1.3.0 | ||||
|   browser-tools: circleci/browser-tools@1.2.3 | ||||
| jobs: | ||||
|   npm-audit: | ||||
|     parameters: | ||||
| @@ -105,7 +101,7 @@ jobs: | ||||
|             equal: [ "FirefoxESR", <<parameters.browser>> ] | ||||
|           steps: | ||||
|             - browser-tools/install-firefox: | ||||
|                 version: "91.7.1esr" #https://archive.mozilla.org/pub/firefox/releases/ | ||||
|                 version: "91.7.1esr" #https://archive.mozilla.org/pub/firefox/releases/           | ||||
|       - when: | ||||
|           condition: | ||||
|             equal: [ "FirefoxHeadless", <<parameters.browser>> ] | ||||
| @@ -118,13 +114,12 @@ jobs: | ||||
|             - browser-tools/install-chrome: | ||||
|                 replace-existing: false | ||||
|       - run: npm run test -- --browsers=<<parameters.browser>> | ||||
|       - run: npm run cov:unit:publish | ||||
|       - save_cache_cmd: | ||||
|           node-version: <<parameters.node-version>> | ||||
|       - store_test_results: | ||||
|           path: dist/reports/tests/ | ||||
|       - store_artifacts: | ||||
|           path: coverage | ||||
|           path: dist/reports/ | ||||
|       - generate_and_store_version_and_filesystem_artifacts | ||||
|   e2e-test: | ||||
|     parameters: | ||||
| @@ -133,49 +128,28 @@ jobs: | ||||
|       suite: | ||||
|         type: string | ||||
|     executor: pw-focal-development | ||||
|     parallelism: 4 | ||||
|     steps: | ||||
|       - build_and_install: | ||||
|           node-version: <<parameters.node-version>> | ||||
|       - when: #Only install chrome-beta when running the full suite to save $$$ | ||||
|           condition: | ||||
|             equal: [ "full", <<parameters.suite>> ] | ||||
|           steps: | ||||
|             - run: npx playwright install chrome-beta | ||||
|       - run: SHARD="$((${CIRCLE_NODE_INDEX}+1))"; npm run test:e2e:<<parameters.suite>> -- --shard=${SHARD}/${CIRCLE_NODE_TOTAL} | ||||
|       - generate_e2e_code_cov_report: | ||||
|          suite: <<parameters.suite>>           | ||||
|       - run: npx playwright install | ||||
|       - run: npm run test:e2e:<<parameters.suite>> | ||||
|       - store_test_results: | ||||
|           path: test-results/results.xml | ||||
|       - store_artifacts: | ||||
|           path: test-results | ||||
|       - store_artifacts: | ||||
|           path: coverage | ||||
|       - store_artifacts: | ||||
|           path: html-test-results | ||||
|       - generate_and_store_version_and_filesystem_artifacts | ||||
|   perf-test: | ||||
|     parameters: | ||||
|       node-version: | ||||
|         type: string | ||||
|     executor: pw-focal-development | ||||
|     steps: | ||||
|       - build_and_install: | ||||
|           node-version: <<parameters.node-version>> | ||||
|       - run: npm run test:perf | ||||
|       - store_test_results: | ||||
|           path: test-results/results.xml | ||||
|       - store_artifacts: | ||||
|           path: test-results | ||||
|       - store_artifacts: | ||||
|           path: html-test-results | ||||
|       - generate_and_store_version_and_filesystem_artifacts | ||||
| workflows: | ||||
|   overall-circleci-commit-status: #These jobs run on every commit | ||||
|     jobs: | ||||
|       - lint: | ||||
|           name: node14-lint | ||||
|           name: node16-lint | ||||
|           node-version: lts/gallium | ||||
|       - unit-test:  | ||||
|           name: node14-chrome | ||||
|           node-version: lts/fermium | ||||
|           browser: ChromeHeadless | ||||
|           post-steps: | ||||
|             - upload_code_covio | ||||
|       - unit-test: | ||||
|           name: node16-chrome | ||||
|           node-version: lts/gallium | ||||
| @@ -188,8 +162,6 @@ 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,7 +29,6 @@ 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 --markdown --> | ||||
| <!--- npx envinfo --system --browsers --npmPackages --binaries --languages --markdown --> | ||||
| * Open MCT Version: <!--- date of build, version, or SHA --> | ||||
| * Deployment Type: <!--- npm dev? VIPER Dev? openmct-yamcs? --> | ||||
| * OS: | ||||
| @@ -40,8 +40,6 @@ 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 OR is this a dependency/testcase change? | ||||
| * [ ] Testing instructions included in associated issue? | ||||
|  | ||||
| ### 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@v2 | ||||
|       uses: github/codeql-action/init@v1 | ||||
|       with: | ||||
|         languages: javascript | ||||
|  | ||||
|     - name: Autobuild | ||||
|       uses: github/codeql-action/autobuild@v2 | ||||
|       uses: github/codeql-action/autobuild@v1 | ||||
|  | ||||
|     - name: Perform CodeQL Analysis | ||||
|       uses: github/codeql-action/analyze@v2 | ||||
|       uses: github/codeql-action/analyze@v1 | ||||
|   | ||||
							
								
								
									
										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.21.1 install | ||||
|       - run: npx playwright@1.19.2 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.21.1 install | ||||
|       - run: npx playwright@1.19.2 install | ||||
|       - run: npm install | ||||
|       - name: Run the e2e visual tests | ||||
|         run: npm run test:e2e:visual | ||||
|   | ||||
							
								
								
									
										22
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										22
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -15,6 +15,8 @@ | ||||
| *.idea | ||||
| *.iml | ||||
|  | ||||
| # External dependencies | ||||
|  | ||||
| # Build output | ||||
| target | ||||
| dist | ||||
| @@ -22,24 +24,30 @@ dist | ||||
| # Mac OS X Finder | ||||
| .DS_Store | ||||
|  | ||||
| # Closed source libraries | ||||
| closed-lib | ||||
|  | ||||
| # Node, Bower dependencies | ||||
| node_modules | ||||
| bower_components | ||||
|  | ||||
| # Protractor logs | ||||
| protractor/logs | ||||
|  | ||||
| # npm-debug log | ||||
| npm-debug.log | ||||
|  | ||||
| # karma reports | ||||
| report.*.json | ||||
|  | ||||
| # Lighthouse reports | ||||
| .lighthouseci | ||||
|  | ||||
| # e2e test artifacts | ||||
| test-results | ||||
| html-test-results | ||||
| allure-results | ||||
|  | ||||
| # codecov artifacts | ||||
| .nyc_output | ||||
| coverage | ||||
| codecov | ||||
|  | ||||
| # :( | ||||
| package-lock.json | ||||
|  | ||||
| #codecov artifacts | ||||
| codecov | ||||
|   | ||||
							
								
								
									
										2
									
								
								app.js
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								app.js
									
									
									
									
									
								
							| @@ -49,7 +49,7 @@ class WatchRunPlugin { | ||||
| } | ||||
|  | ||||
| const webpack = require('webpack'); | ||||
| const webpackConfig = process.env.CI ? require('./webpack.coverage.js') : require('./webpack.dev.js'); | ||||
| const webpackConfig = require('./webpack.dev.js'); | ||||
| webpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin()); | ||||
| webpackConfig.plugins.push(new WatchRunPlugin()); | ||||
|  | ||||
|   | ||||
							
								
								
									
										17
									
								
								codecov.yml
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								codecov.yml
									
									
									
									
									
								
							| @@ -13,16 +13,17 @@ coverage: | ||||
|   round: down | ||||
|   range: "66...100" | ||||
|  | ||||
| flags: | ||||
|   unit: | ||||
|     carryforward: true  | ||||
|   e2e-ci: | ||||
|     carryforward: true | ||||
|   e2e-full: | ||||
|     carryforward: true     | ||||
| ignore: | ||||
|  | ||||
| parsers: | ||||
|   gcov: | ||||
|     branch_detection: | ||||
|       conditional: true | ||||
|       loop: true | ||||
|       method: false | ||||
|       macro: false | ||||
|  | ||||
| comment: | ||||
|   layout: "reach,diff,flags,files,footer" | ||||
|   behavior: default | ||||
|   require_changes: false | ||||
|   show_carryforward_flags: true | ||||
| @@ -1,12 +1,4 @@ | ||||
| /* eslint-disable no-undef */ | ||||
| module.exports = { | ||||
|     "extends": ["plugin:playwright/playwright-test"], | ||||
|     "overrides": [ | ||||
|         { | ||||
|             "files": ["tests/visual/*.spec.js"], | ||||
|             "rules": { | ||||
|                 "playwright/no-wait-for-timeout": "off" | ||||
|             } | ||||
|         } | ||||
|     ] | ||||
|     "extends": ["plugin:playwright/playwright-test"] | ||||
| }; | ||||
|   | ||||
| @@ -1,69 +0,0 @@ | ||||
| /* This file extends the base functionality of the playwright test framework to enable | ||||
|  * code coverage instrumentation, console log error detection and working with a 3rd | ||||
|  * party Chrome-as-a-service extension called Browserless. | ||||
|  */ | ||||
|  | ||||
| const base = require('@playwright/test'); | ||||
| const { expect } = require('@playwright/test'); | ||||
| const fs = require('fs'); | ||||
| const path = require('path'); | ||||
| const { v4: uuid } = require('uuid'); | ||||
|  | ||||
| /** | ||||
|  * Takes a `ConsoleMessage` and returns a formatted string | ||||
|  * @param {import('@playwright/test').ConsoleMessage} msg | ||||
|  * @returns {String} formatted string with message type, text, url, and line and column numbers | ||||
|  */ | ||||
| function consoleMessageToString(msg) { | ||||
|     const { url, lineNumber, columnNumber } = msg.location(); | ||||
|  | ||||
|     return `[${msg.type()}] ${msg.text()} | ||||
|     at (${url} ${lineNumber}:${columnNumber})`; | ||||
| } | ||||
|  | ||||
| //The following is based on https://github.com/mxschmitt/playwright-test-coverage | ||||
| // eslint-disable-next-line no-undef | ||||
| const istanbulCLIOutput = path.join(process.cwd(), '.nyc_output'); | ||||
|  | ||||
| // eslint-disable-next-line no-undef | ||||
| exports.test = base.test.extend({ | ||||
|     //The following is based on https://github.com/mxschmitt/playwright-test-coverage | ||||
|     context: async ({ context }, use) => { | ||||
|         await context.addInitScript(() => | ||||
|             window.addEventListener('beforeunload', () => | ||||
|                 (window).collectIstanbulCoverage(JSON.stringify((window).__coverage__)) | ||||
|             ) | ||||
|         ); | ||||
|         await fs.promises.mkdir(istanbulCLIOutput, { recursive: true }); | ||||
|         await context.exposeFunction('collectIstanbulCoverage', (coverageJSON) => { | ||||
|             if (coverageJSON) { | ||||
|                 fs.writeFileSync(path.join(istanbulCLIOutput, `playwright_coverage_${uuid()}.json`), coverageJSON); | ||||
|             } | ||||
|         }); | ||||
|         await use(context); | ||||
|         for (const page of context.pages()) { | ||||
|             await page.evaluate(() => (window).collectIstanbulCoverage(JSON.stringify((window).__coverage__))); | ||||
|         } | ||||
|     }, | ||||
|     page: async ({ baseURL, page }, use) => { | ||||
|         const messages = []; | ||||
|         page.on('console', (msg) => messages.push(msg)); | ||||
|         await use(page); | ||||
|         messages.forEach( | ||||
|             msg => expect.soft(msg.type(), `Console error detected: ${consoleMessageToString(msg)}`).not.toEqual('error') | ||||
|         ); | ||||
|     }, | ||||
|     browser: async ({ playwright, browser }, use, workerInfo) => { | ||||
|         // Use browserless if configured | ||||
|         if (workerInfo.project.name.match(/browserless/)) { | ||||
|             const vBrowser = await playwright.chromium.connectOverCDP({ | ||||
|                 endpointURL: 'ws://localhost:3003' | ||||
|             }); | ||||
|             await use(vBrowser); | ||||
|         } else { | ||||
|             // Use Local Browser for testing. | ||||
|             await use(browser); | ||||
|         } | ||||
|     } | ||||
| }); | ||||
|  | ||||
| @@ -2,42 +2,38 @@ | ||||
| // 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: 3, //Retries 3 times for a total of 4. When running sharded and with maxFailures = 5, this should ensure that flake is managed without failing the full suite | ||||
|     retries: 2, | ||||
|     testDir: 'tests', | ||||
|     testIgnore: '**/*.perf.spec.js', //Ignore performance tests and define in playwright-perfromance.config.js | ||||
|     timeout: 60 * 1000, | ||||
|     timeout: 90 * 1000, | ||||
|     webServer: { | ||||
|         command: 'npm run start', | ||||
|         url: 'http://localhost:8080/#', | ||||
|         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: 'only-on-failure', | ||||
|         trace: 'on-first-retry', | ||||
|         video: 'on-first-retry' | ||||
|         screenshot: 'on', | ||||
|         trace: 'on', | ||||
|         video: 'on' | ||||
|     }, | ||||
|     projects: [ | ||||
|         { | ||||
|             name: 'chrome', | ||||
|             use: { | ||||
|                 browserName: 'chromium' | ||||
|                 browserName: 'chromium', | ||||
|                 ...devices['Desktop Chrome'] | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|             name: 'MMOC', | ||||
|             testMatch: '**/*.e2e.spec.js', // only run e2e tests | ||||
|             grepInvert: /@snapshot/, | ||||
|             use: { | ||||
|                 browserName: 'chromium', | ||||
|                 viewport: { | ||||
| @@ -45,32 +41,19 @@ const config = { | ||||
|                     height: 1440 | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|             name: 'firefox', | ||||
|             testMatch: '**/*.e2e.spec.js', // only run e2e tests | ||||
|             grepInvert: /@snapshot/, | ||||
|             use: { | ||||
|                 browserName: 'firefox' | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|             name: 'chrome-beta', //Only Chrome Beta is available on ubuntu -- not chrome canary | ||||
|             testMatch: '**/*.e2e.spec.js', // only run e2e tests | ||||
|             grepInvert: /@snapshot/, | ||||
|             use: { | ||||
|                 browserName: 'chromium', | ||||
|                 channel: 'chrome-beta' | ||||
|             } | ||||
|         } | ||||
|         /*{ | ||||
|             name: 'ipad', | ||||
|             use: { | ||||
|                 browserName: 'webkit', | ||||
|                 ...devices['iPad (gen 7) landscape'] // Complete List https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json | ||||
|             } | ||||
|         }*/ | ||||
|     ], | ||||
|     reporter: [ | ||||
|         ['list'], | ||||
|         ['html', { | ||||
|             open: 'never', | ||||
|             outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840 | ||||
|         }], | ||||
|         ['junit', { outputFile: 'test-results/results.xml' }], | ||||
|         ['allure-playwright'], | ||||
|         ['github'] | ||||
|     ] | ||||
| }; | ||||
|   | ||||
| @@ -2,18 +2,16 @@ | ||||
| // 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', | ||||
|         url: 'http://localhost:8080/#', | ||||
|         port: 8080, | ||||
|         timeout: 120 * 1000, | ||||
|         reuseExistingServer: !process.env.CI | ||||
|     }, | ||||
| @@ -23,21 +21,20 @@ const config = { | ||||
|         baseURL: 'http://localhost:8080/', | ||||
|         headless: false, | ||||
|         ignoreHTTPSErrors: true, | ||||
|         screenshot: 'only-on-failure', | ||||
|         trace: 'retain-on-failure', | ||||
|         video: 'retain-on-failure' | ||||
|         screenshot: 'on', | ||||
|         trace: 'on', | ||||
|         video: 'on' | ||||
|     }, | ||||
|     projects: [ | ||||
|         { | ||||
|             name: 'chrome', | ||||
|             use: { | ||||
|                 browserName: 'chromium' | ||||
|                 browserName: 'chromium', | ||||
|                 ...devices['Desktop Chrome'] | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|             name: 'MMOC', | ||||
|             testMatch: '**/*.e2e.spec.js', // only run e2e tests | ||||
|             grepInvert: /@snapshot/, | ||||
|             use: { | ||||
|                 browserName: 'chromium', | ||||
|                 viewport: { | ||||
| @@ -45,59 +42,18 @@ const config = { | ||||
|                     height: 1440 | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|             name: 'safari', | ||||
|             testMatch: '**/*.e2e.spec.js', // only run e2e tests | ||||
|             grep: /@ipad/, // only run ipad tests due to this bug https://github.com/microsoft/playwright/issues/8340 | ||||
|             grepInvert: /@snapshot/, | ||||
|             use: { | ||||
|                 browserName: 'webkit' | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|             name: 'firefox', | ||||
|             testMatch: '**/*.e2e.spec.js', // only run e2e tests | ||||
|             grepInvert: /@snapshot/, | ||||
|             use: { | ||||
|                 browserName: 'firefox' | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|             name: 'canary', | ||||
|             testMatch: '**/*.e2e.spec.js', // only run e2e tests | ||||
|             grepInvert: /@snapshot/, | ||||
|             use: { | ||||
|                 browserName: 'chromium', | ||||
|                 channel: 'chrome-canary' //Note this is not available in ubuntu/CircleCI | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|             name: 'chrome-beta', | ||||
|             testMatch: '**/*.e2e.spec.js', // only run e2e tests | ||||
|             grepInvert: /@snapshot/, | ||||
|             use: { | ||||
|                 browserName: 'chromium', | ||||
|                 channel: 'chrome-beta' | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|         } | ||||
|         /*{ | ||||
|             name: 'ipad', | ||||
|             testMatch: '**/*.e2e.spec.js', // only run e2e tests | ||||
|             grep: /@ipad/, | ||||
|             grepInvert: /@snapshot/, | ||||
|             use: { | ||||
|                 browserName: 'webkit', | ||||
|                 ...devices['iPad (gen 7) landscape'] // Complete List https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json | ||||
|             } | ||||
|         } | ||||
|         }*/ | ||||
|     ], | ||||
|     reporter: [ | ||||
|         ['list'], | ||||
|         ['html', { | ||||
|             open: 'on-failure', | ||||
|             outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840 | ||||
|         }] | ||||
|         ['allure-playwright'] | ||||
|     ] | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -1,41 +0,0 @@ | ||||
| /* eslint-disable no-undef */ | ||||
| // playwright.config.js | ||||
| // @ts-check | ||||
|  | ||||
| /** @type {import('@playwright/test').PlaywrightTestConfig} */ | ||||
| const config = { | ||||
|     retries: 1, //Only for debugging purposes because trace is enabled only on first retry | ||||
|     testDir: 'tests/performance/', | ||||
|     timeout: 60 * 1000, | ||||
|     workers: 1, //Only run in serial with 1 worker | ||||
|     webServer: { | ||||
|         command: 'npm run start', | ||||
|         url: 'http://localhost:8080/#', | ||||
|         timeout: 200 * 1000, | ||||
|         reuseExistingServer: !process.env.CI | ||||
|     }, | ||||
|     use: { | ||||
|         browserName: "chromium", | ||||
|         baseURL: 'http://localhost:8080/', | ||||
|         headless: Boolean(process.env.CI), //Only if running locally | ||||
|         ignoreHTTPSErrors: true, | ||||
|         screenshot: 'off', | ||||
|         trace: 'on-first-retry', | ||||
|         video: 'off' | ||||
|     }, | ||||
|     projects: [ | ||||
|         { | ||||
|             name: 'chrome', | ||||
|             use: { | ||||
|                 browserName: 'chromium' | ||||
|             } | ||||
|         } | ||||
|     ], | ||||
|     reporter: [ | ||||
|         ['list'], | ||||
|         ['junit', { outputFile: 'test-results/results.xml' }], | ||||
|         ['json', { outputFile: 'test-results/results.json' }] | ||||
|     ] | ||||
| }; | ||||
|  | ||||
| module.exports = config; | ||||
| @@ -4,20 +4,20 @@ | ||||
|  | ||||
| /** @type {import('@playwright/test').PlaywrightTestConfig} */ | ||||
| const config = { | ||||
|     retries: 0, // visual tests should never retry due to snapshot comparison errors | ||||
|     testDir: 'tests/visual', | ||||
|     retries: 0, | ||||
|     testDir: 'tests', | ||||
|     timeout: 90 * 1000, | ||||
|     workers: 1, // visual tests should never run in parallel due to test pollution | ||||
|     workers: 1, | ||||
|     webServer: { | ||||
|         command: 'npm run start', | ||||
|         url: 'http://localhost:8080/#', | ||||
|         port: 8080, | ||||
|         timeout: 200 * 1000, | ||||
|         reuseExistingServer: !process.env.CI | ||||
|     }, | ||||
|     use: { | ||||
|         browserName: "chromium", | ||||
|         baseURL: 'http://localhost:8080/', | ||||
|         headless: true, // this needs to remain headless to avoid visual changes due to GPU | ||||
|         headless: true, | ||||
|         ignoreHTTPSErrors: true, | ||||
|         screenshot: 'on', | ||||
|         trace: 'off', | ||||
| @@ -25,7 +25,8 @@ const config = { | ||||
|     }, | ||||
|     reporter: [ | ||||
|         ['list'], | ||||
|         ['junit', { outputFile: 'test-results/results.xml' }] | ||||
|         ['junit', { outputFile: 'test-results/results.xml' }], | ||||
|         ['allure-playwright'] | ||||
|     ] | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -1 +0,0 @@ | ||||
| {"openmct":{"b3cee102-86dd-4c0a-8eec-4d5d276f8691":{"identifier":{"key":"b3cee102-86dd-4c0a-8eec-4d5d276f8691","namespace":""},"name":"Performance Display Layout","type":"layout","composition":[{"key":"9666e7b4-be0c-47a5-94b8-99accad7155e","namespace":""}],"configuration":{"items":[{"width":32,"height":18,"x":12,"y":9,"identifier":{"key":"9666e7b4-be0c-47a5-94b8-99accad7155e","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"23ca351d-a67d-46aa-a762-290eb742d2f1"}],"layoutGrid":[10,10]},"modified":1654299875432,"location":"mine","persisted":1654299878751},"9666e7b4-be0c-47a5-94b8-99accad7155e":{"identifier":{"key":"9666e7b4-be0c-47a5-94b8-99accad7155e","namespace":""},"name":"Performance Example Imagery","type":"example.imagery","configuration":{"imageLocation":"","imageLoadDelayInMilliSeconds":20000,"imageSamples":[],"layers":[{"source":"dist/imagery/example-imagery-layer-16x9.png","name":"16:9","visible":false},{"source":"dist/imagery/example-imagery-layer-safe.png","name":"Safe","visible":false},{"source":"dist/imagery/example-imagery-layer-scale.png","name":"Scale","visible":false}]},"telemetry":{"values":[{"name":"Name","key":"name"},{"name":"Time","key":"utc","format":"utc","hints":{"domain":2}},{"name":"Local Time","key":"local","format":"local-format","hints":{"domain":1}},{"name":"Image","key":"url","format":"image","hints":{"image":1},"layers":[{"source":"dist/imagery/example-imagery-layer-16x9.png","name":"16:9"},{"source":"dist/imagery/example-imagery-layer-safe.png","name":"Safe"},{"source":"dist/imagery/example-imagery-layer-scale.png","name":"Scale"}]},{"name":"Image Download Name","key":"imageDownloadName","format":"imageDownloadName","hints":{"imageDownloadName":1}}]},"modified":1654299840077,"location":"b3cee102-86dd-4c0a-8eec-4d5d276f8691","persisted":1654299840078}},"rootId":"b3cee102-86dd-4c0a-8eec-4d5d276f8691"} | ||||
| @@ -1 +0,0 @@ | ||||
| {"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"} | ||||
| @@ -1,22 +0,0 @@ | ||||
| { | ||||
|   "cookies": [], | ||||
|   "origins": [ | ||||
|     { | ||||
|       "origin": "http://localhost:8080", | ||||
|       "localStorage": [ | ||||
|         { | ||||
|           "name": "tcHistory", | ||||
|           "value": "{\"utc\":[{\"start\":1654548551471,\"end\":1654550351471}]}" | ||||
|         }, | ||||
|         { | ||||
|           "name": "mct", | ||||
|           "value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"527856c0-cced-4b64-bb19-f943432326d0\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1654550352296,\"modified\":1654550352296},\"527856c0-cced-4b64-bb19-f943432326d0\":{\"identifier\":{\"key\":\"527856c0-cced-4b64-bb19-f943432326d0\",\"namespace\":\"\"},\"name\":\"Unnamed Overlay Plot\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"ce88ce37-8bb9-45e1-a85b-bb7e3c8453b9\",\"namespace\":\"\"}],\"configuration\":{\"series\":[{\"identifier\":{\"key\":\"ce88ce37-8bb9-45e1-a85b-bb7e3c8453b9\",\"namespace\":\"\"}}],\"yAxis\":{},\"xAxis\":{}},\"modified\":1654550353356,\"location\":\"mine\",\"persisted\":1654550353357},\"ce88ce37-8bb9-45e1-a85b-bb7e3c8453b9\":{\"name\":\"Unnamed Sine Wave Generator\",\"type\":\"generator\",\"identifier\":{\"key\":\"ce88ce37-8bb9-45e1-a85b-bb7e3c8453b9\",\"namespace\":\"\"},\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":\"5000\"},\"modified\":1654550353350,\"location\":\"527856c0-cced-4b64-bb19-f943432326d0\",\"persisted\":1654550353350}}" | ||||
|         }, | ||||
|         { | ||||
|           "name": "mct-tree-expanded", | ||||
|           "value": "[\"/browse/mine\"]" | ||||
|         } | ||||
|       ] | ||||
|     } | ||||
|   ] | ||||
| } | ||||
| @@ -1,22 +0,0 @@ | ||||
| { | ||||
|   "cookies": [], | ||||
|   "origins": [ | ||||
|     { | ||||
|       "origin": "http://localhost:8080", | ||||
|       "localStorage": [ | ||||
|         { | ||||
|           "name": "tcHistory", | ||||
|           "value": "{\"utc\":[{\"start\":1654537164464,\"end\":1654538964464},{\"start\":1652301954635,\"end\":1652303754635}]}" | ||||
|         }, | ||||
|         { | ||||
|           "name": "mct", | ||||
|           "value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1654538965703,\"modified\":1654538965703},\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"73f2d9ae-d1f3-4561-b7fc-ecd5df557249\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1652303755999,\"location\":\"mine\",\"persisted\":1652303756002},\"2d02a680-eb7e-4645-bba2-dd298f76efb8\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"4291d80c-303c-4d8d-85e1-10f012b864fb\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1654538965702,\"location\":\"mine\",\"persisted\":1654538965702}}" | ||||
|         }, | ||||
|         { | ||||
|           "name": "mct-tree-expanded", | ||||
|           "value": "[]" | ||||
|         } | ||||
|       ] | ||||
|     } | ||||
|   ] | ||||
| } | ||||
| @@ -1,77 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * 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,8 +24,7 @@ | ||||
| This test suite is dedicated to tests which verify branding related components. | ||||
| */ | ||||
|  | ||||
| const { test } = require('../fixtures.js'); | ||||
| const { expect } = require('@playwright/test'); | ||||
| const { test, expect } = require('@playwright/test'); | ||||
|  | ||||
| test.describe('Branding tests', () => { | ||||
|     test('About Modal launches with basic branding properties', async ({ page }) => { | ||||
| @@ -58,7 +57,6 @@ test.describe('Branding tests', () => { | ||||
|             page.waitForEvent('popup'), | ||||
|             page.locator('text=click here for third party licensing information').click() | ||||
|         ]); | ||||
|         await page2.waitForLoadState('networkidle'); //Avoids timing issues with juggler/firefox | ||||
|         expect(page2.waitForURL('**/licenses**')).toBeTruthy(); | ||||
|         expect(page2.waitForURL('**\/licenses**')).toBeTruthy(); | ||||
|     }); | ||||
| }); | ||||
|   | ||||
| @@ -24,8 +24,7 @@ | ||||
| This test suite is dedicated to tests which verify the basic operations surrounding the example event generator. | ||||
| */ | ||||
|  | ||||
| const { test } = require('../../fixtures.js'); | ||||
| const { expect } = require('@playwright/test'); | ||||
| const { test, expect } = require('@playwright/test'); | ||||
|  | ||||
| test.describe('Example Event Generator Operations', () => { | ||||
|     test('Can create example event generator with a name', async ({ page }) => { | ||||
|   | ||||
| @@ -24,13 +24,10 @@ | ||||
| This test suite is dedicated to tests which verify the basic operations surrounding conditionSets. | ||||
| */ | ||||
|  | ||||
| const { test } = require('../../../fixtures.js'); | ||||
| const { expect } = require('@playwright/test'); | ||||
| const { test, expect } = require('@playwright/test'); | ||||
|  | ||||
| test.describe('Sine Wave Generator', () => { | ||||
|     test('Create new Sine Wave Generator Object and validate create Form Logic', async ({ page, browserName }) => { | ||||
|         test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox'); | ||||
|  | ||||
|     test('Create new Sine Wave Generator Object and validate create Form Logic', async ({ page }) => { | ||||
|         //Go to baseURL | ||||
|         await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
| @@ -42,45 +39,44 @@ test.describe('Sine Wave Generator', () => { | ||||
|  | ||||
|         // Verify that the each required field has required indicator | ||||
|         // Title | ||||
|         await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(/req/); | ||||
|         await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(['c-form-row__state-indicator  req']); | ||||
|  | ||||
|         // Verify that the Notes row does not have a required indicator | ||||
|         await expect(page.locator('.c-form__section div:nth-child(3) .form-row .c-form-row__state-indicator')).not.toContain('.req'); | ||||
|         await page.locator('textarea[type="text"]').fill('Optional Note Text'); | ||||
|  | ||||
|         // Period | ||||
|         await expect(page.locator('div:nth-child(4) .c-form-row__state-indicator')).toHaveClass(/req/); | ||||
|         await expect(page.locator('.c-form__section div:nth-child(4) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator  req']); | ||||
|  | ||||
|         // Amplitude | ||||
|         await expect(page.locator('div:nth-child(5) .c-form-row__state-indicator')).toHaveClass(/req/); | ||||
|         await expect(page.locator('.c-form__section div:nth-child(5) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator  req']); | ||||
|  | ||||
|         // Offset | ||||
|         await expect(page.locator('div:nth-child(6) .c-form-row__state-indicator')).toHaveClass(/req/); | ||||
|         await expect(page.locator('.c-form__section div:nth-child(6) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator  req']); | ||||
|  | ||||
|         // Data Rate | ||||
|         await expect(page.locator('div:nth-child(7) .c-form-row__state-indicator')).toHaveClass(/req/); | ||||
|         await expect(page.locator('.c-form__section div:nth-child(7) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator  req']); | ||||
|  | ||||
|         // Phase | ||||
|         await expect(page.locator('div:nth-child(8) .c-form-row__state-indicator')).toHaveClass(/req/); | ||||
|         await expect(page.locator('.c-form__section div:nth-child(8) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator  req']); | ||||
|  | ||||
|         // Randomness | ||||
|         await expect(page.locator('div:nth-child(9) .c-form-row__state-indicator')).toHaveClass(/req/); | ||||
|         await expect(page.locator('.c-form__section div:nth-child(9) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator  req']); | ||||
|  | ||||
|         // Verify that by removing value from required text field shows invalid indicator | ||||
|         await page.locator('text=Properties Title Notes Period Amplitude Offset Data Rate (hz) Phase (radians) Ra >> input[type="text"]').fill(''); | ||||
|         await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(/invalid/); | ||||
|         await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(['c-form-row__state-indicator  req invalid']); | ||||
|  | ||||
|         // Verify that by adding value to empty required text field changes invalid to valid indicator | ||||
|         await page.locator('text=Properties Title Notes Period Amplitude Offset Data Rate (hz) Phase (radians) Ra >> input[type="text"]').fill('New Sine Wave Generator'); | ||||
|         await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(/valid/); | ||||
|         await page.locator('text=Properties Title Notes Period Amplitude Offset Data Rate (hz) Phase (radians) Ra >> input[type="text"]').fill('non empty'); | ||||
|         await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(['c-form-row__state-indicator  req valid']); | ||||
|  | ||||
|         // Verify that by removing value from required number field shows invalid indicator | ||||
|         await page.locator('.field.control.l-input-sm input').first().fill(''); | ||||
|         await expect(page.locator('div:nth-child(4) .c-form-row__state-indicator')).toHaveClass(/invalid/); | ||||
|         await expect(page.locator('.c-form__section div:nth-child(4) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator  req invalid']); | ||||
|  | ||||
|         // Verify that by adding value to empty required number field changes invalid to valid indicator | ||||
|         await page.locator('.field.control.l-input-sm input').first().fill('3'); | ||||
|         await expect(page.locator('div:nth-child(4) .c-form-row__state-indicator')).toHaveClass(/valid/); | ||||
|         await expect(page.locator('.c-form__section div:nth-child(4) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator  req valid']); | ||||
|  | ||||
|         // Verify that can change value of number field by up/down arrows keys | ||||
|         // Click .field.control.l-input-sm input >> nth=0 | ||||
| @@ -93,6 +89,57 @@ test.describe('Sine Wave Generator', () => { | ||||
|         const value = await page.locator('.field.control.l-input-sm input').first().inputValue(); | ||||
|         await expect(value).toBe('6'); | ||||
|  | ||||
|         // Click .c-form-row__state-indicator.grows | ||||
|         await page.locator('.c-form-row__state-indicator.grows').click(); | ||||
|  | ||||
|         // Click text=Properties Title Notes Period Amplitude Offset Data Rate (hz) Phase (radians) Ra >> input[type="text"] | ||||
|         await page.locator('text=Properties Title Notes Period Amplitude Offset Data Rate (hz) Phase (radians) Ra >> input[type="text"]').click(); | ||||
|  | ||||
|         // Click .c-form-row__state-indicator >> nth=0 | ||||
|         await page.locator('.c-form-row__state-indicator').first().click(); | ||||
|  | ||||
|         // Fill text=Properties Title Notes Period Amplitude Offset Data Rate (hz) Phase (radians) Ra >> input[type="text"] | ||||
|         await page.locator('text=Properties Title Notes Period Amplitude Offset Data Rate (hz) Phase (radians) Ra >> input[type="text"]').fill('New Sine Wave Generator'); | ||||
|  | ||||
|         // Double click div:nth-child(4) .form-row .c-form-row__controls | ||||
|         await page.locator('div:nth-child(4) .form-row .c-form-row__controls').dblclick(); | ||||
|  | ||||
|         // Click .field.control.l-input-sm input >> nth=0 | ||||
|         await page.locator('.field.control.l-input-sm input').first().click(); | ||||
|  | ||||
|         // Click div:nth-child(4) .form-row .c-form-row__state-indicator | ||||
|         await page.locator('div:nth-child(4) .form-row .c-form-row__state-indicator').click(); | ||||
|  | ||||
|         // Click .field.control.l-input-sm input >> nth=0 | ||||
|         await page.locator('.field.control.l-input-sm input').first().click(); | ||||
|  | ||||
|         // Click .field.control.l-input-sm input >> nth=0 | ||||
|         await page.locator('.field.control.l-input-sm input').first().click(); | ||||
|  | ||||
|         // Click div:nth-child(5) .form-row .c-form-row__controls .form-control .field input | ||||
|         await page.locator('div:nth-child(5) .form-row .c-form-row__controls .form-control .field input').click(); | ||||
|  | ||||
|         // Click div:nth-child(5) .form-row .c-form-row__controls .form-control .field input | ||||
|         await page.locator('div:nth-child(5) .form-row .c-form-row__controls .form-control .field input').click(); | ||||
|  | ||||
|         // Click div:nth-child(5) .form-row .c-form-row__controls .form-control .field input | ||||
|         await page.locator('div:nth-child(5) .form-row .c-form-row__controls .form-control .field input').click(); | ||||
|  | ||||
|         // Click div:nth-child(6) .form-row .c-form-row__controls .form-control .field input | ||||
|         await page.locator('div:nth-child(6) .form-row .c-form-row__controls .form-control .field input').click(); | ||||
|  | ||||
|         // Double click div:nth-child(7) .form-row .c-form-row__controls .form-control .field input | ||||
|         await page.locator('div:nth-child(7) .form-row .c-form-row__controls .form-control .field input').dblclick(); | ||||
|  | ||||
|         // Click div:nth-child(7) .form-row .c-form-row__state-indicator | ||||
|         await page.locator('div:nth-child(7) .form-row .c-form-row__state-indicator').click(); | ||||
|  | ||||
|         // Click div:nth-child(7) .form-row .c-form-row__controls .form-control .field input | ||||
|         await page.locator('div:nth-child(7) .form-row .c-form-row__controls .form-control .field input').click(); | ||||
|  | ||||
|         // Fill div:nth-child(7) .form-row .c-form-row__controls .form-control .field input | ||||
|         await page.locator('div:nth-child(7) .form-row .c-form-row__controls .form-control .field input').fill('3'); | ||||
|  | ||||
|         //Click text=OK | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
| @@ -103,7 +150,7 @@ test.describe('Sine Wave Generator', () => { | ||||
|         // Verify object properties | ||||
|         await expect(page.locator('.l-browse-bar__object-name')).toContainText('New Sine Wave Generator'); | ||||
|  | ||||
|         // Verify canvas rendered and can be interacted with | ||||
|         // Verify canvas rendered | ||||
|         await page.locator('canvas').nth(1).click({ | ||||
|             position: { | ||||
|                 x: 341, | ||||
|   | ||||
| @@ -24,114 +24,19 @@ | ||||
| This test suite is dedicated to tests which verify the basic operations surrounding moving objects. | ||||
| */ | ||||
|  | ||||
| const { test } = require('../fixtures.js'); | ||||
| const { expect } = require('@playwright/test'); | ||||
| const { test, expect } = require('@playwright/test'); | ||||
|  | ||||
| test.describe('Move item tests', () => { | ||||
|     test('Create a basic object and verify that it can be moved to another folder', async ({ page }) => { | ||||
|         // Go to Open MCT | ||||
|         await page.goto('/'); | ||||
|  | ||||
|         // Create a new folder in the root my items folder | ||||
|         let folder1 = "Folder1"; | ||||
|         await page.locator('button:has-text("Create")').click(); | ||||
|         await page.locator('li.icon-folder').click(); | ||||
|  | ||||
|         await page.locator('text=Properties Title Notes >> input[type="text"]').click(); | ||||
|         await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder1); | ||||
|  | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('text=OK').click(), | ||||
|             page.waitForSelector('.c-message-banner__message') | ||||
|         ]); | ||||
|         //Wait until Save Banner is gone | ||||
|         await page.locator('.c-message-banner__close-button').click(); | ||||
|         await page.waitForSelector('.c-message-banner__message', { state: 'detached'}); | ||||
|  | ||||
|         // Create another folder with a new name at default location, which is currently inside Folder 1 | ||||
|         let folder2 = "Folder2"; | ||||
|         await page.locator('button:has-text("Create")').click(); | ||||
|         await page.locator('li.icon-folder').click(); | ||||
|         await page.locator('text=Properties Title Notes >> input[type="text"]').click(); | ||||
|         await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder2); | ||||
|  | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('text=OK').click(), | ||||
|             page.waitForSelector('.c-message-banner__message') | ||||
|         ]); | ||||
|         //Wait until Save Banner is gone | ||||
|         await page.locator('.c-message-banner__close-button').click(); | ||||
|         await page.waitForSelector('.c-message-banner__message', { state: 'detached'}); | ||||
|  | ||||
|         // Move Folder 2 from Folder 1 to My Items | ||||
|         await page.locator('text=Open MCT My Items >> span').nth(3).click(); | ||||
|         await page.locator('.c-tree__scrollable div div:nth-child(2) .c-tree__item .c-tree__item__view-control').click(); | ||||
|  | ||||
|         await page.locator(`a:has-text("${folder2}")`).click({ | ||||
|             button: 'right' | ||||
|         }); | ||||
|         await page.locator('li.icon-move').click(); | ||||
|         await page.locator('form[name="mctForm"] >> text=My Items').click(); | ||||
|  | ||||
|         await page.locator('text=OK').click(); | ||||
|  | ||||
|         // Expect that Folder 2 is in My Items, the root folder | ||||
|         expect(page.locator(`text=My Items >> nth=0:has(text=${folder2})`)).toBeTruthy(); | ||||
|     test.fixme('Create a basic object and verify that it 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 cannot be moved to telemetry object without Composition Provider', async ({ page }) => { | ||||
|         // Go to Open MCT | ||||
|         await page.goto('/'); | ||||
|  | ||||
|         // Create Telemetry Table | ||||
|         let telemetryTable = 'Test Telemetry Table'; | ||||
|         await page.locator('button:has-text("Create")').click(); | ||||
|         await page.locator('li:has-text("Telemetry Table")').click(); | ||||
|         await page.locator('text=Properties Title Notes >> input[type="text"]').click(); | ||||
|         await page.locator('text=Properties Title Notes >> input[type="text"]').fill(telemetryTable); | ||||
|  | ||||
|         await page.locator('text=OK').click(); | ||||
|  | ||||
|         // Finish editing and save Telemetry Table | ||||
|         await page.locator('.c-button--menu.c-button--major.icon-save').click(); | ||||
|         await page.locator('text=Save and Finish Editing').click(); | ||||
|  | ||||
|         // Create New Folder Basic Domain Object | ||||
|         let folder = 'Test Folder'; | ||||
|         await page.locator('button:has-text("Create")').click(); | ||||
|         await page.locator('li:has-text("Folder")').click(); | ||||
|         await page.locator('text=Properties Title Notes >> input[type="text"]').click(); | ||||
|         await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder); | ||||
|  | ||||
|         // See if it's possible to put the folder in the Telemetry object during creation (Soft Assert) | ||||
|         await page.locator(`form[name="mctForm"] >> text=${telemetryTable}`).click(); | ||||
|         let okButton = await page.locator('button.c-button.c-button--major:has-text("OK")'); | ||||
|         let okButtonStateDisabled = await okButton.isDisabled(); | ||||
|         expect.soft(okButtonStateDisabled).toBeTruthy(); | ||||
|  | ||||
|         // Continue test regardless of assertion and create it in My Items | ||||
|         await page.locator('form[name="mctForm"] >> text=My Items').click(); | ||||
|         await page.locator('text=OK').click(); | ||||
|  | ||||
|         // Open My Items | ||||
|         await page.locator('text=Open MCT My Items >> span').nth(3).click(); | ||||
|  | ||||
|         // Select Folder Object and select Move from context menu | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator(`a:has-text("${folder}")`).click() | ||||
|         ]); | ||||
|         await page.locator('.c-tree__item.is-navigated-object .c-tree__item__label .c-tree__item__type-icon').click({ | ||||
|             button: 'right' | ||||
|         }); | ||||
|         await page.locator('li.icon-move').click(); | ||||
|  | ||||
|         // See if it's possible to put the folder in the Telemetry object after creation | ||||
|         await page.locator('text=Location Open MCT My Items >> span').nth(3).click(); | ||||
|         await page.locator(`form[name="mctForm"] >> text=${telemetryTable}`).click(); | ||||
|         let okButton2 = await page.locator('button.c-button.c-button--major:has-text("OK")'); | ||||
|         let okButtonStateDisabled2 = await okButton2.isDisabled(); | ||||
|         expect(okButtonStateDisabled2).toBeTruthy(); | ||||
|     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. | ||||
|     }); | ||||
| }); | ||||
|   | ||||
| @@ -1,177 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* | ||||
| This test suite is dedicated to performance tests to ensure that testability of performance | ||||
| is not broken upstream on Open MCT. Any assumptions made downstream will be tested here | ||||
|  | ||||
| TODO: | ||||
|  - Update resolution of performance config | ||||
|  - Add Performance Observer on init to push all performance marks | ||||
|  - Move client CDP connection to before or to a fixture | ||||
|  - | ||||
|  | ||||
| */ | ||||
|  | ||||
| const { test, expect } = require('@playwright/test'); | ||||
|  | ||||
| const filePath = 'e2e/test-data/PerformanceDisplayLayout.json'; | ||||
|  | ||||
| test.describe('Performance tests', () => { | ||||
|     test.beforeEach(async ({ page, browser }, testInfo) => { | ||||
|         // Go to baseURL | ||||
|         await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|         // Click a:has-text("My Items") | ||||
|         await page.locator('a:has-text("My Items")').click({ | ||||
|             button: 'right' | ||||
|         }); | ||||
|  | ||||
|         // Click text=Import from JSON | ||||
|         await page.locator('text=Import from JSON').click(); | ||||
|  | ||||
|         // Upload Performance Display Layout.json | ||||
|         await page.setInputFiles('#fileElem', filePath); | ||||
|  | ||||
|         // Click text=OK | ||||
|         await page.locator('text=OK').click(); | ||||
|  | ||||
|         await expect(page.locator('a:has-text("Performance Display Layout Display Layout")')).toBeVisible(); | ||||
|  | ||||
|         //Create a Chrome Performance Timeline trace to store as a test artifact | ||||
|         console.log("\n==== Devtools: startTracing ====\n"); | ||||
|         await browser.startTracing(page, { | ||||
|             path: `${testInfo.outputPath()}-trace.json`, | ||||
|             screenshots: true | ||||
|         }); | ||||
|     }); | ||||
|     test.afterEach(async ({ page, browser}) => { | ||||
|         console.log("\n==== Devtools: stopTracing ====\n"); | ||||
|         await browser.stopTracing(); | ||||
|  | ||||
|         /* Measurement Section | ||||
|         / The following section includes a block of performance measurements. | ||||
|         */ | ||||
|         //Get time difference between viewlarge actionability and evaluate time | ||||
|         await page.evaluate(() => (window.performance.measure("machine-time-difference", "viewlarge.start", "viewLarge.start.test"))); | ||||
|  | ||||
|         //Get StartTime | ||||
|         const startTime = await page.evaluate(() => window.performance.timing.navigationStart); | ||||
|         console.log('window.performance.timing.navigationStart', startTime); | ||||
|  | ||||
|         //Get All Performance Marks | ||||
|         const getAllMarksJson = await page.evaluate(() => JSON.stringify(window.performance.getEntriesByType("mark"))); | ||||
|         const getAllMarks = JSON.parse(getAllMarksJson); | ||||
|         console.log('window.performance.getEntriesByType("mark")', getAllMarks); | ||||
|  | ||||
|         //Get All Performance Measures | ||||
|         const getAllMeasuresJson = await page.evaluate(() => JSON.stringify(window.performance.getEntriesByType("measure"))); | ||||
|         const getAllMeasures = JSON.parse(getAllMeasuresJson); | ||||
|         console.log('window.performance.getEntriesByType("measure")', getAllMeasures); | ||||
|  | ||||
|     }); | ||||
|     /* The following test will navigate to a previously created Performance Display Layout and measure the | ||||
|     /  following metrics: | ||||
|     /  - ElementResourceTiming | ||||
|     /  - Interaction Timing | ||||
|     */ | ||||
|     test('Embedded View Large for Imagery is performant in Fixed Time', async ({ page, browser }) => { | ||||
|         const client = await page.context().newCDPSession(page); | ||||
|         // Tell the DevTools session to record performance metrics | ||||
|         // https://chromedevtools.github.io/devtools-protocol/tot/Performance/#method-getMetrics | ||||
|         await client.send('Performance.enable'); | ||||
|         // Go to baseURL | ||||
|         await page.goto('/'); | ||||
|  | ||||
|         // Search Available after Launch | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); | ||||
|         await page.evaluate(() => window.performance.mark("search-available")); | ||||
|         // Fill Search input | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Performance Display Layout'); | ||||
|         await page.evaluate(() => window.performance.mark("search-entered")); | ||||
|         //Search Result Appears and is clicked | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('a:has-text("Performance Display Layout")').first().click(), | ||||
|             page.evaluate(() => window.performance.mark("click-search-result")) | ||||
|         ]); | ||||
|  | ||||
|         //Time to Example Imagery Frame loads within Display Layout | ||||
|         await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible'}); | ||||
|         //Time to Example Imagery object loads | ||||
|         await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible'}); | ||||
|  | ||||
|         //Get background-image url from background-image css prop | ||||
|         const backgroundImage = await page.locator('.c-imagery__main-image__background-image'); | ||||
|         let backgroundImageUrl = await backgroundImage.evaluate((el) => { | ||||
|             return window.getComputedStyle(el).getPropertyValue('background-image').match(/url\(([^)]+)\)/)[1]; | ||||
|         }); | ||||
|         backgroundImageUrl = backgroundImageUrl.slice(1, -1); //forgive me, padre | ||||
|         console.log('backgroundImageurl ' + backgroundImageUrl); | ||||
|  | ||||
|         //Get ResourceTiming of background-image jpg | ||||
|         const resourceTimingJson = await page.evaluate((bgImageUrl) => | ||||
|             JSON.stringify(window.performance.getEntriesByName(bgImageUrl).pop()), | ||||
|         backgroundImageUrl | ||||
|         ); | ||||
|         console.log('resourceTimingJson ' + resourceTimingJson); | ||||
|  | ||||
|         //Open Large view | ||||
|         await page.locator('button:has-text("Large View")').click(); //This action includes the performance.mark named 'viewLarge.start' | ||||
|         await page.evaluate(() => window.performance.mark("viewLarge.start.test")); //This is a mark only to compare evaluate timing | ||||
|  | ||||
|         //Time to Imagery Rendered in Large Frame | ||||
|         await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible'}); | ||||
|         await page.evaluate(() => window.performance.mark("background-image-frame")); | ||||
|  | ||||
|         //Time to Example Imagery object loads | ||||
|         await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible'}); | ||||
|         await page.evaluate(() => window.performance.mark("background-image-visible")); | ||||
|  | ||||
|         // Get Current number of images in thumbstrip | ||||
|         await page.waitForSelector('.c-imagery__thumb'); | ||||
|         const thumbCount = await page.locator('.c-imagery__thumb').count(); | ||||
|         console.log('number of thumbs rendered ' + thumbCount); | ||||
|         await page.locator('.c-imagery__thumb').last().click(); | ||||
|  | ||||
|         //Get ResourceTiming of all jpg resources | ||||
|         const resourceTimingJson2 = await page.evaluate(() => | ||||
|             JSON.stringify(window.performance.getEntriesByType('resource')) | ||||
|         ); | ||||
|         const resourceTiming = JSON.parse(resourceTimingJson2); | ||||
|         const jpgResourceTiming = resourceTiming.find((element) => | ||||
|             element.name.includes('.jpg') | ||||
|         ); | ||||
|         console.log('jpgResourceTiming ' + JSON.stringify(jpgResourceTiming)); | ||||
|  | ||||
|         // Click Close Icon | ||||
|         await page.locator('[aria-label="Close"]').click(); | ||||
|         await page.evaluate(() => window.performance.mark("view-large-close-button")); | ||||
|  | ||||
|         //await client.send('HeapProfiler.enable'); | ||||
|         await client.send('HeapProfiler.collectGarbage'); | ||||
|  | ||||
|         let performanceMetrics = await client.send('Performance.getMetrics'); | ||||
|         console.log(performanceMetrics.metrics); | ||||
|  | ||||
|     }); | ||||
| }); | ||||
| @@ -1,119 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* | ||||
| This test suite is an initial example for memory leak testing using performance. This configuration and execution must | ||||
| be kept separate from the traditional performance measurements to avoid any "observer" effects associated with tracing | ||||
| or profiling playwright and/or the browser. | ||||
|  | ||||
| Based on a pattern identified in https://github.com/trentmwillis/devtools-protocol-demos/blob/master/testing-demos/memory-leak-by-heap.js | ||||
| and https://github.com/paulirish/automated-chrome-profiling/issues/3 | ||||
|  | ||||
| Best path forward: https://github.com/cowchimp/headless-devtools/blob/master/src/Memory/example.js | ||||
|  | ||||
| */ | ||||
|  | ||||
| const { test, expect } = require('@playwright/test'); | ||||
|  | ||||
| const filePath = 'e2e/test-data/PerformanceDisplayLayout.json'; | ||||
|  | ||||
| // eslint-disable-next-line playwright/no-skipped-test | ||||
| test.describe.skip('Memory Performance tests', () => { | ||||
|     test.beforeEach(async ({ page, browser }, testInfo) => { | ||||
|         // Go to baseURL | ||||
|         await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|         // Click a:has-text("My Items") | ||||
|         await page.locator('a:has-text("My Items")').click({ | ||||
|             button: 'right' | ||||
|         }); | ||||
|  | ||||
|         // Click text=Import from JSON | ||||
|         await page.locator('text=Import from JSON').click(); | ||||
|  | ||||
|         // Upload Performance Display Layout.json | ||||
|         await page.setInputFiles('#fileElem', filePath); | ||||
|  | ||||
|         // Click text=OK | ||||
|         await page.locator('text=OK').click(); | ||||
|  | ||||
|         await expect(page.locator('a:has-text("Performance Display Layout Display Layout")')).toBeVisible(); | ||||
|     }); | ||||
|  | ||||
|     test('Embedded View Large for Imagery is performant in Fixed Time', async ({ page, browser }) => { | ||||
|  | ||||
|         await page.goto('/', {waitUntil: 'networkidle'}); | ||||
|  | ||||
|         // To to Search Available after Launch | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); | ||||
|         // Fill Search input | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Performance Display Layout'); | ||||
|         //Search Result Appears and is clicked | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('a:has-text("Performance Display Layout")').first().click() | ||||
|         ]); | ||||
|  | ||||
|         //Time to Example Imagery Frame loads within Display Layout | ||||
|         await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible'}); | ||||
|         //Time to Example Imagery object loads | ||||
|         await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible'}); | ||||
|  | ||||
|         const client = await page.context().newCDPSession(page); | ||||
|         await client.send('HeapProfiler.enable'); | ||||
|         await client.send('HeapProfiler.startSampling'); | ||||
|         // await client.send('HeapProfiler.collectGarbage'); | ||||
|         await client.send('Performance.enable'); | ||||
|  | ||||
|         let performanceMetricsBefore = await client.send('Performance.getMetrics'); | ||||
|         console.log(performanceMetricsBefore.metrics); | ||||
|  | ||||
|         //await client.send('Performance.disable'); | ||||
|  | ||||
|         //Open Large view | ||||
|         await page.locator('button:has-text("Large View")').click(); | ||||
|         await client.send('HeapProfiler.takeHeapSnapshot'); | ||||
|  | ||||
|         //Time to Imagery Rendered in Large Frame | ||||
|         await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible'}); | ||||
|  | ||||
|         //Time to Example Imagery object loads | ||||
|         await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible'}); | ||||
|  | ||||
|         // Click Close Icon | ||||
|         await page.locator('.c-click-icon').click(); | ||||
|  | ||||
|         //Time to Example Imagery Frame loads within Display Layout | ||||
|         await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible'}); | ||||
|         //Time to Example Imagery object loads | ||||
|         await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible'}); | ||||
|  | ||||
|         await client.send('HeapProfiler.collectGarbage'); | ||||
|         //await client.send('Performance.enable'); | ||||
|  | ||||
|         let performanceMetricsAfter = await client.send('Performance.getMetrics'); | ||||
|         console.log(performanceMetricsAfter.metrics); | ||||
|  | ||||
|         //await client.send('Performance.disable'); | ||||
|  | ||||
|     }); | ||||
| }); | ||||
| @@ -1,158 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* | ||||
| This test suite is dedicated to performance tests to ensure that testability of performance | ||||
| is not broken upstream on Open MCT. Any assumptions made downstream will be tested here. | ||||
|  | ||||
| TODO: | ||||
|  - Update resolution of performance config | ||||
|  - Add Performance Observer on init to push all performance marks | ||||
|  - Move client CDP connection to before or to a fixture | ||||
|  | ||||
| */ | ||||
|  | ||||
| const { test, expect } = require('@playwright/test'); | ||||
|  | ||||
| const notebookFilePath = 'e2e/test-data/PerformanceNotebook.json'; | ||||
|  | ||||
| test.describe('Performance tests', () => { | ||||
|     test.beforeEach(async ({ page, browser }, testInfo) => { | ||||
|         // Go to baseURL | ||||
|         await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|         // Click a:has-text("My Items") | ||||
|         await page.locator('a:has-text("My Items")').click({ | ||||
|             button: 'right' | ||||
|         }); | ||||
|  | ||||
|         // Click text=Import from JSON | ||||
|         await page.locator('text=Import from JSON').click(); | ||||
|  | ||||
|         // Upload Performance Display Layout.json | ||||
|         await page.setInputFiles('#fileElem', notebookFilePath); | ||||
|  | ||||
|         // TODO Fix this | ||||
|         await page.locator('text=OK >> nth=1').click(); | ||||
|  | ||||
|         await expect(page.locator('a:has-text("Performance Notebook")')).toBeVisible(); | ||||
|  | ||||
|         //Create a Chrome Performance Timeline trace to store as a test artifact | ||||
|         console.log("\n==== Devtools: startTracing ====\n"); | ||||
|         await browser.startTracing(page, { | ||||
|             path: `${testInfo.outputPath()}-trace.json`, | ||||
|             screenshots: true | ||||
|         }); | ||||
|     }); | ||||
|     test.afterEach(async ({ page, browser}) => { | ||||
|         console.log("\n==== Devtools: stopTracing ====\n"); | ||||
|         await browser.stopTracing(); | ||||
|  | ||||
|         /* Measurement Section | ||||
|         / The following section includes a block of performance measurements. | ||||
|         */ | ||||
|         const startTime = await page.evaluate(() => window.performance.timing.navigationStart); | ||||
|         console.log('window.performance.timing.navigationStart', startTime); | ||||
|  | ||||
|         //Get All Performance Marks | ||||
|         const getAllMarksJson = await page.evaluate(() => JSON.stringify(window.performance.getEntriesByType("mark"))); | ||||
|         const getAllMarks = JSON.parse(getAllMarksJson); | ||||
|         console.log('window.performance.getEntriesByType("mark")', getAllMarks); | ||||
|  | ||||
|         //Get All Performance Measures | ||||
|         const getAllMeasuresJson = await page.evaluate(() => JSON.stringify(window.performance.getEntriesByType("measure"))); | ||||
|         const getAllMeasures = JSON.parse(getAllMeasuresJson); | ||||
|         console.log('window.performance.getEntriesByType("measure")', getAllMeasures); | ||||
|  | ||||
|     }); | ||||
|     /* The following test will navigate to a previously created Performance Display Layout and measure the | ||||
|     /  following metrics: | ||||
|     /  - ElementResourceTiming | ||||
|     /  - Interaction Timing | ||||
|     */ | ||||
|     test('Notebook Search, Add Entry, Update Entry are performant', async ({ page, browser }) => { | ||||
|         const client = await page.context().newCDPSession(page); | ||||
|         // Tell the DevTools session to record performance metrics | ||||
|         // https://chromedevtools.github.io/devtools-protocol/tot/Performance/#method-getMetrics | ||||
|         await client.send('Performance.enable'); | ||||
|         // Go to baseURL | ||||
|         await page.goto('/'); | ||||
|  | ||||
|         // To to Search Available after Launch | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); | ||||
|         await page.evaluate(() => window.performance.mark("search-available")); | ||||
|         // Fill Search input | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Performance Notebook'); | ||||
|         await page.evaluate(() => window.performance.mark("search-entered")); | ||||
|         //Search Result Appears and is clicked | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('a:has-text("Performance Notebook")').first().click(), | ||||
|             page.evaluate(() => window.performance.mark("click-search-result")) | ||||
|         ]); | ||||
|  | ||||
|         await page.waitForSelector('.c-tree__item c-tree-and-search__loading loading', {state: 'hidden'}); | ||||
|         await page.evaluate(() => window.performance.mark("search-spinner-gone")); | ||||
|  | ||||
|         await page.waitForSelector('.l-browse-bar__object-name', { state: 'visible'}); | ||||
|         await page.evaluate(() => window.performance.mark("object-title-appears")); | ||||
|  | ||||
|         await page.waitForSelector('.c-notebook__entry >> nth=0', { state: 'visible'}); | ||||
|         await page.evaluate(() => window.performance.mark("notebook-entry-appears")); | ||||
|  | ||||
|         // Click Add new Notebook Entry | ||||
|         await page.locator('.c-notebook__drag-area').click(); | ||||
|         await page.evaluate(() => window.performance.mark("new-notebook-entry-created")); | ||||
|  | ||||
|         // Enter Notebook Entry text | ||||
|         await page.locator('div.c-ne__text').last().fill('New Entry'); | ||||
|         await page.keyboard.press('Enter'); | ||||
|         await page.evaluate(() => window.performance.mark("new-notebook-entry-filled")); | ||||
|  | ||||
|         //Individual Notebook Entry Search | ||||
|         await page.evaluate(() => window.performance.mark("notebook-search-start")); | ||||
|         await page.locator('.c-notebook__search >> input').fill('Existing Entry'); | ||||
|         await page.evaluate(() => window.performance.mark("notebook-search-filled")); | ||||
|         await page.waitForSelector('text=Search Results (3)', { state: 'visible'}); | ||||
|         await page.evaluate(() => window.performance.mark("notebook-search-processed")); | ||||
|         await page.waitForSelector('.c-notebook__entry >> nth=2', { state: 'visible'}); | ||||
|         await page.evaluate(() => window.performance.mark("notebook-search-processed")); | ||||
|  | ||||
|         //Clear Search | ||||
|         await page.locator('.c-search.c-notebook__search .c-search__clear-input').click(); | ||||
|         await page.evaluate(() => window.performance.mark("notebook-search-processed")); | ||||
|  | ||||
|         // Hover on Last | ||||
|         await page.evaluate(() => window.performance.mark("new-notebook-entry-delete")); | ||||
|         await page.locator('div.c-ne__time-and-content').last().hover(); | ||||
|         await page.locator('button[title="Delete this entry"]').last().click(); | ||||
|         await page.locator('button:has-text("Ok")').click(); | ||||
|         await page.waitForSelector('.c-notebook__entry >> nth=3', { state: 'detached'}); | ||||
|         await page.evaluate(() => window.performance.mark("new-notebook-entry-deleted")); | ||||
|  | ||||
|         //await client.send('HeapProfiler.enable'); | ||||
|         await client.send('HeapProfiler.collectGarbage'); | ||||
|  | ||||
|         let performanceMetrics = await client.send('Performance.getMetrics'); | ||||
|         console.log(performanceMetrics.metrics); | ||||
|     }); | ||||
| }); | ||||
| @@ -24,11 +24,12 @@ | ||||
| This test suite is dedicated to tests which verify the basic operations surrounding conditionSets. | ||||
| */ | ||||
|  | ||||
| const { test } = require('../../fixtures.js'); | ||||
| const { expect } = require('@playwright/test'); | ||||
| const { test, expect } = require('@playwright/test'); | ||||
| const path = require('path'); | ||||
|  | ||||
| test.describe('Persistence operations @addInit', () => { | ||||
| // https://github.com/nasa/openmct/issues/4323#issuecomment-1067282651 | ||||
|  | ||||
| test.describe('Persistence operations', () => { | ||||
|     // add non persistable root item | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         // eslint-disable-next-line no-undef | ||||
| @@ -36,10 +37,6 @@ test.describe('Persistence operations @addInit', () => { | ||||
|     }); | ||||
|  | ||||
|     test('Persistability should be respected in the create form location field', async ({ page }) => { | ||||
|         test.info().annotations.push({ | ||||
|             type: 'issue', | ||||
|             description: 'https://github.com/nasa/openmct/issues/4323' | ||||
|         }); | ||||
|         // Go to baseURL | ||||
|         await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|   | ||||
| @@ -24,10 +24,7 @@ | ||||
| This test suite is dedicated to tests which verify the basic operations surrounding exportAsJSON. | ||||
| */ | ||||
|  | ||||
| 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'); | ||||
| const { test, 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,10 +24,7 @@ | ||||
| This test suite is dedicated to tests which verify the basic operations surrounding importAsJSON. | ||||
| */ | ||||
|  | ||||
| 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'); | ||||
| const { test, expect } = require('@playwright/test'); | ||||
|  | ||||
| test.describe('ExportAsJSON', () => { | ||||
|     test.fixme('Verify that domain object can be importAsJSON from Tree', async ({ page }) => { | ||||
|   | ||||
| @@ -24,8 +24,7 @@ | ||||
| This test suite is dedicated to tests which verify the basic operations surrounding Clock. | ||||
| */ | ||||
|  | ||||
| const { test } = require('../../../fixtures.js'); | ||||
| const { expect } = require('@playwright/test'); | ||||
| const { test, expect } = require('@playwright/test'); | ||||
|  | ||||
| test.describe('Clock Generator', () => { | ||||
|  | ||||
| @@ -46,22 +45,22 @@ test.describe('Clock Generator', () => { | ||||
|         // Click .icon-arrow-down | ||||
|         await page.locator('.icon-arrow-down').click(); | ||||
|         //verify if the autocomplete dropdown is visible | ||||
|         await expect(page.locator(".c-input--autocomplete__options")).toBeVisible(); | ||||
|         await expect(page.locator(".optionPreSelected")).toBeVisible(); | ||||
|         // Click .icon-arrow-down | ||||
|         await page.locator('.icon-arrow-down').click(); | ||||
|  | ||||
|         // Verify clicking on the autocomplete arrow collapses the dropdown | ||||
|         await expect(page.locator(".c-input--autocomplete__options")).not.toBeVisible(); | ||||
|         await expect(page.locator(".optionPreSelected")).not.toBeVisible(); | ||||
|  | ||||
|         // Click timezone input to open dropdown | ||||
|         await page.locator('.c-input--autocomplete__input').click(); | ||||
|         await page.locator('.autocompleteInput').click(); | ||||
|         //verify if the autocomplete dropdown is visible | ||||
|         await expect(page.locator(".c-input--autocomplete__options")).toBeVisible(); | ||||
|         await expect(page.locator(".optionPreSelected")).toBeVisible(); | ||||
|  | ||||
|         // Verify clicking outside the autocomplete dropdown collapses it | ||||
|         await page.locator('text=Timezone').click(); | ||||
|         // Verify clicking on the autocomplete arrow collapses the dropdown | ||||
|         await expect(page.locator(".c-input--autocomplete__options")).not.toBeVisible(); | ||||
|         await expect(page.locator(".optionPreSelected")).not.toBeVisible(); | ||||
|  | ||||
|     }); | ||||
| }); | ||||
|   | ||||
| @@ -26,53 +26,50 @@ 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 } = require('../../../fixtures.js'); | ||||
| const { expect } = require('@playwright/test'); | ||||
| const { test, expect } = require('@playwright/test'); | ||||
|  | ||||
| let conditionSetUrl; | ||||
| let getConditionSetIdentifierFromUrl; | ||||
|  | ||||
| test('Create new Condition Set object and store @localStorage', async ({ page, context }) => { | ||||
|     //Go to baseURL | ||||
|     await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|     //Click the Create button | ||||
|     await page.click('button:has-text("Create")'); | ||||
|  | ||||
|     // Click text=Condition Set | ||||
|     await page.click('text=Condition Set'); | ||||
|  | ||||
|     // Click text=OK | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.click('text=OK') | ||||
|     ]); | ||||
|  | ||||
|     await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set'); | ||||
|     //Save localStorage for future test execution | ||||
|     await context.storageState({ path: './e2e/tests/recycled_storage.json' }); | ||||
|  | ||||
|     //Set object identifier from url | ||||
|     conditionSetUrl = await page.url(); | ||||
|     console.log('conditionSetUrl ' + conditionSetUrl); | ||||
|  | ||||
|     getConditionSetIdentifierFromUrl = await conditionSetUrl.split('/').pop().split('?')[0]; | ||||
|     console.log('getConditionSetIdentifierFromUrl ' + getConditionSetIdentifierFromUrl); | ||||
|  | ||||
| }); | ||||
|  | ||||
| test.describe.serial('Condition Set CRUD Operations on @localStorage', () => { | ||||
|     test.beforeAll(async ({ browser}) => { | ||||
|         const context = await browser.newContext(); | ||||
|         const page = await context.newPage(); | ||||
|         //Go to baseURL | ||||
|         await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|         //Click the Create button | ||||
|         await page.click('button:has-text("Create")'); | ||||
|  | ||||
|         // Click text=Condition Set | ||||
|         await page.locator('li:has-text("Condition Set")').click(); | ||||
|  | ||||
|         // Click text=OK | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.click('text=OK') | ||||
|         ]); | ||||
|  | ||||
|         //Save localStorage for future test execution | ||||
|         await context.storageState({ path: './e2e/test-data/recycled_local_storage.json' }); | ||||
|  | ||||
|         //Set object identifier from url | ||||
|         conditionSetUrl = await page.url(); | ||||
|         console.log('conditionSetUrl ' + conditionSetUrl); | ||||
|  | ||||
|         getConditionSetIdentifierFromUrl = await conditionSetUrl.split('/').pop().split('?')[0]; | ||||
|         console.debug('getConditionSetIdentifierFromUrl ' + getConditionSetIdentifierFromUrl); | ||||
|         await page.close(); | ||||
|     }); | ||||
|     test.afterAll(async ({ browser }) => { | ||||
|         await browser.close(); | ||||
|     }); | ||||
|     //Load localStorage for subsequent tests | ||||
|     test.use({ storageState: './e2e/test-data/recycled_local_storage.json' }); | ||||
|     test.use({ storageState: './e2e/tests/recycled_storage.json' }); | ||||
|  | ||||
|     //Begin suite of tests again localStorage | ||||
|     test('Condition set object properties persist in main view and inspector @localStorage', async ({ page }) => { | ||||
|     test('Condition set object properties persist in main view and inspector', async ({ page }) => { | ||||
|         //Navigate to baseURL with injected localStorage | ||||
|         await page.goto(conditionSetUrl, { waitUntil: 'networkidle' }); | ||||
|  | ||||
|         //Assertions on loaded Condition Set in main view. This is a stateful transition step after page.goto() | ||||
|         //Assertions on loaded Condition Set in main view | ||||
|         await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set'); | ||||
|  | ||||
|         //Assertions on loaded Condition Set in Inspector | ||||
| @@ -93,7 +90,7 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => { | ||||
|     test('condition set object can be modified on @localStorage', async ({ page }) => { | ||||
|         await page.goto(conditionSetUrl, { waitUntil: 'networkidle' }); | ||||
|  | ||||
|         //Assertions on loaded Condition Set in main view. This is a stateful transition step after page.goto() | ||||
|         //Assertions on loaded Condition Set in main view | ||||
|         await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set'); | ||||
|  | ||||
|         //Update the Condition Set properties | ||||
| @@ -123,7 +120,7 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => { | ||||
|         // Verify Condition Set Object is renamed in Tree | ||||
|         await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); | ||||
|         // Verify Search Tree reflects renamed Name property | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Renamed'); | ||||
|         await page.locator('input[type="search"]').fill('Renamed'); | ||||
|         await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); | ||||
|  | ||||
|         //Reload Page | ||||
| @@ -147,33 +144,35 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => { | ||||
|         // Verify Condition Set Object is renamed in Tree | ||||
|         await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); | ||||
|         // Verify Search Tree reflects renamed Name property | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Renamed'); | ||||
|         await page.locator('input[type="search"]').fill('Renamed'); | ||||
|         await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); | ||||
|     }); | ||||
|     test('condition set object can be deleted by Search Tree Actions menu on @localStorage', async ({ page }) => { | ||||
|         //Navigate to baseURL | ||||
|         await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|         //Assertions on loaded Condition Set in main view. This is a stateful transition step after page.goto() | ||||
|         await expect(page.locator('a:has-text("Unnamed Condition Set Condition Set") >> nth=0')).toBeVisible(); | ||||
|  | ||||
|         const numberOfConditionSetsToStart = await page.locator('a:has-text("Unnamed Condition Set Condition Set")').count(); | ||||
|         //Expect Unnamed Condition Set to be visible in Main View | ||||
|         await expect(page.locator('a:has-text("Unnamed Condition Set Condition Set")')).toBeVisible(); | ||||
|  | ||||
|         // Search for Unnamed Condition Set | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Unnamed Condition Set'); | ||||
|         // Click Search Result | ||||
|         await page.locator('[aria-label="OpenMCT Search"] >> text=Unnamed Condition Set').first().click(); | ||||
|         // Click hamburger button | ||||
|         await page.locator('[title="More options"]').click(); | ||||
|  | ||||
|         // Click text=Remove | ||||
|         await page.locator('input[type="search"]').fill('Unnamed Condition Set'); | ||||
|         // Right Click to Open Actions Menu | ||||
|         await page.locator('a:has-text("Unnamed Condition Set")').click({ | ||||
|             button: 'right' | ||||
|         }); | ||||
|         // Click Remove Action | ||||
|         await page.locator('text=Remove').click(); | ||||
|  | ||||
|         await page.locator('text=OK').click(); | ||||
|  | ||||
|         //Expect Unnamed Condition Set to be removed in Main View | ||||
|         const numberOfConditionSetsAtEnd = await page.locator('a:has-text("Unnamed Condition Set Condition Set")').count(); | ||||
|         await expect(page.locator('a:has-text("Unnamed Condition Set Condition Set")')).not.toBeVisible(); | ||||
|  | ||||
|         expect(numberOfConditionSetsAtEnd).toEqual(numberOfConditionSetsToStart - 1); | ||||
|         await page.locator('.c-search__clear-input').click(); | ||||
|         // Search for Unnamed Condition Set | ||||
|         await page.locator('input[type="search"]').fill('Unnamed Condition Set'); | ||||
|         // Expect Unnamed Condition Set to be removed | ||||
|         await expect(page.locator('a:has-text("Unnamed Condition Set")')).not.toBeVisible(); | ||||
|  | ||||
|         //Feature? | ||||
|         //Domain Object is still available by direct URL after delete | ||||
|   | ||||
| @@ -24,17 +24,13 @@ | ||||
| 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 } = require('../../../fixtures.js'); | ||||
| const { expect } = require('@playwright/test'); | ||||
| const { test, expect } = require('@playwright/test'); | ||||
|  | ||||
| const backgroundImageSelector = '.c-imagery__main-image__background-image'; | ||||
|  | ||||
| //The following block of tests verifies the basic functionality of example imagery and serves as a template for Imagery objects embedded in other objects. | ||||
| test.describe('Example Imagery Object', () => { | ||||
| test.describe('Example Imagery', () => { | ||||
|  | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         page.on('console', msg => console.log(msg.text())) | ||||
|         //Go to baseURL | ||||
|         await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
| @@ -46,35 +42,30 @@ test.describe('Example Imagery Object', () => { | ||||
|  | ||||
|         // 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') | ||||
|             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') | ||||
|         ]); | ||||
|         // Close Banner | ||||
|         await page.locator('.c-message-banner__close-button').click(); | ||||
|  | ||||
|         //Wait until Save Banner is gone | ||||
|         await page.waitForSelector('.c-message-banner__message', { state: 'detached'}); | ||||
|         await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery'); | ||||
|         await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|     }); | ||||
|  | ||||
|     const backgroundImageSelector = '.c-imagery__main-image__background-image'; | ||||
|     test('Can use Mouse Wheel to zoom in and out of latest image', async ({ page }) => { | ||||
|         const bgImageLocator = await page.locator(backgroundImageSelector); | ||||
|         const deltaYStep = 100; //equivalent to 1x zoom | ||||
|         await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|         await bgImageLocator.hover(); | ||||
|         const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|         // zoom in | ||||
|         await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|         await bgImageLocator.hover(); | ||||
|         await page.mouse.wheel(0, deltaYStep * 2); | ||||
|         // wait for zoom animation to finish | ||||
|         await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|         await bgImageLocator.hover(); | ||||
|         const imageMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|         // zoom out | ||||
|         await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|         await bgImageLocator.hover(); | ||||
|         await page.mouse.wheel(0, -deltaYStep); | ||||
|         // wait for zoom animation to finish | ||||
|         await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|         await bgImageLocator.hover(); | ||||
|         const imageMouseZoomedOut = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|  | ||||
|         expect(imageMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height); | ||||
| @@ -86,14 +77,13 @@ test.describe('Example Imagery Object', () => { | ||||
|  | ||||
|     test('Can use alt+drag to move around image once zoomed in', async ({ page }) => { | ||||
|         const deltaYStep = 100; //equivalent to 1x zoom | ||||
|         const panHotkey = process.platform === 'linux' ? ['Control', 'Alt'] : ['Alt']; | ||||
|  | ||||
|         await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|  | ||||
|         const bgImageLocator = await page.locator(backgroundImageSelector); | ||||
|         await bgImageLocator.hover(); | ||||
|         // zoom in | ||||
|         await page.mouse.wheel(0, deltaYStep * 2); | ||||
|         await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|         const zoomedBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|         await bgImageLocator.hover(); | ||||
|         const zoomedBoundingBox = await bgImageLocator.boundingBox(); | ||||
|         const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2; | ||||
|         const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2; | ||||
|         // move to the right | ||||
| @@ -101,101 +91,88 @@ test.describe('Example Imagery Object', () => { | ||||
|         // 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 Promise.all(panHotkey.map(x => page.keyboard.down(x))); | ||||
|         await page.keyboard.down('Alt'); | ||||
|         await page.mouse.down(); | ||||
|         await page.mouse.move(imageCenterX - 200, imageCenterY, 10); | ||||
|         await page.mouse.up(); | ||||
|         await Promise.all(panHotkey.map(x => page.keyboard.up(x))); | ||||
|         const afterRightPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|         await page.keyboard.up('Alt'); | ||||
|         const afterRightPanBoundingBox = await bgImageLocator.boundingBox(); | ||||
|         expect(zoomedBoundingBox.x).toBeGreaterThan(afterRightPanBoundingBox.x); | ||||
|  | ||||
|         // pan left | ||||
|         await Promise.all(panHotkey.map(x => page.keyboard.down(x))); | ||||
|         await page.keyboard.down('Alt'); | ||||
|         await page.mouse.down(); | ||||
|         await page.mouse.move(imageCenterX, imageCenterY, 10); | ||||
|         await page.mouse.up(); | ||||
|         await Promise.all(panHotkey.map(x => page.keyboard.up(x))); | ||||
|         const afterLeftPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|         await page.keyboard.up('Alt'); | ||||
|         const afterLeftPanBoundingBox = await bgImageLocator.boundingBox(); | ||||
|         expect(afterRightPanBoundingBox.x).toBeLessThan(afterLeftPanBoundingBox.x); | ||||
|  | ||||
|         // pan up | ||||
|         await page.mouse.move(imageCenterX, imageCenterY); | ||||
|         await Promise.all(panHotkey.map(x => page.keyboard.down(x))); | ||||
|         await page.keyboard.down('Alt'); | ||||
|         await page.mouse.down(); | ||||
|         await page.mouse.move(imageCenterX, imageCenterY + 200, 10); | ||||
|         await page.mouse.up(); | ||||
|         await Promise.all(panHotkey.map(x => page.keyboard.up(x))); | ||||
|         const afterUpPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|         await page.keyboard.up('Alt'); | ||||
|         const afterUpPanBoundingBox = await bgImageLocator.boundingBox(); | ||||
|         expect(afterUpPanBoundingBox.y).toBeGreaterThan(afterLeftPanBoundingBox.y); | ||||
|  | ||||
|         // pan down | ||||
|         await Promise.all(panHotkey.map(x => page.keyboard.down(x))); | ||||
|         await page.keyboard.down('Alt'); | ||||
|         await page.mouse.down(); | ||||
|         await page.mouse.move(imageCenterX, imageCenterY - 200, 10); | ||||
|         await page.mouse.up(); | ||||
|         await Promise.all(panHotkey.map(x => page.keyboard.up(x))); | ||||
|         const afterDownPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|         await page.keyboard.up('Alt'); | ||||
|         const afterDownPanBoundingBox = await bgImageLocator.boundingBox(); | ||||
|         expect(afterDownPanBoundingBox.y).toBeLessThan(afterUpPanBoundingBox.y); | ||||
|  | ||||
|     }); | ||||
|  | ||||
|     test('Can use + - buttons to zoom on the image', async ({ page }) => { | ||||
|         await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|         const zoomInBtn = page.locator('.t-btn-zoom-in').nth(0); | ||||
|         const zoomOutBtn = page.locator('.t-btn-zoom-out').nth(0); | ||||
|         const initialBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|         const bgImageLocator = await page.locator(backgroundImageSelector); | ||||
|         await bgImageLocator.hover(); | ||||
|         const zoomInBtn = await page.locator('.t-btn-zoom-in'); | ||||
|         const zoomOutBtn = await page.locator('.t-btn-zoom-out'); | ||||
|         const initialBoundingBox = await bgImageLocator.boundingBox(); | ||||
|  | ||||
|         await zoomInBtn.click(); | ||||
|         await zoomInBtn.click(); | ||||
|         // wait for zoom animation to finish | ||||
|         await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|         const zoomedInBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|         await bgImageLocator.hover(); | ||||
|         const zoomedInBoundingBox = await bgImageLocator.boundingBox(); | ||||
|         expect(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height); | ||||
|         expect(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width); | ||||
|  | ||||
|         await zoomOutBtn.click(); | ||||
|         // wait for zoom animation to finish | ||||
|         await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|         const zoomedOutBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|         await bgImageLocator.hover(); | ||||
|         const zoomedOutBoundingBox = await bgImageLocator.boundingBox(); | ||||
|         expect(zoomedOutBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height); | ||||
|         expect(zoomedOutBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width); | ||||
|  | ||||
|     }); | ||||
|  | ||||
|     test('Can use the reset button to reset the image', async ({ page }) => { | ||||
|         // wait for zoom animation to finish | ||||
|         await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|  | ||||
|         const zoomInBtn = page.locator('.t-btn-zoom-in').nth(0); | ||||
|         const zoomResetBtn = page.locator('.t-btn-zoom-reset').nth(0); | ||||
|         const initialBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|         const bgImageLocator = await page.locator(backgroundImageSelector); | ||||
|         await bgImageLocator.hover(); | ||||
|         const zoomInBtn = await page.locator('.t-btn-zoom-in'); | ||||
|         const zoomResetBtn = await page.locator('.t-btn-zoom-reset'); | ||||
|         const initialBoundingBox = await bgImageLocator.boundingBox(); | ||||
|  | ||||
|         await zoomInBtn.click(); | ||||
|         // wait for zoom animation to finish | ||||
|         await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|         await zoomInBtn.click(); | ||||
|         // wait for zoom animation to finish | ||||
|         await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|  | ||||
|         const zoomedInBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|         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 page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|         await bgImageLocator.hover(); | ||||
|  | ||||
|         const resetBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|         const resetBoundingBox = await bgImageLocator.boundingBox(); | ||||
|         expect.soft(resetBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height); | ||||
|         expect.soft(resetBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width); | ||||
|  | ||||
| @@ -203,549 +180,38 @@ test.describe('Example Imagery Object', () => { | ||||
|         expect(resetBoundingBox.width).toEqual(initialBoundingBox.width); | ||||
|     }); | ||||
|  | ||||
|     test('Using the zoom features does not pause telemetry', async ({ page }) => { | ||||
|         const pausePlayButton = page.locator('.c-button.pause-play'); | ||||
|         // wait for zoom animation to finish | ||||
|         await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|  | ||||
|         // open the time conductor drop down | ||||
|         await page.locator('button:has-text("Fixed Timespan")').click(); | ||||
|         // Click local clock | ||||
|         await page.locator('[data-testid="conductor-modeOption-realtime"]').click(); | ||||
|  | ||||
|         await expect.soft(pausePlayButton).not.toHaveClass(/is-paused/); | ||||
|         const zoomInBtn = page.locator('.t-btn-zoom-in').nth(0); | ||||
|         await zoomInBtn.click(); | ||||
|         // wait for zoom animation to finish | ||||
|         await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|  | ||||
|         return expect(pausePlayButton).not.toHaveClass(/is-paused/); | ||||
|     }); | ||||
|  | ||||
|     //test('Can use Mouse Wheel to zoom in and out of previous image'); | ||||
|     //test('Can zoom into the latest image and the real-time/fixed-time imagery will pause'); | ||||
|     //test.skip('Can zoom into a previous image from thumbstrip in real-time or fixed-time'); | ||||
|     //test.skip('Clicking on the left arrow should pause the imagery and go to previous image'); | ||||
|     //test.skip('If the imagery view is in pause mode, it should not be updated when new images come in'); | ||||
|     //test.skip('If the imagery view is not in pause mode, it should be updated when new images come in'); | ||||
| }); | ||||
|  | ||||
| // The following test case will cover these scenarios | ||||
| // ('Can use Mouse Wheel to zoom in and out of previous image'); | ||||
| // ('Can use alt+drag to move around image once zoomed in'); | ||||
| // ('Clicking on the left arrow should pause the imagery and go to previous image'); | ||||
| // ('If the imagery view is in pause mode, it should not be updated when new images come in'); | ||||
| // ('If the imagery view is not in pause mode, it should be updated when new images come in'); | ||||
| test('Example Imagery in Display layout', async ({ page, browserName }) => { | ||||
|     test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox'); | ||||
|     test.info().annotations.push({ | ||||
|         type: 'issue', | ||||
|         description: 'https://github.com/nasa/openmct/issues/5265' | ||||
|     }); | ||||
|  | ||||
|     // Go to baseURL | ||||
|     await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|     // Click the Create button | ||||
|     await page.click('button:has-text("Create")'); | ||||
|  | ||||
|     // Click text=Example Imagery | ||||
|     await page.click('text=Example Imagery'); | ||||
|  | ||||
|     // Clear and set Image load delay to minimum value | ||||
|     // 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'); | ||||
|     await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|  | ||||
|     // Click previous image button | ||||
|     const previousImageButton = page.locator('.c-nav--prev'); | ||||
|     await previousImageButton.click(); | ||||
|  | ||||
|     // Verify previous image | ||||
|     const selectedImage = page.locator('.selected'); | ||||
|     await expect(selectedImage).toBeVisible(); | ||||
|  | ||||
|     // Zoom in | ||||
|     const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|     await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|     const deltaYStep = 100; // equivalent to 1x zoom | ||||
|     await page.mouse.wheel(0, deltaYStep * 2); | ||||
|     const zoomedBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|     const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2; | ||||
|     const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2; | ||||
|  | ||||
|     // Wait for zoom animation to finish | ||||
|     await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|     const imageMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|     expect(imageMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height); | ||||
|     expect(imageMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width); | ||||
|  | ||||
|     // Center the mouse pointer | ||||
|     await page.mouse.move(imageCenterX, imageCenterY); | ||||
|  | ||||
|     // Pan Imagery Hints | ||||
|     const expectedAltText = process.platform === 'linux' ? 'Ctrl+Alt drag to pan' : 'Alt drag to pan'; | ||||
|     const imageryHintsText = await page.locator('.c-imagery__hints').innerText(); | ||||
|     expect(expectedAltText).toEqual(imageryHintsText); | ||||
|  | ||||
|     // Click next image button | ||||
|     const nextImageButton = page.locator('.c-nav--next'); | ||||
|     await nextImageButton.click(); | ||||
|  | ||||
|     // Click time conductor mode button | ||||
|     await page.locator('.c-mode-button').click(); | ||||
|  | ||||
|     // Select local clock mode | ||||
|     await page.locator('[data-testid=conductor-modeOption-realtime]').click(); | ||||
|  | ||||
|     // Zoom in on next image | ||||
|     await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|     await page.mouse.wheel(0, deltaYStep * 2); | ||||
|  | ||||
|     // Wait for zoom animation to finish | ||||
|     await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|     const imageNextMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|     expect(imageNextMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height); | ||||
|     expect(imageNextMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width); | ||||
|  | ||||
|     // Click previous image button | ||||
|     await previousImageButton.click(); | ||||
|  | ||||
|     // Verify previous image | ||||
|     await expect(selectedImage).toBeVisible(); | ||||
|  | ||||
|     const imageCount = await page.locator('.c-imagery__thumb').count(); | ||||
|     await expect.poll(async () => { | ||||
|         const newImageCount = await page.locator('.c-imagery__thumb').count(); | ||||
|  | ||||
|         return newImageCount; | ||||
|     }, { | ||||
|         message: "verify that old images are discarded", | ||||
|         timeout: 6 * 1000 | ||||
|     }).toBe(imageCount); | ||||
|  | ||||
|     // Verify selected image is still displayed | ||||
|     await expect(selectedImage).toBeVisible(); | ||||
| }); | ||||
|  | ||||
| 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 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'); | ||||
| }); | ||||
|  | ||||
| test.describe('Example Imagery in Flexible layout', () => { | ||||
|     test('Example Imagery in Flexible layout', async ({ page, browserName }) => { | ||||
|         test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox'); | ||||
|         test.info().annotations.push({ | ||||
|             type: 'issue', | ||||
|             description: 'https://github.com/nasa/openmct/issues/5326' | ||||
|         }); | ||||
|  | ||||
|         // Go to baseURL | ||||
|         await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|         // Click the Create button | ||||
|         await page.click('button:has-text("Create")'); | ||||
|  | ||||
|         // Click text=Example Imagery | ||||
|         await page.click('text=Example Imagery'); | ||||
|  | ||||
|         // Clear and set Image load delay (milliseconds) | ||||
|         await page.click('input[type="number"]', {clickCount: 3}); | ||||
|         await page.type('input[type="number"]', "20"); | ||||
|  | ||||
|         // Click text=OK | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation({waitUntil: 'networkidle'}), | ||||
|             page.click('text=OK'), | ||||
|             //Wait for Save Banner to appear | ||||
|             page.waitForSelector('.c-message-banner__message') | ||||
|         ]); | ||||
|         // Wait until Save Banner is gone | ||||
|         await page.waitForSelector('.c-message-banner__message', { state: 'detached'}); | ||||
|         await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery'); | ||||
|         await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|  | ||||
|         // Click the Create button | ||||
|         await page.click('button:has-text("Create")'); | ||||
|  | ||||
|         // Click text=Flexible Layout | ||||
|         await page.click('text=Flexible Layout'); | ||||
|  | ||||
|         // Assert Flexable layout | ||||
|         await expect(page.locator('.js-form-title')).toHaveText('Create a New Flexible Layout'); | ||||
|  | ||||
|         await page.locator('form[name="mctForm"] >> text=My Items').click(); | ||||
|  | ||||
|         // Click My Items | ||||
|         await Promise.all([ | ||||
|             page.locator('text=OK').click(), | ||||
|             page.waitForNavigation({waitUntil: 'networkidle'}) | ||||
|         ]); | ||||
|  | ||||
|         // Click My Items | ||||
|         await page.locator('.c-disclosure-triangle').click(); | ||||
|  | ||||
|         // Right click example imagery | ||||
|         await page.click(('text=Unnamed Example Imagery'), { button: 'right' }); | ||||
|  | ||||
|         // Click move | ||||
|         await page.locator('.icon-move').click(); | ||||
|  | ||||
|         // Click triangle to open sub menu | ||||
|         await page.locator('.c-form__section .c-disclosure-triangle').click(); | ||||
|  | ||||
|         // Click Flexable Layout | ||||
|         await page.click('.c-overlay__outer >> text=Unnamed Flexible Layout'); | ||||
|  | ||||
|         // Click text=OK | ||||
|         await page.locator('text=OK').click(); | ||||
|  | ||||
|         // Save template | ||||
|         await saveTemplate(page); | ||||
|  | ||||
|         // Zoom in | ||||
|         await mouseZoomIn(page); | ||||
|  | ||||
|         // Center the mouse pointer | ||||
|         const zoomedBoundingBox = await await page.locator(backgroundImageSelector).boundingBox(); | ||||
|         const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2; | ||||
|         const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2; | ||||
|         await page.mouse.move(imageCenterX, imageCenterY); | ||||
|  | ||||
|         // Pan zoom | ||||
|         await panZoomAndAssertImageProperties(page); | ||||
|  | ||||
|         // Click previous image button | ||||
|         const previousImageButton = page.locator('.c-nav--prev'); | ||||
|         await previousImageButton.click(); | ||||
|  | ||||
|         // Verify previous image | ||||
|         const selectedImage = page.locator('.selected'); | ||||
|         await expect(selectedImage).toBeVisible(); | ||||
|  | ||||
|         // Click time conductor mode button | ||||
|         await page.locator('.c-mode-button').click(); | ||||
|  | ||||
|         // Select local clock mode | ||||
|         await page.locator('[data-testid=conductor-modeOption-realtime]').click(); | ||||
|  | ||||
|         // Zoom in on next image | ||||
|         await mouseZoomIn(page); | ||||
|  | ||||
|         // Click previous image button | ||||
|         await previousImageButton.click(); | ||||
|  | ||||
|         // Verify previous image | ||||
|         await expect(selectedImage).toBeVisible(); | ||||
|  | ||||
|         const imageCount = await page.locator('.c-imagery__thumb').count(); | ||||
|         await expect.poll(async () => { | ||||
|             const newImageCount = await page.locator('.c-imagery__thumb').count(); | ||||
|  | ||||
|             return newImageCount; | ||||
|         }, { | ||||
|             message: "verify that old images are discarded", | ||||
|             timeout: 6 * 1000 | ||||
|         }).toBe(imageCount); | ||||
|  | ||||
|         // Verify selected image is still displayed | ||||
|         await expect(selectedImage).toBeVisible(); | ||||
|  | ||||
|         // Unpause imagery | ||||
|         await page.locator('.pause-play').click(); | ||||
|  | ||||
|         //Get background-image url from background-image css prop | ||||
|         await assertBackgroundImageUrlFromBackgroundCss(page); | ||||
|  | ||||
|         // Open the image filter menu | ||||
|         await page.locator('[role=toolbar] button[title="Brightness and contrast"]').click(); | ||||
|  | ||||
|         // Drag the brightness and contrast sliders around and assert filter values | ||||
|         await dragBrightnessSliderAndAssertFilterValues(page); | ||||
|         await dragContrastSliderAndAssertFilterValues(page); | ||||
|     }); | ||||
|     test.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.describe('Example Imagery in Tabs view', () => { | ||||
|     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'); | ||||
|     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'); | ||||
| }); | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function saveTemplate(page) { | ||||
|     await page.locator('.c-button--menu.c-button--major.icon-save').click(); | ||||
|     await page.locator('text=Save and Finish Editing').click(); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Drag the brightness slider to max, min, and midpoint and assert the filter values | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function dragBrightnessSliderAndAssertFilterValues(page) { | ||||
|     const brightnessSlider = 'div.c-image-controls__slider-wrapper.icon-brightness > input'; | ||||
|     const brightnessBoundingBox = await page.locator(brightnessSlider).boundingBox(); | ||||
|     const brightnessMidX = brightnessBoundingBox.x + brightnessBoundingBox.width / 2; | ||||
|     const brightnessMidY = brightnessBoundingBox.y + brightnessBoundingBox.height / 2; | ||||
|  | ||||
|     await page.locator(brightnessSlider).hover({trial: true}); | ||||
|     await page.mouse.down(); | ||||
|     await page.mouse.move(brightnessBoundingBox.x + brightnessBoundingBox.width, brightnessMidY); | ||||
|     await assertBackgroundImageBrightness(page, '500'); | ||||
|     await page.mouse.move(brightnessBoundingBox.x, brightnessMidY); | ||||
|     await assertBackgroundImageBrightness(page, '0'); | ||||
|     await page.mouse.move(brightnessMidX, brightnessMidY); | ||||
|     await assertBackgroundImageBrightness(page, '250'); | ||||
|     await page.mouse.up(); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Drag the contrast slider to max, min, and midpoint and assert the filter values | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function dragContrastSliderAndAssertFilterValues(page) { | ||||
|     const contrastSlider = 'div.c-image-controls__slider-wrapper.icon-contrast > input'; | ||||
|     const contrastBoundingBox = await page.locator(contrastSlider).boundingBox(); | ||||
|     const contrastMidX = contrastBoundingBox.x + contrastBoundingBox.width / 2; | ||||
|     const contrastMidY = contrastBoundingBox.y + contrastBoundingBox.height / 2; | ||||
|  | ||||
|     await page.locator(contrastSlider).hover({trial: true}); | ||||
|     await page.mouse.down(); | ||||
|     await page.mouse.move(contrastBoundingBox.x + contrastBoundingBox.width, contrastMidY); | ||||
|     await assertBackgroundImageContrast(page, '500'); | ||||
|     await page.mouse.move(contrastBoundingBox.x, contrastMidY); | ||||
|     await assertBackgroundImageContrast(page, '0'); | ||||
|     await page.mouse.move(contrastMidX, contrastMidY); | ||||
|     await assertBackgroundImageContrast(page, '250'); | ||||
|     await page.mouse.up(); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Gets the filter:brightness value of the current background-image and | ||||
|  * asserts against an expected value | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  * @param {String} expected The expected brightness value | ||||
|  */ | ||||
| async function assertBackgroundImageBrightness(page, expected) { | ||||
|     const backgroundImage = page.locator('.c-imagery__main-image__background-image'); | ||||
|  | ||||
|     // Get the brightness filter value (i.e: filter: brightness(500%) => "500") | ||||
|     const actual = await backgroundImage.evaluate((el) => { | ||||
|         return el.style.filter.match(/brightness\((\d{1,3})%\)/)[1]; | ||||
|     }); | ||||
|     expect(actual).toBe(expected); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Gets the filter:contrast value of the current background-image and | ||||
|  * asserts against an expected value | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  * @param {String} expected The expected contrast value | ||||
|  */ | ||||
| async function assertBackgroundImageContrast(page, expected) { | ||||
|     const backgroundImage = page.locator('.c-imagery__main-image__background-image'); | ||||
|  | ||||
|     // Get the contrast filter value (i.e: filter: contrast(500%) => "500") | ||||
|     const actual = await backgroundImage.evaluate((el) => { | ||||
|         return el.style.filter.match(/contrast\((\d{1,3})%\)/)[1]; | ||||
|     }); | ||||
|     expect(actual).toBe(expected); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function assertBackgroundImageUrlFromBackgroundCss(page) { | ||||
|     const backgroundImage = page.locator('.c-imagery__main-image__background-image'); | ||||
|     let backgroundImageUrl = await backgroundImage.evaluate((el) => { | ||||
|         return window.getComputedStyle(el).getPropertyValue('background-image').match(/url\(([^)]+)\)/)[1]; | ||||
|     }); | ||||
|     let backgroundImageUrl1 = backgroundImageUrl.slice(1, -1); //forgive me, padre | ||||
|     console.log('backgroundImageUrl1 ' + backgroundImageUrl1); | ||||
|  | ||||
|     let backgroundImageUrl2; | ||||
|     await expect.poll(async () => { | ||||
|         // Verify next image has updated | ||||
|         let backgroundImageUrlNext = await backgroundImage.evaluate((el) => { | ||||
|             return window.getComputedStyle(el).getPropertyValue('background-image').match(/url\(([^)]+)\)/)[1]; | ||||
|         }); | ||||
|         backgroundImageUrl2 = backgroundImageUrlNext.slice(1, -1); //forgive me, padre | ||||
|  | ||||
|         return backgroundImageUrl2; | ||||
|     }, { | ||||
|         message: "verify next image has updated", | ||||
|         timeout: 6 * 1000 | ||||
|     }).not.toBe(backgroundImageUrl1); | ||||
|     console.log('backgroundImageUrl2 ' + backgroundImageUrl2); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function panZoomAndAssertImageProperties(page) { | ||||
|     const panHotkey = process.platform === 'linux' ? ['Control', 'Alt'] : ['Alt']; | ||||
|     const expectedAltText = process.platform === 'linux' ? 'Ctrl+Alt drag to pan' : 'Alt drag to pan'; | ||||
|     const imageryHintsText = await page.locator('.c-imagery__hints').innerText(); | ||||
|     expect(expectedAltText).toEqual(imageryHintsText); | ||||
|     const zoomedBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|     const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2; | ||||
|     const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2; | ||||
|  | ||||
|     // Pan right | ||||
|     await Promise.all(panHotkey.map(x => page.keyboard.down(x))); | ||||
|     await page.mouse.down(); | ||||
|     await page.mouse.move(imageCenterX - 200, imageCenterY, 10); | ||||
|     await page.mouse.up(); | ||||
|     await Promise.all(panHotkey.map(x => page.keyboard.up(x))); | ||||
|     const afterRightPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|     expect(zoomedBoundingBox.x).toBeGreaterThan(afterRightPanBoundingBox.x); | ||||
|  | ||||
|     // Pan left | ||||
|     await Promise.all(panHotkey.map(x => page.keyboard.down(x))); | ||||
|     await page.mouse.down(); | ||||
|     await page.mouse.move(imageCenterX, imageCenterY, 10); | ||||
|     await page.mouse.up(); | ||||
|     await Promise.all(panHotkey.map(x => page.keyboard.up(x))); | ||||
|     const afterLeftPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|     expect(afterRightPanBoundingBox.x).toBeLessThan(afterLeftPanBoundingBox.x); | ||||
|  | ||||
|     // Pan up | ||||
|     await page.mouse.move(imageCenterX, imageCenterY); | ||||
|     await Promise.all(panHotkey.map(x => page.keyboard.down(x))); | ||||
|     await page.mouse.down(); | ||||
|     await page.mouse.move(imageCenterX, imageCenterY + 200, 10); | ||||
|     await page.mouse.up(); | ||||
|     await Promise.all(panHotkey.map(x => page.keyboard.up(x))); | ||||
|     const afterUpPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|     expect(afterUpPanBoundingBox.y).toBeGreaterThanOrEqual(afterLeftPanBoundingBox.y); | ||||
|  | ||||
|     // Pan down | ||||
|     await Promise.all(panHotkey.map(x => page.keyboard.down(x))); | ||||
|     await page.mouse.down(); | ||||
|     await page.mouse.move(imageCenterX, imageCenterY - 200, 10); | ||||
|     await page.mouse.up(); | ||||
|     await Promise.all(panHotkey.map(x => page.keyboard.up(x))); | ||||
|     const afterDownPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|     expect(afterDownPanBoundingBox.y).toBeLessThanOrEqual(afterUpPanBoundingBox.y); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
| */ | ||||
| async function mouseZoomIn(page) { | ||||
|     // Zoom in | ||||
|     const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|     await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|     const deltaYStep = 100; // equivalent to 1x zoom | ||||
|     await page.mouse.wheel(0, deltaYStep * 2); | ||||
|     const zoomedBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|     const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2; | ||||
|     const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2; | ||||
|  | ||||
|     // center the mouse pointer | ||||
|     await page.mouse.move(imageCenterX, imageCenterY); | ||||
|  | ||||
|     // Wait for zoom animation to finish | ||||
|     await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|     const imageMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|     expect(imageMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height); | ||||
|     expect(imageMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width); | ||||
| } | ||||
|   | ||||
| @@ -1,30 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| // this will be called from the test suite with | ||||
| // await page.addInitScript({ path: path.join(__dirname, 'addInitRestrictedNotebook.js') }); | ||||
| // it will install the RestrictedNotebook since it is not installed by default | ||||
|  | ||||
| document.addEventListener('DOMContentLoaded', () => { | ||||
|     const openmct = window.openmct; | ||||
|     openmct.install(openmct.plugins.RestrictedNotebook('CUSTOM_NAME')); | ||||
| }); | ||||
| @@ -1,198 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* | ||||
| This test suite is dedicated to tests which verify the basic operations surrounding Notebooks. | ||||
| */ | ||||
|  | ||||
| const { test } = require('../../../fixtures'); | ||||
|  | ||||
| test.describe('Notebook CRUD Operations', () => { | ||||
|     test.fixme('Can create a Notebook Object', async ({ page }) => { | ||||
|         //Create domain object | ||||
|         //Newly created notebook should have one Section and one page, 'Unnamed Section'/'Unnamed Page' | ||||
|     }); | ||||
|     test.fixme('Can update a Notebook Object', async ({ page }) => {}); | ||||
|     test.fixme('Can view a perviously created Notebook Object', async ({ page }) => {}); | ||||
|     test.fixme('Can Delete a Notebook Object', async ({ page }) => { | ||||
|         // Other than non-persistible objects | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| test.describe('Default Notebook', () => { | ||||
|     // General Default Notebook statements | ||||
|     // ## Useful commands: | ||||
|     // 1.  - To check default notebook: | ||||
|     //     `JSON.parse(localStorage.getItem('notebook-storage'));` | ||||
|     // 1.  - Clear default notebook: | ||||
|     //     `localStorage.setItem('notebook-storage', null);` | ||||
|     test.fixme('A newly created Notebook is automatically set as the default notebook if no other notebooks exist', async ({ page }) => { | ||||
|         //Create new notebook | ||||
|         //Verify Default Notebook Characteristics | ||||
|     }); | ||||
|     test.fixme('A newly created Notebook is automatically set as the default notebook if at least one other notebook exists', async ({ page }) => { | ||||
|         //Create new notebook A | ||||
|         //Create second notebook B | ||||
|         //Verify Non-Default Notebook A Characteristics | ||||
|         //Verify Default Notebook B Characteristics | ||||
|     }); | ||||
|     test.fixme('If a default notebook is deleted, the second most recent notebook becomes the default', async ({ page }) => { | ||||
|         //Create new notebook A | ||||
|         //Create second notebook B | ||||
|         //Delete Notebook B | ||||
|         //Verify Default Notebook A Characteristics | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| test.describe('Notebook section tests', () => { | ||||
|     //The following test cases are associated with Notebook Sections | ||||
|     test.fixme('New sections are automatically named Unnamed Section with Unnamed Page', async ({ page }) => { | ||||
|         //Create new notebook A | ||||
|         //Add section | ||||
|         //Verify new section and new page details | ||||
|     }); | ||||
|     test.fixme('Section selection operations and associated behavior', async ({ page }) => { | ||||
|         //Create new notebook A | ||||
|         //Add Sections until 6 total with no default section/page | ||||
|         //Select 3rd section | ||||
|         //Delete 4th section | ||||
|         //3rd section is still selected | ||||
|         //Delete 3rd section | ||||
|         //1st section is selected | ||||
|         //Set 3rd section as default | ||||
|         //Delete 2nd section | ||||
|         //3rd section is still default | ||||
|         //Delete 3rd section | ||||
|         //1st is selected and there is no default notebook | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| test.describe('Notebook page tests', () => { | ||||
|     //The following test cases are associated with Notebook Pages | ||||
|     test.fixme('Page selection operations and associated behavior', async ({ page }) => { | ||||
|         //Create new notebook A | ||||
|         //Delete existing Page | ||||
|         //New 'Unnamed Page' automatically created | ||||
|         //Create 6 total Pages without a default page | ||||
|         //Select 3rd | ||||
|         //Delete 3rd | ||||
|         //First is now selected | ||||
|         //Set 3rd as default | ||||
|         //Select 2nd page | ||||
|         //Delete 2nd page | ||||
|         //3rd (default) is now selected | ||||
|         //Set 3rd as default page | ||||
|         //Select 3rd (default) page | ||||
|         //Delete 3rd page | ||||
|         //First is now selected and there is no default notebook | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| test.describe('Notebook search tests', () => { | ||||
|     test.fixme('Can search for a single result', async ({ page }) => {}); | ||||
|     test.fixme('Can search for many results', async ({ page }) => {}); | ||||
|     test.fixme('Can search for new and recently modified entries', async ({ page }) => {}); | ||||
|     test.fixme('Can search for section text', async ({ page }) => {}); | ||||
|     test.fixme('Can search for page text', async ({ page }) => {}); | ||||
|     test.fixme('Can search for entry text', async ({ page }) => {}); | ||||
| }); | ||||
|  | ||||
| test.describe('Notebook entry tests', () => { | ||||
|     test.fixme('When a new entry is created, it should be focused', async ({ page }) => {}); | ||||
|     test.fixme('When a telemetry object is dropped into a notebook, a new entry is created and it should be focused', async ({ page }) => { | ||||
|         // Drag and drop any telmetry object on 'drop object' | ||||
|         // new entry gets created with telemtry object | ||||
|     }); | ||||
|     test.fixme('When a telemetry object is dropped into a notebooks existing entry, it should be focused', async ({ page }) => { | ||||
|         // Drag and drop any telemetry object onto existing entry | ||||
|         // Entry updated with object and snapshot | ||||
|     }); | ||||
|     test.fixme('new entries persist through navigation events without save', async ({ page }) => {}); | ||||
|     test.fixme('previous and new entries can be deleted', async ({ page }) => {}); | ||||
| }); | ||||
|  | ||||
| test.describe('Snapshot Menu tests', () => { | ||||
|     test.fixme('When no default notebook is selected, Snapshot Menu dropdown should only have a single option', async ({ page }) => { | ||||
|         // There should be no default notebook | ||||
|         // Clear default notebook if exists using `localStorage.setItem('notebook-storage', null);` | ||||
|         // refresh page | ||||
|         // Click on 'Notebook Snaphot Menu' | ||||
|         // 'save to Notebook Snapshots' should be only option there | ||||
|     }); | ||||
|     test.fixme('When default notebook is updated selected, Snapshot Menu dropdown should list it as the newest option', async ({ page }) => { | ||||
|         // Create 2a notebooks | ||||
|         // Set Notebook A as Default | ||||
|         // Open Snapshot Menu and note that Notebook A is listed | ||||
|         // Close Snapshot Menu | ||||
|         // Set Default Notebook to Notebook B | ||||
|         // Open Snapshot Notebook and note that Notebook B is listed | ||||
|         // Select Default Notebook Option and verify that Snapshot is added to Notebook B | ||||
|     }); | ||||
|     test.fixme('Can add Snapshots via Snapshot Menu and details are correct', async ({ page }) => { | ||||
|         //Note this should be a visual test, too | ||||
|         // Create Telemetry object | ||||
|         // Create A notebook with many pages and sections. | ||||
|         // Set page and section defaults to be between first and last of many. i.e. 3 of 5 | ||||
|         // Navigate to Telemetry object | ||||
|         // Select Default Notebook Option and verify that Snapshot is added to Notebook A | ||||
|         // Verify Snapshot Details appear correctly | ||||
|     }); | ||||
|     test.fixme('Snapshots adjust time conductor', async ({ page }) => { | ||||
|         // Create Telemetry object | ||||
|         // Set Telemetry object's timeconductor to Fixed time with Start and Endtimes are recorded | ||||
|         // Embed Telemetry object into notebook | ||||
|         // Set Time Conductor to Local clock | ||||
|         // Click into embedded telemetry object and verify object appears with same fixed time from record | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| test.describe('Snapshot Container tests', () => { | ||||
|     test.fixme('5 Snapshots can be added to a container', async ({ page }) => {}); | ||||
|     test.fixme('5 Snapshots can be added to a container and Deleted with Delete All action', async ({ page }) => {}); | ||||
|     test.fixme('A snapshot can be Deleted from Container', async ({ page }) => {}); | ||||
|     test.fixme('A snapshot can be Previewed from Container', async ({ page }) => {}); | ||||
|     test.fixme('A snapshot Container can be open and closed', async ({ page }) => {}); | ||||
|     test.fixme('Can add object to Snapshot container and pull into notebook and create a new entry', async ({ page }) => { | ||||
|         //Create Notebook | ||||
|         //Create Telemetry Object | ||||
|         //From Telemetry Object, use 'save to Notebook Snapshots' | ||||
|         //Snapshots indicator should blink, click on it to view snapshots | ||||
|         //Navigate to Notebook | ||||
|         //Drag and Drop onto droppable area for new entry | ||||
|         //New Entry created with given snapshot added | ||||
|         //Snapshot removed from container? | ||||
|     }); | ||||
|     test.fixme('Can add object to Snapshot container and pull into notebook and existing entry', async ({ page }) => { | ||||
|         //Create Notebook | ||||
|         //Create Telemetry Object | ||||
|         //From Telemetry Object, use 'save to Notebook Snapshots' | ||||
|         //Snapshots indicator should blink, click on it to view snapshots | ||||
|         //Navigate to Notebook | ||||
|         //Drag and Drop into exiting entry | ||||
|         //Existing Entry updated with given snapshot | ||||
|         //Snapshot removed from container? | ||||
|     }); | ||||
|     test.fixme('Verify Embedded options for PNG, JPG, and Annotate work correctly', async ({ page }) => { | ||||
|         //Add snapshot to container | ||||
|         //Verify PNG, JPG, and Annotate buttons work correctly | ||||
|     }); | ||||
| }); | ||||
| @@ -1,262 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| const { test } = require('../../../fixtures'); | ||||
| const { expect } = require('@playwright/test'); | ||||
| const path = require('path'); | ||||
|  | ||||
| const TEST_TEXT = 'Testing text for entries.'; | ||||
| const TEST_TEXT_NAME = 'Test Page'; | ||||
| const CUSTOM_NAME = 'CUSTOM_NAME'; | ||||
| const NOTEBOOK_DROP_AREA = '.c-notebook__drag-area'; | ||||
|  | ||||
| test.describe('Restricted Notebook', () => { | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         await startAndAddRestrictedNotebookObject(page); | ||||
|     }); | ||||
|  | ||||
|     test('Can be renamed @addInit', async ({ page }) => { | ||||
|         await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText(`Unnamed ${CUSTOM_NAME}`); | ||||
|     }); | ||||
|  | ||||
|     test('Can be deleted if there are no locked pages @addInit', async ({ page }) => { | ||||
|         await openContextMenuRestrictedNotebook(page); | ||||
|  | ||||
|         const menuOptions = page.locator('.c-menu ul'); | ||||
|         await expect.soft(menuOptions).toContainText('Remove'); | ||||
|  | ||||
|         const restrictedNotebookTreeObject = page.locator(`a:has-text("Unnamed ${CUSTOM_NAME}")`); | ||||
|  | ||||
|         // notbook tree object exists | ||||
|         expect.soft(await restrictedNotebookTreeObject.count()).toEqual(1); | ||||
|  | ||||
|         // Click Remove Text | ||||
|         await page.locator('text=Remove').click(); | ||||
|  | ||||
|         //Wait until Save Banner is gone | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('text=OK').click(), | ||||
|             page.waitForSelector('.c-message-banner__message') | ||||
|         ]); | ||||
|         await page.locator('.c-message-banner__close-button').click(); | ||||
|  | ||||
|         // has been deleted | ||||
|         expect.soft(await restrictedNotebookTreeObject.count()).toEqual(0); | ||||
|     }); | ||||
|  | ||||
|     test('Can be locked if at least one page has one entry @addInit', async ({ page }) => { | ||||
|  | ||||
|         await enterTextEntry(page); | ||||
|  | ||||
|         const commitButton = page.locator('button:has-text("Commit Entries")'); | ||||
|         expect.soft(await commitButton.count()).toEqual(1); | ||||
|     }); | ||||
|  | ||||
| }); | ||||
|  | ||||
| test.describe('Restricted Notebook with at least one entry and with the page locked @addInit', () => { | ||||
|  | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         await startAndAddRestrictedNotebookObject(page); | ||||
|         await enterTextEntry(page); | ||||
|         await lockPage(page); | ||||
|  | ||||
|         // open sidebar | ||||
|         await page.locator('button.c-notebook__toggle-nav-button').click(); | ||||
|     }); | ||||
|  | ||||
|     test('Locked page should now be in a locked state @addInit', async ({ page }) => { | ||||
|         // main lock message on page | ||||
|         const lockMessage = page.locator('text=This page has been committed and cannot be modified or removed'); | ||||
|         expect.soft(await lockMessage.count()).toEqual(1); | ||||
|  | ||||
|         // lock icon on page in sidebar | ||||
|         const pageLockIcon = page.locator('ul.c-notebook__pages li div.icon-lock'); | ||||
|         expect.soft(await pageLockIcon.count()).toEqual(1); | ||||
|  | ||||
|         // no way to remove a restricted notebook with a locked page | ||||
|         await openContextMenuRestrictedNotebook(page); | ||||
|  | ||||
|         const menuOptions = page.locator('.c-menu ul'); | ||||
|  | ||||
|         await expect.soft(menuOptions).not.toContainText('Remove'); | ||||
|  | ||||
|     }); | ||||
|  | ||||
|     test('Can still: add page, rename, add entry, delete unlocked pages @addInit', async ({ page }) => { | ||||
|         // Click text=Page Add >> button | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('text=Page Add >> button').click() | ||||
|         ]); | ||||
|         // Click text=Unnamed Page >> nth=1 | ||||
|         await page.locator('text=Unnamed Page').nth(1).click(); | ||||
|         // Press a with modifiers | ||||
|         await page.locator('text=Unnamed Page').nth(1).fill(TEST_TEXT_NAME); | ||||
|  | ||||
|         // expect to be able to rename unlocked pages | ||||
|         const newPageElement = page.locator(`text=${TEST_TEXT_NAME}`); | ||||
|         const newPageCount = await newPageElement.count(); | ||||
|         await newPageElement.press('Enter'); // exit contenteditable state | ||||
|         expect.soft(newPageCount).toEqual(1); | ||||
|  | ||||
|         // enter test text | ||||
|         await enterTextEntry(page); | ||||
|  | ||||
|         // expect new page to be lockable | ||||
|         const commitButton = page.locator('BUTTON:HAS-TEXT("COMMIT ENTRIES")'); | ||||
|         expect.soft(await commitButton.count()).toEqual(1); | ||||
|  | ||||
|         // Click text=Unnamed PageTest Page >> button | ||||
|         await page.locator('text=Unnamed PageTest Page >> button').click(); | ||||
|         // Click text=Delete Page | ||||
|         await page.locator('text=Delete Page').click(); | ||||
|         // Click text=Ok | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('text=Ok').click() | ||||
|         ]); | ||||
|  | ||||
|         // deleted page, should no longer exist | ||||
|         const deletedPageElement = page.locator(`text=${TEST_TEXT_NAME}`); | ||||
|         expect.soft(await deletedPageElement.count()).toEqual(0); | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| test.describe('Restricted Notebook with a page locked and with an embed @addInit', () => { | ||||
|  | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         await startAndAddRestrictedNotebookObject(page); | ||||
|         await dragAndDropEmbed(page); | ||||
|     }); | ||||
|  | ||||
|     test('Allows embeds to be deleted if page unlocked @addInit', async ({ page }) => { | ||||
|         // Click .c-ne__embed__name .c-popup-menu-button | ||||
|         await page.locator('.c-ne__embed__name .c-popup-menu-button').click(); // embed popup menu | ||||
|  | ||||
|         const embedMenu = page.locator('body >> .c-menu'); | ||||
|         await expect.soft(embedMenu).toContainText('Remove This Embed'); | ||||
|     }); | ||||
|  | ||||
|     test('Disallows embeds to be deleted if page locked @addInit', async ({ page }) => { | ||||
|         await lockPage(page); | ||||
|         // Click .c-ne__embed__name .c-popup-menu-button | ||||
|         await page.locator('.c-ne__embed__name .c-popup-menu-button').click(); // embed popup menu | ||||
|  | ||||
|         const embedMenu = page.locator('body >> .c-menu'); | ||||
|         await expect.soft(embedMenu).not.toContainText('Remove This Embed'); | ||||
|     }); | ||||
|  | ||||
| }); | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function startAndAddRestrictedNotebookObject(page) { | ||||
|     // eslint-disable-next-line no-undef | ||||
|     await page.addInitScript({ path: path.join(__dirname, 'addInitRestrictedNotebook.js') }); | ||||
|     //Go to baseURL | ||||
|     await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|     //Click the Create button | ||||
|     await page.click('button:has-text("Create")'); | ||||
|     // Click text=CUSTOME_NAME | ||||
|     await page.click(`text=${CUSTOM_NAME}`); // secondarily tests renamability also | ||||
|     // Click text=OK | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation({waitUntil: 'networkidle'}), | ||||
|         page.click('text=OK') | ||||
|     ]); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function enterTextEntry(page) { | ||||
|     // Click .c-notebook__drag-area | ||||
|     await page.locator(NOTEBOOK_DROP_AREA).click(); | ||||
|  | ||||
|     // enter text | ||||
|     await page.locator('div.c-ne__text').click(); | ||||
|     await page.locator('div.c-ne__text').fill(TEST_TEXT); | ||||
|     await page.locator('div.c-ne__text').press('Enter'); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function dragAndDropEmbed(page) { | ||||
|     // Click button:has-text("Create") | ||||
|     await page.locator('button:has-text("Create")').click(); | ||||
|     // Click li:has-text("Sine Wave Generator") | ||||
|     await page.locator('li:has-text("Sine Wave Generator")').click(); | ||||
|     // Click form[name="mctForm"] >> text=My Items | ||||
|     await page.locator('form[name="mctForm"] >> text=My Items').click(); | ||||
|     // Click text=OK | ||||
|     await page.locator('text=OK').click(); | ||||
|     // Click text=Open MCT My Items >> span >> nth=3 | ||||
|     await page.locator('text=Open MCT My Items >> span').nth(3).click(); | ||||
|     // Click text=Unnamed CUSTOM_NAME | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('text=Unnamed CUSTOM_NAME').click() | ||||
|     ]); | ||||
|  | ||||
|     await page.dragAndDrop('text=UNNAMED SINE WAVE GENERATOR', NOTEBOOK_DROP_AREA); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function lockPage(page) { | ||||
|     const commitButton = page.locator('button:has-text("Commit Entries")'); | ||||
|     await commitButton.click(); | ||||
|  | ||||
|     //Wait until Lock Banner is visible | ||||
|     await Promise.all([ | ||||
|         page.locator('text=Lock Page').click(), | ||||
|         page.waitForSelector('.c-message-banner__message') | ||||
|     ]); | ||||
|     // Close Lock Banner | ||||
|     await page.locator('.c-message-banner__close-button').click(); | ||||
|  | ||||
|     //artifically wait to avoid mutation delay TODO: https://github.com/nasa/openmct/issues/5409 | ||||
|     // eslint-disable-next-line playwright/no-wait-for-timeout | ||||
|     await page.waitForTimeout(1 * 1000); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function openContextMenuRestrictedNotebook(page) { | ||||
|     // Click text=Open MCT My Items (This expands the My Items folder to show it's chilren in the tree) | ||||
|     await page.locator('text=Open MCT My Items >> span').nth(3).click(); | ||||
|  | ||||
|     //artifically wait to avoid mutation delay TODO: https://github.com/nasa/openmct/issues/5409 | ||||
|     // eslint-disable-next-line playwright/no-wait-for-timeout | ||||
|     await page.waitForTimeout(1 * 1000); | ||||
|  | ||||
|     // Click a:has-text("Unnamed CUSTOM_NAME") | ||||
|     await page.locator(`a:has-text("Unnamed ${CUSTOM_NAME}")`).click({ | ||||
|         button: 'right' | ||||
|     }); | ||||
| } | ||||
| @@ -1,205 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* | ||||
| This test suite is dedicated to tests which verify form functionality. | ||||
| */ | ||||
|  | ||||
| const { expect } = require('@playwright/test'); | ||||
| const { test } = require('../../../fixtures'); | ||||
|  | ||||
| /** | ||||
|   * Creates a notebook object and adds an entry. | ||||
|   * @param {import('@playwright/test').Page} - page to load | ||||
|   * @param {number} [iterations = 1] - the number of entries to create | ||||
|   */ | ||||
| async function createNotebookAndEntry(page, iterations = 1) { | ||||
|     //Go to baseURL | ||||
|     await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|     // Click button:has-text("Create") | ||||
|     await page.locator('button:has-text("Create")').click(); | ||||
|     await page.locator('[title="Create and save timestamped notes with embedded object snapshots."]').click(); | ||||
|     // Click button:has-text("OK") | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('[name="mctForm"] >> text=My Items').click(), | ||||
|         page.locator('button:has-text("OK")').click() | ||||
|     ]); | ||||
|  | ||||
|     for (let iteration = 0; iteration < iterations; iteration++) { | ||||
|         // Click text=To start a new entry, click here or drag and drop any object | ||||
|         await page.locator('text=To start a new entry, click here or drag and drop any object').click(); | ||||
|         const entryLocator = `[aria-label="Notebook Entry Input"] >> nth = ${iteration}`; | ||||
|         await page.locator(entryLocator).click(); | ||||
|         await page.locator(entryLocator).fill(`Entry ${iteration}`); | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| /** | ||||
|   * Creates a notebook object, adds an entry, and adds a tag. | ||||
|   * @param {import('@playwright/test').Page} page | ||||
|   * @param {number} [iterations = 1] - the number of entries (and tags) to create | ||||
|   */ | ||||
| async function createNotebookEntryAndTags(page, iterations = 1) { | ||||
|     await createNotebookAndEntry(page, iterations); | ||||
|  | ||||
|     for (let iteration = 0; iteration < iterations; iteration++) { | ||||
|         // Click text=To start a new entry, click here or drag and drop any object | ||||
|         await page.locator(`button:has-text("Add Tag") >> nth = ${iteration}`).click(); | ||||
|  | ||||
|         // Click [placeholder="Type to select tag"] | ||||
|         await page.locator('[placeholder="Type to select tag"]').click(); | ||||
|         // Click text=Driving | ||||
|         await page.locator('[aria-label="Autocomplete Options"] >> text=Driving').click(); | ||||
|  | ||||
|         // Click button:has-text("Add Tag") | ||||
|         await page.locator(`button:has-text("Add Tag") >> nth = ${iteration}`).click(); | ||||
|         // Click [placeholder="Type to select tag"] | ||||
|         await page.locator('[placeholder="Type to select tag"]').click(); | ||||
|         // Click text=Science | ||||
|         await page.locator('[aria-label="Autocomplete Options"] >> text=Science').click(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| test.describe('Tagging in Notebooks', () => { | ||||
|     test('Can load tags', async ({ page }) => { | ||||
|         await createNotebookAndEntry(page); | ||||
|         // Click text=To start a new entry, click here or drag and drop any object | ||||
|         await page.locator('button:has-text("Add Tag")').click(); | ||||
|  | ||||
|         // Click [placeholder="Type to select tag"] | ||||
|         await page.locator('[placeholder="Type to select tag"]').click(); | ||||
|  | ||||
|         await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Science"); | ||||
|         await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Drilling"); | ||||
|         await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Driving"); | ||||
|     }); | ||||
|     test('Can add tags', async ({ page }) => { | ||||
|         await createNotebookEntryAndTags(page); | ||||
|  | ||||
|         await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Science"); | ||||
|         await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Driving"); | ||||
|  | ||||
|         // Click button:has-text("Add Tag") | ||||
|         await page.locator('button:has-text("Add Tag")').click(); | ||||
|         // Click [placeholder="Type to select tag"] | ||||
|         await page.locator('[placeholder="Type to select tag"]').click(); | ||||
|  | ||||
|         await expect(page.locator('[aria-label="Autocomplete Options"]')).not.toContainText("Science"); | ||||
|         await expect(page.locator('[aria-label="Autocomplete Options"]')).not.toContainText("Driving"); | ||||
|         await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Drilling"); | ||||
|     }); | ||||
|     test('Can search for tags', async ({ page }) => { | ||||
|         await createNotebookEntryAndTags(page); | ||||
|         // Click [aria-label="OpenMCT Search"] input[type="search"] | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); | ||||
|         // Fill [aria-label="OpenMCT Search"] input[type="search"] | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc'); | ||||
|         await expect(page.locator('[aria-label="Search Result"]')).toContainText("Science"); | ||||
|         await expect(page.locator('[aria-label="Search Result"]')).toContainText("Driving"); | ||||
|  | ||||
|         // Click [aria-label="OpenMCT Search"] input[type="search"] | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); | ||||
|         // Fill [aria-label="OpenMCT Search"] input[type="search"] | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Sc'); | ||||
|         await expect(page.locator('[aria-label="Search Result"]')).toContainText("Science"); | ||||
|         await expect(page.locator('[aria-label="Search Result"]')).toContainText("Driving"); | ||||
|  | ||||
|         // Click [aria-label="OpenMCT Search"] input[type="search"] | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); | ||||
|         // Fill [aria-label="OpenMCT Search"] input[type="search"] | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Xq'); | ||||
|         await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible(); | ||||
|         await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible(); | ||||
|     }); | ||||
|  | ||||
|     test('Can delete tags', async ({ page }) => { | ||||
|         await createNotebookEntryAndTags(page); | ||||
|         await page.locator('[aria-label="Notebook Entries"]').click(); | ||||
|         // Delete Driving | ||||
|         await page.locator('text=Science Driving Add Tag >> button').nth(1).click(); | ||||
|  | ||||
|         await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Science"); | ||||
|         await expect(page.locator('[aria-label="Notebook Entry"]')).not.toContainText("Driving"); | ||||
|  | ||||
|         // Fill [aria-label="OpenMCT Search"] input[type="search"] | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc'); | ||||
|         await expect(page.locator('[aria-label="Search Result"]')).not.toContainText("Driving"); | ||||
|     }); | ||||
|     test('Tags persist across reload', async ({ page }) => { | ||||
|         //Go to baseURL | ||||
|         await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|         // Create a clock object we can navigate to | ||||
|         await page.click('button:has-text("Create")'); | ||||
|  | ||||
|         // Click Clock | ||||
|         await page.click('text=Clock'); | ||||
|         // Click button:has-text("OK") | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('[name="mctForm"] >> text=My Items').click(), | ||||
|             page.locator('button:has-text("OK")').click() | ||||
|         ]); | ||||
|  | ||||
|         await page.click('.c-disclosure-triangle'); | ||||
|  | ||||
|         const ITERATIONS = 4; | ||||
|         await createNotebookEntryAndTags(page, ITERATIONS); | ||||
|  | ||||
|         for (let iteration = 0; iteration < ITERATIONS; iteration++) { | ||||
|             const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`; | ||||
|             await expect(page.locator(entryLocator)).toContainText("Science"); | ||||
|             await expect(page.locator(entryLocator)).toContainText("Driving"); | ||||
|         } | ||||
|  | ||||
|         // Click Unnamed Clock | ||||
|         await page.click('text="Unnamed Clock"'); | ||||
|  | ||||
|         // Click Unnamed Notebook | ||||
|         await page.click('text="Unnamed Notebook"'); | ||||
|  | ||||
|         for (let iteration = 0; iteration < ITERATIONS; iteration++) { | ||||
|             const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`; | ||||
|             await expect(page.locator(entryLocator)).toContainText("Science"); | ||||
|             await expect(page.locator(entryLocator)).toContainText("Driving"); | ||||
|         } | ||||
|  | ||||
|         //Reload Page | ||||
|         await Promise.all([ | ||||
|             page.reload(), | ||||
|             page.waitForLoadState('networkidle') | ||||
|         ]); | ||||
|  | ||||
|         // Click Unnamed Notebook | ||||
|         await page.click('text="Unnamed Notebook"'); | ||||
|  | ||||
|         for (let iteration = 0; iteration < ITERATIONS; iteration++) { | ||||
|             const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`; | ||||
|             await expect(page.locator(entryLocator)).toContainText("Science"); | ||||
|             await expect(page.locator(entryLocator)).toContainText("Driving"); | ||||
|         } | ||||
|  | ||||
|     }); | ||||
| }); | ||||
| @@ -21,11 +21,23 @@ | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* | ||||
| Testsuite for plot autoscale. | ||||
| Test for plot autoscale. | ||||
| */ | ||||
|  | ||||
| const { test } = require('../../../fixtures.js'); | ||||
| const { expect } = require('@playwright/test'); | ||||
| const { test: _test, expect } = require('@playwright/test'); | ||||
|  | ||||
| // create a new `test` API that will not append platform details to snapshot | ||||
| // file names, only for the tests in this file, so that the same snapshots will | ||||
| // be used for all platforms. | ||||
| const test = _test.extend({ | ||||
|     _autoSnapshotSuffix: [ | ||||
|         async ({}, use, testInfo) => { | ||||
|             testInfo.snapshotSuffix = ''; | ||||
|             await use(); | ||||
|         }, | ||||
|         { auto: true } | ||||
|     ] | ||||
| }); | ||||
|  | ||||
| test.use({ | ||||
|     viewport: { | ||||
| @@ -35,10 +47,7 @@ test.use({ | ||||
| }); | ||||
|  | ||||
| test.describe('ExportAsJSON', () => { | ||||
|     test('User can set autoscale with a valid range @snapshot', async ({ page }) => { | ||||
|         //This is necessary due to the size of the test suite. | ||||
|         test.slow(); | ||||
|  | ||||
|     test('autoscale off causes no error from undefined user range', async ({ page }) => { | ||||
|         await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|         await setTimeRange(page); | ||||
| @@ -49,16 +58,24 @@ test.describe('ExportAsJSON', () => { | ||||
|  | ||||
|         await turnOffAutoscale(page); | ||||
|  | ||||
|         // Make sure that after turning off autoscale, the user selected range values start at the same values the plot had prior. | ||||
|         await testYTicks(page, ['-1.00', '-0.50', '0.00', '0.50', '1.00']); | ||||
|  | ||||
|         const canvas = page.locator('canvas').nth(1); | ||||
|  | ||||
|         await canvas.hover({trial: true}); | ||||
|         // Make sure that after turning off autoscale, the user selected range values start at the same values the plot had prior. | ||||
|         await Promise.all([ | ||||
|             testYTicks(page, ['-1.00', '-0.50', '0.00', '0.50', '1.00']), | ||||
|             new Promise(r => setTimeout(r, 100)) | ||||
|                 .then(() => canvas.screenshot()) | ||||
|                 .then(shot => expect(shot).toMatchSnapshot('autoscale-canvas-prepan.png', { maxDiffPixels: 40 })) | ||||
|         ]); | ||||
|  | ||||
|         expect(await canvas.screenshot()).toMatchSnapshot('autoscale-canvas-prepan'); | ||||
|         let errorCount = 0; | ||||
|  | ||||
|         function onError() { | ||||
|             errorCount++; | ||||
|         } | ||||
|  | ||||
|         page.on('pageerror', onError); | ||||
|  | ||||
|         //Alt Drag Start | ||||
|         await page.keyboard.down('Alt'); | ||||
|  | ||||
|         await canvas.dragTo(canvas, { | ||||
| @@ -72,15 +89,21 @@ test.describe('ExportAsJSON', () => { | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         //Alt Drag End | ||||
|         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 testYTicks(page, ['0.00', '0.50', '1.00', '1.50', '2.00']); | ||||
|  | ||||
|         await canvas.hover({trial: true}); | ||||
|  | ||||
|         expect(await canvas.screenshot()).toMatchSnapshot('autoscale-canvas-panned'); | ||||
|         await Promise.all([ | ||||
|             testYTicks(page, ['0.00', '0.50', '1.00', '1.50', '2.00']), | ||||
|             new Promise(r => setTimeout(r, 100)) | ||||
|                 .then(() => canvas.screenshot()) | ||||
|                 .then(shot => expect(shot).toMatchSnapshot('autoscale-canvas-panned.png', { maxDiffPixels: 40 })) | ||||
|         ]); | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| @@ -111,14 +134,9 @@ async function createSinewaveOverlayPlot(page) { | ||||
|     // add overlay plot with defaults | ||||
|     await page.locator('li:has-text("Overlay Plot")').click(); | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('text=OK').click(), | ||||
|         //Wait for Save Banner to appear1 | ||||
|         page.waitForSelector('.c-message-banner__message') | ||||
|         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() | ||||
|     ]); | ||||
|     //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(); | ||||
| @@ -130,19 +148,14 @@ 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(), | ||||
|         page.locator('text=OK').click(), | ||||
|         //Wait for Save Banner to appear1 | ||||
|         page.waitForSelector('.c-message-banner__message') | ||||
|         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() | ||||
|     ]); | ||||
|     //Wait until Save Banner is gone | ||||
|     await page.locator('.c-message-banner__close-button').click(); | ||||
|     await page.waitForSelector('.c-message-banner__message', { state: 'detached'}); | ||||
|  | ||||
|     // focus the overlay plot | ||||
|     await page.locator('text=Open MCT My Items >> span').nth(3).click(); | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.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=Unnamed Overlay Plot').first().click() | ||||
|     ]); | ||||
| } | ||||
| @@ -155,18 +168,11 @@ async function turnOffAutoscale(page) { | ||||
|     await page.locator('text=Unnamed Overlay Plot Snapshot >> button').nth(3).click(); | ||||
|  | ||||
|     // uncheck autoscale | ||||
|     await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"] >> nth=1').uncheck(); | ||||
|     await page.locator('text=Y Axis Scaling Auto scale Padding >> input[type="checkbox"]').uncheck(); | ||||
|  | ||||
|     // save | ||||
|     await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click(); | ||||
|     await Promise.all([ | ||||
|         page.locator('text=Save and Finish Editing').click(), | ||||
|         //Wait for Save Banner to appear | ||||
|         page.waitForSelector('.c-message-banner__message') | ||||
|     ]); | ||||
|     //Wait until Save Banner is gone | ||||
|     await page.locator('.c-message-banner__close-button').click(); | ||||
|     await page.waitForSelector('.c-message-banner__message', { state: 'detached'}); | ||||
|     await page.locator('text=Save and Finish Editing').click(); | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -174,7 +180,6 @@ 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: 16 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 15 KiB | 
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 24 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 18 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 18 KiB | 
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 30 KiB | 
| @@ -21,18 +21,13 @@ | ||||
|  *****************************************************************************/ | ||||
| 
 | ||||
| /* | ||||
| 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. | ||||
| Tests to verify log plot functionality. | ||||
| */ | ||||
| 
 | ||||
| const { test } = require('../../../fixtures.js'); | ||||
| const { expect } = require('@playwright/test'); | ||||
| const { test, expect } = require('@playwright/test'); | ||||
| 
 | ||||
| test.describe('Log plot tests', () => { | ||||
|     test('Log Plot ticks are functionally correct in regular and log mode and after refresh', async ({ page }) => { | ||||
|         //Test.slow decorator is currently broken. Needs to be fixed in https://github.com/nasa/openmct/issues/5374
 | ||||
|         test.slow(); | ||||
| 
 | ||||
|     test.only('Can create a log plot.', async ({ page }) => { | ||||
|         await makeOverlayPlot(page); | ||||
|         await testRegularTicks(page); | ||||
|         await enableEditMode(page); | ||||
| @@ -44,11 +39,17 @@ test.describe('Log plot tests', () => { | ||||
|         await testLogTicks(page); | ||||
|         await saveOverlayPlot(page); | ||||
|         await testLogTicks(page); | ||||
|         await testLogPlotPixels(page); | ||||
| 
 | ||||
|         // refresh page
 | ||||
|         await page.reload(); | ||||
| 
 | ||||
|         // test log ticks hold up after refresh
 | ||||
|         await testLogTicks(page); | ||||
|         await testLogPlotPixels(page); | ||||
|     }); | ||||
| 
 | ||||
|     // Leaving test as 'TODO' for now.
 | ||||
|     // NOTE: Not eligible for community contributions.
 | ||||
|     test.fixme('Verify that log mode option is reflected in import/export JSON', async ({ page }) => { | ||||
|     test.only('Verify that log mode option is reflected in import/export JSON', async ({ page }) => { | ||||
|         await makeOverlayPlot(page); | ||||
|         await enableEditMode(page); | ||||
|         await enableLogMode(page); | ||||
| @@ -56,7 +57,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...
 | ||||
| @@ -87,18 +88,14 @@ 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({ waitUntil: 'networkidle'}), | ||||
|         page.locator('text=OK').click(), | ||||
|         //Wait for Save Banner to appear
 | ||||
|         page.waitForSelector('.c-message-banner__message') | ||||
|         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() | ||||
|     ]); | ||||
|     //Wait until Save Banner is gone
 | ||||
|     await page.locator('.c-message-banner__close-button').click(); | ||||
|     await page.waitForSelector('.c-message-banner__message', { state: 'detached'}); | ||||
| 
 | ||||
|     // save the overlay plot
 | ||||
| 
 | ||||
|     await saveOverlayPlot(page); | ||||
|     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(); | ||||
| 
 | ||||
|     // create a sinewave generator
 | ||||
| 
 | ||||
| @@ -107,32 +104,27 @@ async function makeOverlayPlot(page) { | ||||
| 
 | ||||
|     // set amplitude to 6, offset 4, period 2
 | ||||
| 
 | ||||
|     await page.locator('div:nth-child(5) .c-form-row__controls .form-control .field input').click(); | ||||
|     await page.locator('div:nth-child(5) .c-form-row__controls .form-control .field input').fill('6'); | ||||
|     await page.locator('div:nth-child(5) .form-row .c-form-row__controls .form-control .field input').click(); | ||||
|     await page.locator('div:nth-child(5) .form-row .c-form-row__controls .form-control .field input').fill('6'); | ||||
| 
 | ||||
|     await page.locator('div:nth-child(6) .c-form-row__controls .form-control .field input').click(); | ||||
|     await page.locator('div:nth-child(6) .c-form-row__controls .form-control .field input').fill('4'); | ||||
|     await page.locator('div:nth-child(6) .form-row .c-form-row__controls .form-control .field input').click(); | ||||
|     await page.locator('div:nth-child(6) .form-row .c-form-row__controls .form-control .field input').fill('4'); | ||||
| 
 | ||||
|     await page.locator('div:nth-child(7) .c-form-row__controls .form-control .field input').click(); | ||||
|     await page.locator('div:nth-child(7) .c-form-row__controls .form-control .field input').fill('2'); | ||||
|     await page.locator('div:nth-child(7) .form-row .c-form-row__controls .form-control .field input').click(); | ||||
|     await page.locator('div:nth-child(7) .form-row .c-form-row__controls .form-control .field input').fill('2'); | ||||
| 
 | ||||
|     // Click OK to make generator
 | ||||
| 
 | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation({ waitUntil: 'networkidle'}), | ||||
|         page.locator('text=OK').click(), | ||||
|         //Wait for Save Banner to appear
 | ||||
|         page.waitForSelector('.c-message-banner__message') | ||||
|         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() | ||||
|     ]); | ||||
|     //Wait until Save Banner is gone
 | ||||
|     await page.locator('.c-message-banner__close-button').click(); | ||||
|     await page.waitForSelector('.c-message-banner__message', { state: 'detached'}); | ||||
| 
 | ||||
|     // click on overlay plot
 | ||||
| 
 | ||||
|     await page.locator('text=Open MCT My Items >> span').nth(3).click(); | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.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=Unnamed Overlay Plot').first().click() | ||||
|     ]); | ||||
| } | ||||
| @@ -141,7 +133,7 @@ async function makeOverlayPlot(page) { | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function testRegularTicks(page) { | ||||
|     const yTicks = await page.locator('.gl-plot-y-tick-label'); | ||||
|     const yTicks = 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'); | ||||
| @@ -156,7 +148,7 @@ async function testRegularTicks(page) { | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function testLogTicks(page) { | ||||
|     const yTicks = await page.locator('.gl-plot-y-tick-label'); | ||||
|     const yTicks = 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'); | ||||
| @@ -194,7 +186,6 @@ 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(); | ||||
| } | ||||
| 
 | ||||
| /** | ||||
| @@ -219,27 +210,17 @@ 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 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' }); | ||||
|     await page.locator('text=Save and Finish Editing').click(); | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| // FIXME: Remove this eslint exception once implemented
 | ||||
| // eslint-disable-next-line no-unused-vars
 | ||||
| async function testLogPlotPixels(page) { | ||||
|     const pixelsMatch = await page.evaluate(async () => { | ||||
|         // TODO get canvas pixels at a few locations to make sure they're the correct color, to test that the plot comes out as expected.
 | ||||
| 
 | ||||
|         await new Promise((r) => setTimeout(r, 5 * 1000)); | ||||
|         await new Promise((r) => setTimeout(r, 50)); | ||||
| 
 | ||||
|         // These are some pixels that should be blue points in the log plot.
 | ||||
|         // If the plot changes shape to an unexpected shape, this will
 | ||||
| @@ -1,161 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* | ||||
| Tests to verify log plot functionality when objects are missing | ||||
| */ | ||||
|  | ||||
| const { test } = require('../../../fixtures.js'); | ||||
| const { expect } = require('@playwright/test'); | ||||
|  | ||||
| test.describe('Handle missing object for plots', () => { | ||||
|     test('Displays empty div for missing stacked plot item', async ({ page }) => { | ||||
|         const errorLogs = []; | ||||
|  | ||||
|         page.on("console", (message) => { | ||||
|             if (message.type() === 'warning') { | ||||
|                 errorLogs.push(message.text()); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         //Make stacked plot | ||||
|         await makeStackedPlot(page); | ||||
|  | ||||
|         //Gets local storage and deletes the last sine wave generator in the stacked plot | ||||
|         const localStorage = await page.evaluate(() => window.localStorage); | ||||
|         const parsedData = JSON.parse(localStorage.mct); | ||||
|         const keys = Object.keys(parsedData); | ||||
|         const lastKey = keys[keys.length - 1]; | ||||
|  | ||||
|         delete parsedData[lastKey]; | ||||
|  | ||||
|         //Sets local storage with missing object | ||||
|         await page.evaluate( | ||||
|             `window.localStorage.setItem('mct', '${JSON.stringify(parsedData)}')` | ||||
|         ); | ||||
|  | ||||
|         //Reloads page and clicks on stacked plot | ||||
|         await Promise.all([ | ||||
|             page.reload(), | ||||
|             page.waitForLoadState('networkidle') | ||||
|         ]); | ||||
|  | ||||
|         //Verify Main section is there on load | ||||
|         await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Stacked Plot'); | ||||
|  | ||||
|         await page.locator('text=Open MCT My Items >> span').nth(3).click(); | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('text=Unnamed Stacked Plot').first().click() | ||||
|         ]); | ||||
|  | ||||
|         //Check that there is only one stacked item plot with a plot, the missing one will be empty | ||||
|         await expect(page.locator(".c-plot--stacked-container:has(.gl-plot)")).toHaveCount(1); | ||||
|         //Verify that console.warn is thrown | ||||
|         await expect(errorLogs).toHaveLength(1); | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| /** | ||||
|  * This is used the create a stacked plot object | ||||
|  * @private | ||||
|  */ | ||||
| async function makeStackedPlot(page) { | ||||
|     // fresh page with time range from 2022-03-29 22:00:00.000Z to 2022-03-29 22:00:30.000Z | ||||
|     await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|     // create stacked plot | ||||
|     await page.locator('button.c-create-button').click(); | ||||
|     await page.locator('li:has-text("Stacked Plot")').click(); | ||||
|  | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation({ waitUntil: 'networkidle'}), | ||||
|         page.locator('text=OK').click(), | ||||
|         //Wait for Save Banner to appear | ||||
|         page.waitForSelector('.c-message-banner__message') | ||||
|     ]); | ||||
|  | ||||
|     //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 stacked plot | ||||
|     await saveStackedPlot(page); | ||||
|  | ||||
|     // create a sinewave generator | ||||
|     await createSineWaveGenerator(page); | ||||
|  | ||||
|     // click on stacked plot | ||||
|     await page.locator('text=Open MCT My Items >> span').nth(3).click(); | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('text=Unnamed Stacked Plot').first().click() | ||||
|     ]); | ||||
|  | ||||
|     // create a second sinewave generator | ||||
|     await createSineWaveGenerator(page); | ||||
|  | ||||
|     // click on stacked plot | ||||
|     await page.locator('text=Open MCT My Items >> span').nth(3).click(); | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('text=Unnamed Stacked Plot').first().click() | ||||
|     ]); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * This is used to save a stacked plot object | ||||
|  * @private | ||||
|  */ | ||||
| async function saveStackedPlot(page) { | ||||
|     // save stacked plot | ||||
|     await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click(); | ||||
|  | ||||
|     await Promise.all([ | ||||
|         page.locator('text=Save and Finish Editing').click(), | ||||
|         //Wait for Save Banner to appear | ||||
|         page.waitForSelector('.c-message-banner__message') | ||||
|     ]); | ||||
|     //Wait until Save Banner is gone | ||||
|     await page.locator('.c-message-banner__close-button').click(); | ||||
|     await page.waitForSelector('.c-message-banner__message', { state: 'detached' }); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * This is used to create a sine wave generator object | ||||
|  * @private | ||||
|  */ | ||||
| async function createSineWaveGenerator(page) { | ||||
|     //Create sine wave generator | ||||
|     await page.locator('button.c-create-button').click(); | ||||
|     await page.locator('li:has-text("Sine Wave Generator")').click(); | ||||
|  | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation({ waitUntil: 'networkidle'}), | ||||
|         page.locator('text=OK').click(), | ||||
|         //Wait for Save Banner to appear | ||||
|         page.waitForSelector('.c-message-banner__message') | ||||
|     ]); | ||||
|     //Wait until Save Banner is gone | ||||
|     await page.locator('.c-message-banner__close-button').click(); | ||||
|     await page.waitForSelector('.c-message-banner__message', { state: 'detached'}); | ||||
| } | ||||
| @@ -1,41 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| const { test } = require('../../../fixtures.js'); | ||||
| // eslint-disable-next-line no-unused-vars | ||||
| const { expect } = require('@playwright/test'); | ||||
|  | ||||
| test.describe('Remote Clock', () => { | ||||
|     // eslint-disable-next-line require-await | ||||
|     test.fixme('blocks historical requests until first tick is received', async ({ page }) => { | ||||
|         test.info().annotations.push({ | ||||
|             type: 'issue', | ||||
|             description: 'https://github.com/nasa/openmct/issues/5221' | ||||
|         }); | ||||
|         // addInitScript to with remote clock | ||||
|         // Switch time conductor mode to 'remote clock' | ||||
|         // Navigate to telemetry | ||||
|         // Verify that the plot renders historical data within the correct bounds | ||||
|         // Refresh the page | ||||
|         // Verify again that the plot renders historical data within the correct bounds | ||||
|     }); | ||||
| }); | ||||
| @@ -1,104 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| const { test } = require('../../../fixtures'); | ||||
| const { expect } = require('@playwright/test'); | ||||
|  | ||||
| test.describe('Telemetry Table', () => { | ||||
|     test('unpauses and filters data when paused by button and user changes bounds', async ({ page }) => { | ||||
|         test.info().annotations.push({ | ||||
|             type: 'issue', | ||||
|             description: 'https://github.com/nasa/openmct/issues/5113' | ||||
|         }); | ||||
|  | ||||
|         const bannerMessage = '.c-message-banner__message'; | ||||
|         const createButton = 'button:has-text("Create")'; | ||||
|  | ||||
|         await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|         // Click create button | ||||
|         await page.locator(createButton).click(); | ||||
|         await page.locator('li:has-text("Telemetry Table")').click(); | ||||
|  | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('text=OK').click(), | ||||
|             // Wait for Save Banner to appear | ||||
|             page.waitForSelector(bannerMessage) | ||||
|         ]); | ||||
|  | ||||
|         // Save (exit edit mode) | ||||
|         await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(3).click(); | ||||
|         await page.locator('text=Save and Finish Editing').click(); | ||||
|  | ||||
|         // Click create button | ||||
|         await page.locator(createButton).click(); | ||||
|  | ||||
|         // add Sine Wave Generator with defaults | ||||
|         await page.locator('li:has-text("Sine Wave Generator")').click(); | ||||
|  | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('text=OK').click(), | ||||
|             // Wait for Save Banner to appear | ||||
|             page.waitForSelector(bannerMessage) | ||||
|         ]); | ||||
|  | ||||
|         // focus the Telemetry Table | ||||
|         await page.locator('text=Open MCT My Items >> span').nth(3).click(); | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('text=Unnamed Telemetry Table').first().click() | ||||
|         ]); | ||||
|  | ||||
|         // Click pause button | ||||
|         const pauseButton = page.locator('button.c-button.icon-pause'); | ||||
|         await pauseButton.click(); | ||||
|  | ||||
|         const tableWrapper = page.locator('div.c-table-wrapper'); | ||||
|         await expect(tableWrapper).toHaveClass(/is-paused/); | ||||
|  | ||||
|         // Subtract 5 minutes from the current end bound datetime and set it | ||||
|         const endTimeInput = page.locator('input[type="text"].c-input--datetime').nth(1); | ||||
|         await endTimeInput.click(); | ||||
|  | ||||
|         let endDate = await endTimeInput.inputValue(); | ||||
|         endDate = new Date(endDate); | ||||
|  | ||||
|         endDate.setUTCMinutes(endDate.getUTCMinutes() - 5); | ||||
|         endDate = endDate.toISOString().replace(/T/, ' '); | ||||
|  | ||||
|         await endTimeInput.fill(''); | ||||
|         await endTimeInput.fill(endDate); | ||||
|         await page.keyboard.press('Enter'); | ||||
|  | ||||
|         await expect(tableWrapper).not.toHaveClass(/is-paused/); | ||||
|  | ||||
|         // Get the most recent telemetry date | ||||
|         const latestTelemetryDate = await page.locator('table.c-telemetry-table__body > tbody > tr').last().locator('td').nth(1).getAttribute('title'); | ||||
|  | ||||
|         // Verify that it is <= our new end bound | ||||
|         const latestMilliseconds = Date.parse(latestTelemetryDate); | ||||
|         const endBoundMilliseconds = Date.parse(endDate); | ||||
|         expect(latestMilliseconds).toBeLessThanOrEqual(endBoundMilliseconds); | ||||
|     }); | ||||
| }); | ||||
| @@ -20,12 +20,11 @@ | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| const { test } = require('../../../fixtures.js'); | ||||
| const { expect } = require('@playwright/test'); | ||||
| const { test, expect } = require('@playwright/test'); | ||||
|  | ||||
| test.describe('Time conductor operations', () => { | ||||
| test.describe('Time counductor 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(); | ||||
|  | ||||
| @@ -68,168 +67,3 @@ test.describe('Time conductor 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(); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,184 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| const { test } = require('../../../fixtures.js'); | ||||
| const { expect } = require('@playwright/test'); | ||||
|  | ||||
| test.describe('Timer', () => { | ||||
|  | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         //Go to baseURL | ||||
|         await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|         //Click the Create button | ||||
|         await page.click('button:has-text("Create")'); | ||||
|  | ||||
|         // Click 'Timer' | ||||
|         await page.click('text=Timer'); | ||||
|  | ||||
|         // Click text=OK | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation({waitUntil: 'networkidle'}), | ||||
|             page.click('text=OK') | ||||
|         ]); | ||||
|  | ||||
|         await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Timer'); | ||||
|     }); | ||||
|  | ||||
|     test('Can perform actions on the Timer', async ({ page }) => { | ||||
|         test.info().annotations.push({ | ||||
|             type: 'issue', | ||||
|             description: 'https://github.com/nasa/openmct/issues/4313' | ||||
|         }); | ||||
|  | ||||
|         await test.step("From the tree context menu", async () => { | ||||
|             await triggerTimerContextMenuAction(page, 'Start'); | ||||
|             await triggerTimerContextMenuAction(page, 'Pause'); | ||||
|             await triggerTimerContextMenuAction(page, 'Restart at 0'); | ||||
|             await triggerTimerContextMenuAction(page, 'Stop'); | ||||
|         }); | ||||
|  | ||||
|         await test.step("From the 3dot menu", async () => { | ||||
|             await triggerTimer3dotMenuAction(page, 'Start'); | ||||
|             await triggerTimer3dotMenuAction(page, 'Pause'); | ||||
|             await triggerTimer3dotMenuAction(page, 'Restart at 0'); | ||||
|             await triggerTimer3dotMenuAction(page, 'Stop'); | ||||
|         }); | ||||
|  | ||||
|         await test.step("From the object view", async () => { | ||||
|             await triggerTimerViewAction(page, 'Start'); | ||||
|             await triggerTimerViewAction(page, 'Pause'); | ||||
|             await triggerTimerViewAction(page, 'Restart at 0'); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| /** | ||||
|  * Actions that can be performed on a timer from context menus. | ||||
|  * @typedef {'Start' | 'Stop' | 'Pause' | 'Restart at 0'} TimerAction | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * Actions that can be performed on a timer from the object view. | ||||
|  * @typedef {'Start' | 'Pause' | 'Restart at 0'} TimerViewAction | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * Open the timer context menu from the object tree. | ||||
|  * Expands the 'My Items' folder if it is not already expanded. | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function openTimerContextMenu(page) { | ||||
|     const myItemsFolder = page.locator('text=Open MCT My Items >> span').nth(3); | ||||
|     const className = await myItemsFolder.getAttribute('class'); | ||||
|     if (!className.includes('c-disclosure-triangle--expanded')) { | ||||
|         await myItemsFolder.click(); | ||||
|     } | ||||
|  | ||||
|     await page.locator(`a:has-text("Unnamed Timer")`).click({ | ||||
|         button: 'right' | ||||
|     }); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Trigger a timer action from the tree context menu | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  * @param {TimerAction} action | ||||
|  */ | ||||
| async function triggerTimerContextMenuAction(page, action) { | ||||
|     const menuAction = `.c-menu ul li >> text="${action}"`; | ||||
|     await openTimerContextMenu(page); | ||||
|     await page.locator(menuAction).click(); | ||||
|     assertTimerStateAfterAction(page, action); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Trigger a timer action from the 3dot menu | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  * @param {TimerAction} action | ||||
|  */ | ||||
| async function triggerTimer3dotMenuAction(page, action) { | ||||
|     const menuAction = `.c-menu ul li >> text="${action}"`; | ||||
|     const threeDotMenuButton = 'button[title="More options"]'; | ||||
|     let isActionAvailable = false; | ||||
|     let iterations = 0; | ||||
|     // Dismiss/open the 3dot menu until the action is available | ||||
|     // or a maxiumum number of iterations is reached | ||||
|     while (!isActionAvailable && iterations <= 20) { | ||||
|         await page.click('.c-object-view'); | ||||
|         await page.click(threeDotMenuButton); | ||||
|         isActionAvailable = await page.locator(menuAction).isVisible(); | ||||
|         iterations++; | ||||
|     } | ||||
|  | ||||
|     await page.locator(menuAction).click(); | ||||
|     assertTimerStateAfterAction(page, action); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Trigger a timer action from the object view | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  * @param {TimerViewAction} action | ||||
|  */ | ||||
| async function triggerTimerViewAction(page, action) { | ||||
|     const buttonTitle = buttonTitleFromAction(action); | ||||
|     await page.click(`button[title="${buttonTitle}"]`); | ||||
|     assertTimerStateAfterAction(page, action); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Takes in a TimerViewAction and returns the button title | ||||
|  * @param {TimerViewAction} action | ||||
|  */ | ||||
| function buttonTitleFromAction(action) { | ||||
|     switch (action) { | ||||
|     case 'Start': | ||||
|         return 'Start'; | ||||
|     case 'Pause': | ||||
|         return 'Pause'; | ||||
|     case 'Restart at 0': | ||||
|         return 'Reset'; | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Verify the timer state after a timer action has been performed. | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  * @param {TimerAction} action | ||||
|  */ | ||||
| async function assertTimerStateAfterAction(page, action) { | ||||
|     let timerStateClass; | ||||
|     switch (action) { | ||||
|     case 'Start': | ||||
|     case 'Restart at 0': | ||||
|         timerStateClass = "is-started"; | ||||
|         break; | ||||
|     case 'Stop': | ||||
|         timerStateClass = 'is-stopped'; | ||||
|         break; | ||||
|     case 'Pause': | ||||
|         timerStateClass = 'is-paused'; | ||||
|         break; | ||||
|     } | ||||
|  | ||||
|     await expect.soft(page.locator('.c-timer')).toHaveClass(new RegExp(timerStateClass)); | ||||
| } | ||||
							
								
								
									
										22
									
								
								e2e/tests/recycled_storage.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								e2e/tests/recycled_storage.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| { | ||||
|   "cookies": [], | ||||
|   "origins": [ | ||||
|     { | ||||
|       "origin": "http://localhost:8080", | ||||
|       "localStorage": [ | ||||
|         { | ||||
|           "name": "tcHistory", | ||||
|           "value": "{\"utc\":[{\"start\":1651513945533,\"end\":1651515745533}]}" | ||||
|         }, | ||||
|         { | ||||
|           "name": "mct", | ||||
|           "value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"c0f99e39-85e7-4ef7-99b1-ef52d4ed69b2\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1651515746374,\"modified\":1651515746374},\"c0f99e39-85e7-4ef7-99b1-ef52d4ed69b2\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"c0f99e39-85e7-4ef7-99b1-ef52d4ed69b2\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"e35a066b-eb0e-4b05-a4c9-cc31dc202572\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1651515746373,\"location\":\"mine\",\"persisted\":1651515746373}}" | ||||
|         }, | ||||
|         { | ||||
|           "name": "mct-tree-expanded", | ||||
|           "value": "[]" | ||||
|         } | ||||
|       ] | ||||
|     } | ||||
|   ] | ||||
| } | ||||
| @@ -33,8 +33,7 @@ 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 } = require('../fixtures.js'); | ||||
| const { expect } = require('@playwright/test'); | ||||
| const { test, expect } = require('@playwright/test'); | ||||
| 
 | ||||
| test('Verify that the create button appears and that the Folder Domain Object is available for selection', async ({ page }) => { | ||||
| 
 | ||||
| @@ -45,15 +44,6 @@ test('Verify that the create button appears and that the Folder Domain Object is | ||||
|     await page.click('button:has-text("Create")'); | ||||
| 
 | ||||
|     // Verify that Create Folder appears in the dropdown
 | ||||
|     await expect(page.locator(':nth-match(:text("Folder"), 2)')).toBeEnabled(); | ||||
| }); | ||||
| 
 | ||||
| test('Verify that My Items Tree appears @ipad', async ({ page }) => { | ||||
|     //Test.slow annotation is currently broken. Needs to be fixed in https://github.com/nasa/openmct/issues/5374
 | ||||
|     test.slow(); | ||||
|     //Go to baseURL
 | ||||
|     await page.goto('/'); | ||||
| 
 | ||||
|     //My Items to be visible
 | ||||
|     await expect(page.locator('a:has-text("My Items")')).toBeEnabled(); | ||||
|     const locator = page.locator(':nth-match(:text("Folder"), 2)'); | ||||
|     await expect(locator).toBeEnabled(); | ||||
| }); | ||||
| @@ -1,111 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* | ||||
| This test suite is dedicated to tests which verify search functionality. | ||||
| */ | ||||
|  | ||||
| const { expect } = require('@playwright/test'); | ||||
| const { test } = require('../../../../fixtures'); | ||||
|  | ||||
| /** | ||||
|   * Creates a notebook object and adds an entry. | ||||
|   * @param {import('@playwright/test').Page} page | ||||
|   */ | ||||
| async function createClockAndDisplayLayout(page) { | ||||
|     //Go to baseURL | ||||
|     await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|     // Click button:has-text("Create") | ||||
|     await page.locator('button:has-text("Create")').click(); | ||||
|     // Click li:has-text("Notebook") | ||||
|     await page.locator('li:has-text("Clock")').click(); | ||||
|     // Click button:has-text("OK") | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('button:has-text("OK")').click() | ||||
|     ]); | ||||
|  | ||||
|     // Click a:has-text("My Items") | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('a:has-text("My Items") >> nth=0').click() | ||||
|     ]); | ||||
|     // Click button:has-text("Create") | ||||
|     await page.locator('button:has-text("Create")').click(); | ||||
|     // Click li:has-text("Notebook") | ||||
|     await page.locator('li:has-text("Display Layout")').click(); | ||||
|     // Click button:has-text("OK") | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('button:has-text("OK")').click() | ||||
|     ]); | ||||
| } | ||||
|  | ||||
| test.describe('Grand Search', () => { | ||||
|     test('Can search for objects, and subsequent search dropdown behaves properly', async ({ page }) => { | ||||
|         await createClockAndDisplayLayout(page); | ||||
|  | ||||
|         // Click [aria-label="OpenMCT Search"] input[type="search"] | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); | ||||
|         // Fill [aria-label="OpenMCT Search"] input[type="search"] | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Cl'); | ||||
|         await expect(page.locator('[aria-label="Search Result"]')).toContainText('Clock'); | ||||
|         // Click text=Elements >> nth=0 | ||||
|         await page.locator('text=Elements').first().click(); | ||||
|         await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible(); | ||||
|  | ||||
|         // Click [aria-label="OpenMCT Search"] [aria-label="Search Input"] | ||||
|         await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click(); | ||||
|         // Click [aria-label="Unnamed Clock clock result"] >> text=Unnamed Clock | ||||
|         await page.locator('[aria-label="Unnamed Clock clock result"] >> text=Unnamed Clock').click(); | ||||
|         await expect(page.locator('.js-preview-window')).toBeVisible(); | ||||
|  | ||||
|         // Click [aria-label="Close"] | ||||
|         await page.locator('[aria-label="Close"]').click(); | ||||
|         await expect(page.locator('[aria-label="Search Result"]')).toBeVisible(); | ||||
|         await expect(page.locator('[aria-label="Search Result"]')).toContainText('Cloc'); | ||||
|  | ||||
|         // Click [aria-label="OpenMCT Search"] a >> nth=0 | ||||
|         await page.locator('[aria-label="OpenMCT Search"] a').first().click(); | ||||
|         await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible(); | ||||
|  | ||||
|         // Fill [aria-label="OpenMCT Search"] input[type="search"] | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('foo'); | ||||
|         await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible(); | ||||
|  | ||||
|         // Click text=Snapshot Save and Finish Editing Save and Continue Editing >> button >> nth=1 | ||||
|         await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click(); | ||||
|         // Click text=Save and Finish Editing | ||||
|         await page.locator('text=Save and Finish Editing').click(); | ||||
|         // Click [aria-label="OpenMCT Search"] [aria-label="Search Input"] | ||||
|         await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click(); | ||||
|         // Fill [aria-label="OpenMCT Search"] [aria-label="Search Input"] | ||||
|         await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').fill('Cl'); | ||||
|         // Click text=Unnamed Clock | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('text=Unnamed Clock').click() | ||||
|         ]); | ||||
|         await expect(page.locator('.is-object-type-clock')).toBeVisible(); | ||||
|     }); | ||||
| }); | ||||
| @@ -1,76 +0,0 @@ | ||||
| /* eslint-disable no-undef */ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* | ||||
| Collection of Visual Tests set to run with modified init scripts to inject plugins not otherwise available in the default contexts. | ||||
|  | ||||
| These should only use functional expect statements to verify assumptions about the state | ||||
| in a test and not for functional verification of correctness. Visual tests are not supposed | ||||
| to "fail" on assertions. Instead, they should be used to detect changes between builds or branches. | ||||
|  | ||||
| Note: Larger testsuite sizes are OK due to the setup time associated with these tests. | ||||
| */ | ||||
|  | ||||
| const { test } = require('@playwright/test'); | ||||
| const percySnapshot = require('@percy/playwright'); | ||||
| const path = require('path'); | ||||
| const sinon = require('sinon'); | ||||
|  | ||||
| const VISUAL_GRACE_PERIOD = 5 * 1000; //Lets the application "simmer" before the snapshot is taken | ||||
|  | ||||
| const CUSTOM_NAME = 'CUSTOM_NAME'; | ||||
|  | ||||
| // Snippet from https://github.com/microsoft/playwright/issues/6347#issuecomment-965887758 | ||||
| // Will replace with cy.clock() equivalent | ||||
| test.beforeEach(async ({ context }) => { | ||||
|     await context.addInitScript({ | ||||
|         path: path.join(__dirname, '../../..', './node_modules/sinon/pkg/sinon.js') | ||||
|     }); | ||||
|     await context.addInitScript(() => { | ||||
|         window.__clock = sinon.useFakeTimers({ | ||||
|             now: 0, | ||||
|             shouldAdvanceTime: true | ||||
|         }); //Set browser clock to UNIX Epoch | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| test('Visual - Restricted Notebook is visually correct @addInit', async ({ page }) => { | ||||
|     // eslint-disable-next-line no-undef | ||||
|     await page.addInitScript({ path: path.join(__dirname, '../plugins/notebook', './addInitRestrictedNotebook.js') }); | ||||
|     //Go to baseURL | ||||
|     await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|     //Click the Create button | ||||
|     await page.click('button:has-text("Create")'); | ||||
|     // Click text=CUSTOM_NAME | ||||
|     await page.click(`text=${CUSTOM_NAME}`); | ||||
|     // Click text=OK | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation({waitUntil: 'networkidle'}), | ||||
|         page.click('text=OK') | ||||
|     ]); | ||||
|  | ||||
|     // Take a snapshot of the newly created CUSTOM_NAME notebook | ||||
|     await page.waitForTimeout(VISUAL_GRACE_PERIOD); | ||||
|     await percySnapshot(page, 'Restricted Notebook with CUSTOM_NAME'); | ||||
|  | ||||
| }); | ||||
| @@ -1,70 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* | ||||
| Collection of Visual Tests set to run in a default context. The tests within this suite | ||||
| are only meant to run against openmct's app.js started by `npm run start` within the | ||||
| `./e2e/playwright-visual.config.js` file. | ||||
|  | ||||
| These should only use functional expect statements to verify assumptions about the state | ||||
| in a test and not for functional verification of correctness. Visual tests are not supposed | ||||
| to "fail" on assertions. Instead, they should be used to detect changes between builds or branches. | ||||
|  | ||||
| Note: Larger testsuite sizes are OK due to the setup time associated with these tests. | ||||
| */ | ||||
|  | ||||
| const { test, expect } = require('@playwright/test'); | ||||
| const percySnapshot = require('@percy/playwright'); | ||||
| const path = require('path'); | ||||
| const sinon = require('sinon'); | ||||
|  | ||||
| // Snippet from https://github.com/microsoft/playwright/issues/6347#issuecomment-965887758 | ||||
| // Will replace with cy.clock() equivalent | ||||
| test.beforeEach(async ({ context }) => { | ||||
|     await context.addInitScript({ | ||||
|         // eslint-disable-next-line no-undef | ||||
|         path: path.join(__dirname, '../../..', './node_modules/sinon/pkg/sinon.js') | ||||
|     }); | ||||
|     await context.addInitScript(() => { | ||||
|         window.__clock = sinon.useFakeTimers({ | ||||
|             now: 0, //Set browser clock to UNIX Epoch | ||||
|             shouldAdvanceTime: false, //Don't advance the clock | ||||
|             toFake: ["setTimeout", "nextTick"] | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
| test.use({ storageState: './e2e/test-data/VisualTestData_storage.json' }); | ||||
|  | ||||
| test('Visual - Overlay Plot Loading Indicator @localstorage', async ({ page }) => { | ||||
|     // Go to baseURL | ||||
|     await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|     await page.locator('a:has-text("Unnamed Overlay Plot Overlay Plot")').click(); | ||||
|     //Ensure that we're on the Unnamed Overlay Plot object | ||||
|     await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Overlay Plot'); | ||||
|  | ||||
|     //Wait for canvas to be rendered and stop animating | ||||
|     await page.locator('canvas >> nth=1').hover({trial: true}); | ||||
|  | ||||
|     //Take snapshot of Sine Wave Generator within Overlay Plot | ||||
|     await percySnapshot(page, 'SineWaveInOverlayPlot'); | ||||
| }); | ||||
| @@ -32,8 +32,7 @@ to "fail" on assertions. Instead, they should be used to detect changes between | ||||
| Note: Larger testsuite sizes are OK due to the setup time associated with these tests. | ||||
| */ | ||||
| 
 | ||||
| const { test } = require('../../fixtures.js'); | ||||
| const { expect } = require('@playwright/test'); | ||||
| const { test, expect } = require('@playwright/test'); | ||||
| const percySnapshot = require('@percy/playwright'); | ||||
| const path = require('path'); | ||||
| const sinon = require('sinon'); | ||||
| @@ -48,10 +47,7 @@ test.beforeEach(async ({ context }) => { | ||||
|         path: path.join(__dirname, '../../..', './node_modules/sinon/pkg/sinon.js') | ||||
|     }); | ||||
|     await context.addInitScript(() => { | ||||
|         window.__clock = sinon.useFakeTimers({ | ||||
|             now: 0, | ||||
|             shouldAdvanceTime: true | ||||
|         }); //Set browser clock to UNIX Epoch
 | ||||
|         window.__clock = sinon.useFakeTimers(); //Set browser clock to UNIX Epoch
 | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| @@ -60,7 +56,8 @@ test('Visual - Root and About', async ({ page }) => { | ||||
|     await page.goto('/', { waitUntil: 'networkidle' }); | ||||
| 
 | ||||
|     // Verify that Create button is actionable
 | ||||
|     await expect(page.locator('button:has-text("Create")')).toBeEnabled(); | ||||
|     const createButtonLocator = page.locator('button:has-text("Create")'); | ||||
|     await expect(createButtonLocator).toBeEnabled(); | ||||
| 
 | ||||
|     // Take a snapshot of the Dashboard
 | ||||
|     await page.waitForTimeout(VISUAL_GRACE_PERIOD); | ||||
| @@ -97,11 +94,7 @@ test('Visual - Default Condition Set', async ({ page }) => { | ||||
|     await percySnapshot(page, 'Default Condition Set'); | ||||
| }); | ||||
| 
 | ||||
| test.fixme('Visual - Default Condition Widget', async ({ page }) => { | ||||
|     test.info().annotations.push({ | ||||
|         type: 'issue', | ||||
|         description: 'https://github.com/nasa/openmct/issues/5349' | ||||
|     }); | ||||
| test('Visual - Default Condition Widget', async ({ page }) => { | ||||
|     //Go to baseURL
 | ||||
|     await page.goto('/', { waitUntil: 'networkidle' }); | ||||
| 
 | ||||
| @@ -178,55 +171,3 @@ test('Visual - Sine Wave Generator Form', async ({ page }) => { | ||||
|     await page.waitForTimeout(VISUAL_GRACE_PERIOD); | ||||
|     await percySnapshot(page, 'removed amplitude property value'); | ||||
| }); | ||||
| 
 | ||||
| test('Visual - Save Successful Banner', async ({ page }) => { | ||||
|     //Go to baseURL
 | ||||
|     await page.goto('/', { waitUntil: 'networkidle' }); | ||||
| 
 | ||||
|     //Click the Create button
 | ||||
|     await page.click('button:has-text("Create")'); | ||||
| 
 | ||||
|     //NOTE Something other than example imagery
 | ||||
|     await page.click('text=Timer'); | ||||
| 
 | ||||
|     // Click text=OK
 | ||||
|     await page.click('text=OK'); | ||||
|     await page.locator('.c-message-banner__message').hover({ trial: true }); | ||||
|     await percySnapshot(page, 'Banner message shown'); | ||||
| 
 | ||||
|     //Wait until Save Banner is gone
 | ||||
|     await page.waitForSelector('.c-message-banner__message', { state: 'detached'}); | ||||
|     await percySnapshot(page, 'Banner message gone'); | ||||
| }); | ||||
| 
 | ||||
| test('Visual - Display Layout Icon is correct', async ({ page }) => { | ||||
|     //Go to baseURL
 | ||||
|     await page.goto('/', { waitUntil: 'networkidle' }); | ||||
| 
 | ||||
|     //Click the Create button
 | ||||
|     await page.click('button:has-text("Create")'); | ||||
| 
 | ||||
|     //Hover on Display Layout option.
 | ||||
|     await page.locator('text=Display Layout').hover(); | ||||
|     await percySnapshot(page, 'Display Layout Create Menu'); | ||||
| 
 | ||||
| }); | ||||
| 
 | ||||
| test('Visual - Default Gauge is correct', async ({ page }) => { | ||||
| 
 | ||||
|     //Go to baseURL
 | ||||
|     await page.goto('/', { waitUntil: 'networkidle' }); | ||||
| 
 | ||||
|     //Click the Create button
 | ||||
|     await page.click('button:has-text("Create")'); | ||||
| 
 | ||||
|     await page.click('text=Gauge'); | ||||
| 
 | ||||
|     await page.click('text=OK'); | ||||
| 
 | ||||
|     // Take a snapshot of the newly created Gauge object
 | ||||
|     await page.waitForTimeout(VISUAL_GRACE_PERIOD); | ||||
|     await percySnapshot(page, 'Default Gauge'); | ||||
| 
 | ||||
| }); | ||||
| 
 | ||||
| @@ -1,95 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* | ||||
| This test suite is dedicated to generating LocalStorage via Session Storage to be used | ||||
| in some visual test suites like controlledClock.visual.spec.js. This suite should run to completion | ||||
| and generate an artifact named ./e2e/test-data/VisualTestData_storage.json . This will run | ||||
| on every Commit to ensure that this object still loads into tests correctly and will retain the | ||||
| .e2e.spec.js suffix. | ||||
|  | ||||
| TODO: Provide additional validation of object properties as it grows. | ||||
|  | ||||
| */ | ||||
|  | ||||
| const { test } = require('../../fixtures.js'); | ||||
| const { expect } = require('@playwright/test'); | ||||
|  | ||||
| test('Generate Visual Test Data @localStorage', async ({ page, context }) => { | ||||
|     //Go to baseURL | ||||
|     await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|     await page.locator('button:has-text("Create")').click(); | ||||
|  | ||||
|     // add overlay plot with defaults | ||||
|     await page.locator('li:has-text("Overlay Plot")').click(); | ||||
|  | ||||
|     // Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184 | ||||
|     await page.click('form[name="mctForm"] a:has-text("My Items")'); | ||||
|  | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('text=OK').click(), | ||||
|         //Wait for Save Banner to appear1 | ||||
|         page.waitForSelector('.c-message-banner__message') | ||||
|     ]); | ||||
|     //Wait until Save Banner is gone | ||||
|     await page.locator('.c-message-banner__close-button').click(); | ||||
|     await page.waitForSelector('.c-message-banner__message', { state: 'detached'}); | ||||
|  | ||||
|     // save (exit edit mode) | ||||
|     await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click(); | ||||
|     await page.locator('text=Save and Finish Editing').click(); | ||||
|  | ||||
|     // click create button | ||||
|     await page.locator('button:has-text("Create")').click(); | ||||
|  | ||||
|     // add sine wave generator with defaults | ||||
|     await page.locator('li:has-text("Sine Wave Generator")').click(); | ||||
|  | ||||
|     //Add a 5000 ms Delay | ||||
|     await page.locator('[aria-label="Loading Delay \\(ms\\)"]').fill('5000'); | ||||
|  | ||||
|     // Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184 | ||||
|     await page.click('form[name="mctForm"] a:has-text("Overlay Plot")'); | ||||
|  | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('text=OK').click(), | ||||
|         //Wait for Save Banner to appear1 | ||||
|         page.waitForSelector('.c-message-banner__message') | ||||
|     ]); | ||||
|     //Wait until Save Banner is gone | ||||
|     await page.locator('.c-message-banner__close-button').click(); | ||||
|     await page.waitForSelector('.c-message-banner__message', { state: 'detached'}); | ||||
|  | ||||
|     // focus the overlay plot | ||||
|     await page.locator('text=Open MCT My Items >> span').nth(3).click(); | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('text=Unnamed Overlay Plot').first().click() | ||||
|     ]); | ||||
|  | ||||
|     await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Overlay Plot'); | ||||
|     //Save localStorage for future test execution | ||||
|     await context.storageState({ path: './e2e/test-data/VisualTestData_storage.json' }); | ||||
| }); | ||||
| @@ -1,104 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* | ||||
| This test suite is dedicated to tests which verify search functionality. | ||||
| */ | ||||
|  | ||||
| const { test, expect } = require('@playwright/test'); | ||||
| const percySnapshot = require('@percy/playwright'); | ||||
|  | ||||
| /** | ||||
|   * Creates a notebook object and adds an entry. | ||||
|   * @param {import('@playwright/test').Page} page | ||||
|   */ | ||||
| async function createClockAndDisplayLayout(page) { | ||||
|     //Go to baseURL | ||||
|     await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|     // Click button:has-text("Create") | ||||
|     await page.locator('button:has-text("Create")').click(); | ||||
|     // Click li:has-text("Notebook") | ||||
|     await page.locator('li:has-text("Clock")').click(); | ||||
|     // Click button:has-text("OK") | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('button:has-text("OK")').click() | ||||
|     ]); | ||||
|  | ||||
|     // Click a:has-text("My Items") | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('a:has-text("My Items") >> nth=0').click() | ||||
|     ]); | ||||
|     // Click button:has-text("Create") | ||||
|     await page.locator('button:has-text("Create")').click(); | ||||
|     // Click li:has-text("Notebook") | ||||
|     await page.locator('li:has-text("Display Layout")').click(); | ||||
|     // Click button:has-text("OK") | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('button:has-text("OK")').click() | ||||
|     ]); | ||||
| } | ||||
|  | ||||
| test.describe('Grand Search', () => { | ||||
|     test('Can search for objects, and subsequent search dropdown behaves properly', async ({ page }) => { | ||||
|         await createClockAndDisplayLayout(page); | ||||
|  | ||||
|         // Click [aria-label="OpenMCT Search"] input[type="search"] | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); | ||||
|         // Fill [aria-label="OpenMCT Search"] input[type="search"] | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Cl'); | ||||
|         await expect(page.locator('[aria-label="Search Result"]')).toContainText('Clock'); | ||||
|         await percySnapshot(page, 'Searching for Clocks'); | ||||
|         // Click text=Elements >> nth=0 | ||||
|         await page.locator('text=Elements').first().click(); | ||||
|         await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible(); | ||||
|  | ||||
|         // Click [aria-label="OpenMCT Search"] [aria-label="Search Input"] | ||||
|         await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click(); | ||||
|         // Click [aria-label="Unnamed Clock clock result"] >> text=Unnamed Clock | ||||
|         await page.locator('[aria-label="Unnamed Clock clock result"] >> text=Unnamed Clock').click(); | ||||
|         await percySnapshot(page, 'Preview for clock should display when editing enabled and search item clicked'); | ||||
|  | ||||
|         // Click [aria-label="Close"] | ||||
|         await page.locator('[aria-label="Close"]').click(); | ||||
|         await percySnapshot(page, 'Search should still be showing after preview closed'); | ||||
|  | ||||
|         // Click text=Snapshot Save and Finish Editing Save and Continue Editing >> button >> nth=1 | ||||
|         await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click(); | ||||
|         // Click text=Save and Finish Editing | ||||
|         await page.locator('text=Save and Finish Editing').click(); | ||||
|         // Click [aria-label="OpenMCT Search"] [aria-label="Search Input"] | ||||
|         await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click(); | ||||
|         // Fill [aria-label="OpenMCT Search"] [aria-label="Search Input"] | ||||
|         await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').fill('Cl'); | ||||
|         // Click text=Unnamed Clock | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('text=Unnamed Clock').click() | ||||
|         ]); | ||||
|         await percySnapshot(page, 'Clicking on search results should navigate to them if not editing'); | ||||
|  | ||||
|     }); | ||||
| }); | ||||
| @@ -1,33 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
| import availableTags from './tags.json'; | ||||
| /** | ||||
|  * @returns {function} The plugin install function | ||||
|  */ | ||||
| export default function exampleTagsPlugin() { | ||||
|     return function install(openmct) { | ||||
|         Object.keys(availableTags.tags).forEach(tagKey => { | ||||
|             const tagDefinition = availableTags.tags[tagKey]; | ||||
|             openmct.annotation.defineTag(tagKey, tagDefinition); | ||||
|         }); | ||||
|     }; | ||||
| } | ||||
| @@ -1,19 +0,0 @@ | ||||
| { | ||||
|     "tags": { | ||||
|         "46a62ad1-bb86-4f88-9a17-2a029e12669d": { | ||||
|             "label": "Science", | ||||
|             "backgroundColor": "#cc0000", | ||||
|             "foregroundColor": "#ffffff" | ||||
|         }, | ||||
|         "65f150ef-73b7-409a-b2e8-258cbd8b7323": { | ||||
|             "label": "Driving", | ||||
|             "backgroundColor": "#ffad32", | ||||
|             "foregroundColor": "#333333" | ||||
|         }, | ||||
|         "f156b038-c605-46db-88a6-67cf2489a371": { | ||||
|             "label": "Drilling", | ||||
|             "backgroundColor": "#b0ac4e", | ||||
|             "foregroundColor": "#FFFFFF" | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -21,56 +21,19 @@ | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import EventEmitter from 'EventEmitter'; | ||||
| import { v4 as uuid } from 'uuid'; | ||||
| import uuid from 'uuid'; | ||||
| import createExampleUser from './exampleUserCreator'; | ||||
|  | ||||
| const STATUSES = [{ | ||||
|     key: "NO_STATUS", | ||||
|     label: "Not set", | ||||
|     iconClass: "icon-question-mark", | ||||
|     iconClassPoll: "icon-status-poll-question-mark" | ||||
| }, { | ||||
|     key: "GO", | ||||
|     label: "GO", | ||||
|     iconClass: "icon-check", | ||||
|     iconClassPoll: "icon-status-poll-question-mark", | ||||
|     statusClass: "s-status-ok", | ||||
|     statusBgColor: "#33cc33", | ||||
|     statusFgColor: "#000" | ||||
| }, { | ||||
|     key: "MAYBE", | ||||
|     label: "MAYBE", | ||||
|     iconClass: "icon-alert-triangle", | ||||
|     iconClassPoll: "icon-status-poll-question-mark", | ||||
|     statusClass: "s-status-warning", | ||||
|     statusBgColor: "#ffb66c", | ||||
|     statusFgColor: "#000" | ||||
| }, { | ||||
|     key: "NO_GO", | ||||
|     label: "NO GO", | ||||
|     iconClass: "icon-circle-slash", | ||||
|     iconClassPoll: "icon-status-poll-question-mark", | ||||
|     statusClass: "s-status-error", | ||||
|     statusBgColor: "#9900cc", | ||||
|     statusFgColor: "#fff" | ||||
| }]; | ||||
| /** | ||||
|  * @implements {StatusUserProvider} | ||||
|  */ | ||||
| export default class ExampleUserProvider extends EventEmitter { | ||||
|     constructor(openmct, {defaultStatusRole} = {defaultStatusRole: undefined}) { | ||||
|     constructor(openmct) { | ||||
|         super(); | ||||
|  | ||||
|         this.openmct = openmct; | ||||
|         this.user = undefined; | ||||
|         this.loggedIn = false; | ||||
|         this.autoLoginUser = undefined; | ||||
|         this.status = STATUSES[1]; | ||||
|         this.pollQuestion = undefined; | ||||
|         this.defaultStatusRole = defaultStatusRole; | ||||
|  | ||||
|         this.ExampleUser = createExampleUser(this.openmct.user.User); | ||||
|         this.loginPromise = undefined; | ||||
|     } | ||||
|  | ||||
|     isLoggedIn() { | ||||
| @@ -82,19 +45,11 @@ export default class ExampleUserProvider extends EventEmitter { | ||||
|     } | ||||
|  | ||||
|     getCurrentUser() { | ||||
|         if (!this.loginPromise) { | ||||
|             this.loginPromise = this._login().then(() => this.user); | ||||
|         if (this.loggedIn) { | ||||
|             return Promise.resolve(this.user); | ||||
|         } | ||||
|  | ||||
|         return this.loginPromise; | ||||
|     } | ||||
|  | ||||
|     canProvideStatusForRole() { | ||||
|         return Promise.resolve(true); | ||||
|     } | ||||
|  | ||||
|     canSetPollQuestion() { | ||||
|         return Promise.resolve(true); | ||||
|         return this._login().then(() => this.user); | ||||
|     } | ||||
|  | ||||
|     hasRole(roleId) { | ||||
| @@ -105,55 +60,6 @@ export default class ExampleUserProvider extends EventEmitter { | ||||
|         return Promise.resolve(this.user.getRoles().includes(roleId)); | ||||
|     } | ||||
|  | ||||
|     getStatusRoleForCurrentUser() { | ||||
|         return Promise.resolve(this.defaultStatusRole); | ||||
|     } | ||||
|  | ||||
|     getAllStatusRoles() { | ||||
|         return Promise.resolve([this.defaultStatusRole]); | ||||
|     } | ||||
|  | ||||
|     getStatusForRole(role) { | ||||
|         return Promise.resolve(this.status); | ||||
|     } | ||||
|  | ||||
|     async getDefaultStatusForRole(role) { | ||||
|         const allRoles = await this.getPossibleStatuses(); | ||||
|  | ||||
|         return allRoles?.[0]; | ||||
|     } | ||||
|  | ||||
|     setStatusForRole(role, status) { | ||||
|         this.status = status; | ||||
|         this.emit('statusChange', { | ||||
|             role, | ||||
|             status | ||||
|         }); | ||||
|  | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     getPollQuestion() { | ||||
|         return Promise.resolve({ | ||||
|             question: 'Set "GO" if your position is ready for a boarding action on the Klingon cruiser', | ||||
|             timestamp: Date.now() | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     setPollQuestion(pollQuestion) { | ||||
|         this.pollQuestion = { | ||||
|             question: pollQuestion, | ||||
|             timestamp: Date.now() | ||||
|         }; | ||||
|         this.emit("pollQuestionChange", this.pollQuestion); | ||||
|  | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     getPossibleStatuses() { | ||||
|         return Promise.resolve(STATUSES); | ||||
|     } | ||||
|  | ||||
|     _login() { | ||||
|         const id = uuid(); | ||||
|  | ||||
| @@ -202,6 +108,3 @@ export default class ExampleUserProvider extends EventEmitter { | ||||
|         ); | ||||
|     } | ||||
| } | ||||
| /** | ||||
|  * @typedef {import('@/api/user/StatusUserProvider').default} StatusUserProvider | ||||
|  */ | ||||
|   | ||||
| @@ -22,19 +22,8 @@ | ||||
|  | ||||
| import ExampleUserProvider from './ExampleUserProvider'; | ||||
|  | ||||
| export default function ExampleUserPlugin({autoLoginUser, defaultStatusRole} = { | ||||
|     autoLoginUser: 'guest', | ||||
|     defaultStatusRole: 'test-role' | ||||
| }) { | ||||
| export default function ExampleUserPlugin() { | ||||
|     return function install(openmct) { | ||||
|         const userProvider = new ExampleUserProvider(openmct, { | ||||
|             defaultStatusRole | ||||
|         }); | ||||
|  | ||||
|         if (autoLoginUser !== undefined) { | ||||
|             userProvider.autoLogin(autoLoginUser); | ||||
|         } | ||||
|  | ||||
|         openmct.user.setProvider(userProvider); | ||||
|         openmct.user.setProvider(new ExampleUserProvider(openmct)); | ||||
|     }; | ||||
| } | ||||
|   | ||||
| @@ -26,7 +26,7 @@ import { | ||||
| } from '../../src/utils/testing'; | ||||
| import ExampleUserProvider from './ExampleUserProvider'; | ||||
|  | ||||
| describe("The Example User Plugin", () => { | ||||
| xdescribe("The Example User Plugin", () => { | ||||
|     let openmct; | ||||
|  | ||||
|     beforeEach(() => { | ||||
| @@ -47,4 +47,9 @@ describe("The Example User Plugin", () => { | ||||
|         }); | ||||
|         openmct.install(openmct.plugins.example.ExampleUser()); | ||||
|     }); | ||||
|  | ||||
|     // The rest of the functionality of the ExampleUser Plugin is | ||||
|     // tested in both the UserAPISpec.js and in the UserIndicatorPlugin spec. | ||||
|     // If that changes, those tests can be moved here. | ||||
|  | ||||
| }); | ||||
|   | ||||
| @@ -1,83 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| export default function () { | ||||
|     return function install(openmct) { | ||||
|         openmct.install(openmct.plugins.FaultManagement()); | ||||
|  | ||||
|         openmct.faults.addProvider({ | ||||
|             request(domainObject, options) { | ||||
|                 const faults = JSON.parse(localStorage.getItem('faults')); | ||||
|  | ||||
|                 return Promise.resolve(faults.alarms); | ||||
|             }, | ||||
|             subscribe(domainObject, callback) { | ||||
|                 const faultsData = JSON.parse(localStorage.getItem('faults')).alarms; | ||||
|  | ||||
|                 function getRandomIndex(start, end) { | ||||
|                     return Math.floor(start + (Math.random() * (end - start + 1))); | ||||
|                 } | ||||
|  | ||||
|                 let id = setInterval(() => { | ||||
|                     const index = getRandomIndex(0, faultsData.length - 1); | ||||
|                     const randomFaultData = faultsData[index]; | ||||
|                     const randomFault = randomFaultData.fault; | ||||
|                     randomFault.currentValueInfo.value = Math.random(); | ||||
|                     callback({ | ||||
|                         fault: randomFault, | ||||
|                         type: 'alarms' | ||||
|                     }); | ||||
|                 }, 300); | ||||
|  | ||||
|                 return () => { | ||||
|                     clearInterval(id); | ||||
|                 }; | ||||
|             }, | ||||
|             supportsRequest(domainObject) { | ||||
|                 const faults = localStorage.getItem('faults'); | ||||
|  | ||||
|                 return faults && domainObject.type === 'faultManagement'; | ||||
|             }, | ||||
|             supportsSubscribe(domainObject) { | ||||
|                 const faults = localStorage.getItem('faults'); | ||||
|  | ||||
|                 return faults && domainObject.type === 'faultManagement'; | ||||
|             }, | ||||
|             acknowledgeFault(fault, { comment = '' }) { | ||||
|                 console.log('acknowledgeFault', fault); | ||||
|                 console.log('comment', comment); | ||||
|  | ||||
|                 return Promise.resolve({ | ||||
|                     success: true | ||||
|                 }); | ||||
|             }, | ||||
|             shelveFault(fault, shelveData) { | ||||
|                 console.log('shelveFault', fault); | ||||
|                 console.log('shelveData', shelveData); | ||||
|  | ||||
|                 return Promise.resolve({ | ||||
|                     success: true | ||||
|                 }); | ||||
|             } | ||||
|         }); | ||||
|     }; | ||||
| } | ||||
| @@ -1,47 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import { | ||||
|     createOpenMct, | ||||
|     resetApplicationState | ||||
| } from '../../src/utils/testing'; | ||||
|  | ||||
| describe("The Example Fault Source Plugin", () => { | ||||
|     let openmct; | ||||
|  | ||||
|     beforeEach(() => { | ||||
|         openmct = createOpenMct(); | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     it('is not installed by default', () => { | ||||
|         expect(openmct.faults.provider).toBeUndefined(); | ||||
|     }); | ||||
|  | ||||
|     it('can be installed', () => { | ||||
|         openmct.install(openmct.plugins.example.ExampleFaultSource()); | ||||
|         expect(openmct.faults.provider).not.toBeUndefined(); | ||||
|     }); | ||||
| }); | ||||
| @@ -29,12 +29,12 @@ define([ | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     key: "wavelengths", | ||||
|                     name: "Wavelength", | ||||
|                     unit: "nm", | ||||
|                     format: 'string[]', | ||||
|                     key: "cos", | ||||
|                     name: "Cosine", | ||||
|                     unit: "deg", | ||||
|                     formatString: '%0.2f', | ||||
|                     hints: { | ||||
|                         range: 4 | ||||
|                         domain: 3 | ||||
|                     } | ||||
|                 }, | ||||
|                 // Need to enable "LocalTimeSystem" plugin to make use of this | ||||
| @@ -64,14 +64,6 @@ define([ | ||||
|                     hints: { | ||||
|                         range: 2 | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     key: "intensities", | ||||
|                     name: "Intensities", | ||||
|                     format: 'number[]', | ||||
|                     hints: { | ||||
|                         range: 3 | ||||
|                     } | ||||
|                 } | ||||
|             ] | ||||
|         }, | ||||
|   | ||||
| @@ -32,8 +32,7 @@ define([ | ||||
|         offset: 0, | ||||
|         dataRateInHz: 1, | ||||
|         randomness: 0, | ||||
|         phase: 0, | ||||
|         loadDelay: 0 | ||||
|         phase: 0 | ||||
|     }; | ||||
|  | ||||
|     function GeneratorProvider(openmct) { | ||||
| @@ -54,9 +53,8 @@ define([ | ||||
|             'period', | ||||
|             'offset', | ||||
|             'dataRateInHz', | ||||
|             'randomness', | ||||
|             'phase', | ||||
|             'loadDelay' | ||||
|             'randomness' | ||||
|         ]; | ||||
|  | ||||
|         request = request || {}; | ||||
|   | ||||
| @@ -23,7 +23,7 @@ | ||||
| define([ | ||||
|     'uuid' | ||||
| ], function ( | ||||
|     { v4: uuid } | ||||
|     uuid | ||||
| ) { | ||||
|     function WorkerInterface(openmct) { | ||||
|         // eslint-disable-next-line no-undef | ||||
|   | ||||
| @@ -77,8 +77,7 @@ | ||||
|                             utc: nextStep, | ||||
|                             yesterday: nextStep - 60 * 60 * 24 * 1000, | ||||
|                             sin: sin(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness), | ||||
|                             wavelengths: wavelengths(), | ||||
|                             intensities: intensities(), | ||||
|                             wavelength: wavelength(start, nextStep), | ||||
|                             cos: cos(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness) | ||||
|                         } | ||||
|                     }); | ||||
| @@ -116,7 +115,6 @@ | ||||
|         var dataRateInHz = request.dataRateInHz; | ||||
|         var phase = request.phase; | ||||
|         var randomness = request.randomness; | ||||
|         var loadDelay = Math.max(request.loadDelay, 0); | ||||
|  | ||||
|         var step = 1000 / dataRateInHz; | ||||
|         var nextStep = start - (start % step) + step; | ||||
| @@ -128,20 +126,11 @@ | ||||
|                 utc: nextStep, | ||||
|                 yesterday: nextStep - 60 * 60 * 24 * 1000, | ||||
|                 sin: sin(nextStep, period, amplitude, offset, phase, randomness), | ||||
|                 wavelengths: wavelengths(), | ||||
|                 intensities: intensities(), | ||||
|                 wavelength: wavelength(start, nextStep), | ||||
|                 cos: cos(nextStep, period, amplitude, offset, phase, randomness) | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         if (loadDelay === 0) { | ||||
|             postOnRequest(message, request, data); | ||||
|         } else { | ||||
|             setTimeout(() => postOnRequest(message, request, data), loadDelay); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function postOnRequest(message, request, data) { | ||||
|         self.postMessage({ | ||||
|             id: message.id, | ||||
|             data: request.spectra ? { | ||||
| @@ -165,28 +154,8 @@ | ||||
|             * Math.sin(phase + (timestamp / period / 1000 * Math.PI * 2)) + (amplitude * Math.random() * randomness) + offset; | ||||
|     } | ||||
|  | ||||
|     function wavelengths() { | ||||
|         let values = []; | ||||
|         while (values.length < 5) { | ||||
|             const randomValue = Math.random() * 100; | ||||
|             if (!values.includes(randomValue)) { | ||||
|                 values.push(String(randomValue)); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return values; | ||||
|     } | ||||
|  | ||||
|     function intensities() { | ||||
|         let values = []; | ||||
|         while (values.length < 5) { | ||||
|             const randomValue = Math.random() * 10; | ||||
|             if (!values.includes(randomValue)) { | ||||
|                 values.push(String(randomValue)); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return values; | ||||
|     function wavelength(start, nextStep) { | ||||
|         return (nextStep - start) / 10; | ||||
|     } | ||||
|  | ||||
|     function sendError(error, message) { | ||||
|   | ||||
| @@ -81,7 +81,7 @@ define([ | ||||
|                 { | ||||
|                     name: "Amplitude", | ||||
|                     control: "numberfield", | ||||
|                     cssClass: "l-numeric", | ||||
|                     cssClass: "l-input-sm l-numeric", | ||||
|                     key: "amplitude", | ||||
|                     required: true, | ||||
|                     property: [ | ||||
| @@ -92,7 +92,7 @@ define([ | ||||
|                 { | ||||
|                     name: "Offset", | ||||
|                     control: "numberfield", | ||||
|                     cssClass: "l-numeric", | ||||
|                     cssClass: "l-input-sm l-numeric", | ||||
|                     key: "offset", | ||||
|                     required: true, | ||||
|                     property: [ | ||||
| @@ -132,17 +132,6 @@ define([ | ||||
|                         "telemetry", | ||||
|                         "randomness" | ||||
|                     ] | ||||
|                 }, | ||||
|                 { | ||||
|                     name: "Loading Delay (ms)", | ||||
|                     control: "numberfield", | ||||
|                     cssClass: "l-input-sm l-numeric", | ||||
|                     key: "loadDelay", | ||||
|                     required: true, | ||||
|                     property: [ | ||||
|                         "telemetry", | ||||
|                         "loadDelay" | ||||
|                     ] | ||||
|                 } | ||||
|             ], | ||||
|             initialize: function (object) { | ||||
| @@ -152,8 +141,7 @@ define([ | ||||
|                     offset: 0, | ||||
|                     dataRateInHz: 1, | ||||
|                     phase: 0, | ||||
|                     randomness: 0, | ||||
|                     loadDelay: 0 | ||||
|                     randomness: 0 | ||||
|                 }; | ||||
|             } | ||||
|         }); | ||||
|   | ||||
| @@ -59,8 +59,7 @@ export default function () { | ||||
|                 object.configuration = { | ||||
|                     imageLocation: '', | ||||
|                     imageLoadDelayInMilliSeconds: DEFAULT_IMAGE_LOAD_DELAY_IN_MILISECONDS, | ||||
|                     imageSamples: [], | ||||
|                     layers: [] | ||||
|                     imageSamples: [] | ||||
|                 }; | ||||
|  | ||||
|                 object.telemetry = { | ||||
| @@ -91,21 +90,7 @@ export default function () { | ||||
|                             format: 'image', | ||||
|                             hints: { | ||||
|                                 image: 1 | ||||
|                             }, | ||||
|                             layers: [ | ||||
|                                 { | ||||
|                                     source: 'dist/imagery/example-imagery-layer-16x9.png', | ||||
|                                     name: '16:9' | ||||
|                                 }, | ||||
|                                 { | ||||
|                                     source: 'dist/imagery/example-imagery-layer-safe.png', | ||||
|                                     name: 'Safe' | ||||
|                                 }, | ||||
|                                 { | ||||
|                                     source: 'dist/imagery/example-imagery-layer-scale.png', | ||||
|                                     name: 'Scale' | ||||
|                                 } | ||||
|                             ] | ||||
|                             } | ||||
|                         }, | ||||
|                         { | ||||
|                             name: 'Image Download Name', | ||||
| @@ -168,7 +153,7 @@ function getImageUrlListFromConfig(configuration) { | ||||
| } | ||||
|  | ||||
| function getImageLoadDelay(domainObject) { | ||||
|     const imageLoadDelay = Math.trunc(Number(domainObject.configuration.imageLoadDelayInMilliSeconds)); | ||||
|     const imageLoadDelay = domainObject.configuration.imageLoadDelayInMilliSeconds; | ||||
|     if (!imageLoadDelay) { | ||||
|         openmctInstance.objects.mutate(domainObject, 'configuration.imageLoadDelayInMilliSeconds', DEFAULT_IMAGE_LOAD_DELAY_IN_MILISECONDS); | ||||
|  | ||||
| @@ -190,9 +175,7 @@ function getRealtimeProvider() { | ||||
|         subscribe: (domainObject, callback) => { | ||||
|             const delay = getImageLoadDelay(domainObject); | ||||
|             const interval = setInterval(() => { | ||||
|                 const imageSamples = getImageSamples(domainObject.configuration); | ||||
|                 const datum = pointForTimestamp(Date.now(), domainObject.name, imageSamples, delay); | ||||
|                 callback(datum); | ||||
|                 callback(pointForTimestamp(Date.now(), domainObject.name, getImageSamples(domainObject.configuration), delay)); | ||||
|             }, delay); | ||||
|  | ||||
|             return () => { | ||||
| @@ -231,9 +214,8 @@ function getLadProvider() { | ||||
|         }, | ||||
|         request: (domainObject, options) => { | ||||
|             const delay = getImageLoadDelay(domainObject); | ||||
|             const datum = pointForTimestamp(Date.now(), domainObject.name, getImageSamples(domainObject.configuration), delay); | ||||
|  | ||||
|             return Promise.resolve([datum]); | ||||
|             return Promise.resolve([pointForTimestamp(Date.now(), domainObject.name, delay)]); | ||||
|         } | ||||
|     }; | ||||
| } | ||||
|   | ||||
| @@ -75,12 +75,12 @@ | ||||
|         const TWO_HOURS = ONE_HOUR * 2; | ||||
|         const ONE_DAY = ONE_HOUR * 24; | ||||
|  | ||||
|  | ||||
|         openmct.install(openmct.plugins.LocalStorage()); | ||||
|  | ||||
|         openmct.install(openmct.plugins.example.Generator()); | ||||
|         openmct.install(openmct.plugins.example.EventGeneratorPlugin()); | ||||
|         openmct.install(openmct.plugins.example.ExampleImagery()); | ||||
|         openmct.install(openmct.plugins.example.ExampleTags()); | ||||
|  | ||||
|         openmct.install(openmct.plugins.Espresso()); | ||||
|         openmct.install(openmct.plugins.MyItems()); | ||||
| @@ -191,13 +191,11 @@ | ||||
|         openmct.install(openmct.plugins.ObjectMigration()); | ||||
|         openmct.install(openmct.plugins.ClearData( | ||||
|             ['table', 'telemetry.plot.overlay', 'telemetry.plot.stacked', 'example.imagery'], | ||||
|             { indicator: true } | ||||
|             {indicator: true} | ||||
|         )); | ||||
|         openmct.install(openmct.plugins.Clock({ enableClockIndicator: true })); | ||||
|         openmct.install(openmct.plugins.Timer()); | ||||
|         openmct.install(openmct.plugins.Timelist()); | ||||
|         openmct.install(openmct.plugins.BarChart()); | ||||
|         openmct.install(openmct.plugins.ScatterPlot()); | ||||
|         openmct.start(); | ||||
|     </script> | ||||
| </html> | ||||
|   | ||||
| @@ -74,8 +74,13 @@ module.exports = (config) => { | ||||
|         }, | ||||
|         coverageIstanbulReporter: { | ||||
|             fixWebpackSourcePaths: true, | ||||
|             dir: "coverage/unit", | ||||
|             reports: ['lcovonly'] | ||||
|             dir: "dist/reports/coverage", | ||||
|             reports: ['lcovonly', 'text-summary'], | ||||
|             thresholds: { | ||||
|                 global: { | ||||
|                     lines: 52 | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|         specReporter: { | ||||
|             maxLogLines: 5, | ||||
|   | ||||
							
								
								
									
										69
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										69
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,45 +1,46 @@ | ||||
| { | ||||
|   "name": "openmct", | ||||
|   "version": "2.0.5", | ||||
|   "version": "2.0.4", | ||||
|   "description": "The Open MCT core platform", | ||||
|   "devDependencies": { | ||||
|     "@babel/eslint-parser": "7.18.2", | ||||
|     "@babel/eslint-parser": "7.16.3", | ||||
|     "@braintree/sanitize-url": "6.0.0", | ||||
|     "@percy/cli": "1.2.1", | ||||
|     "@percy/playwright": "1.0.4", | ||||
|     "@playwright/test": "1.23.0", | ||||
|     "@percy/cli": "1.0.4", | ||||
|     "@percy/playwright": "1.0.2", | ||||
|     "@playwright/test": "1.19.2", | ||||
|     "@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", | ||||
|     "codecov":"3.8.3", | ||||
|     "copy-webpack-plugin": "11.0.0", | ||||
|     "copy-webpack-plugin": "10.2.0", | ||||
|     "cross-env": "7.0.3", | ||||
|     "css-loader": "4.0.0", | ||||
|     "d3-axis": "3.0.0", | ||||
|     "d3-scale": "3.3.0", | ||||
|     "d3-selection": "3.0.0", | ||||
|     "d3-axis": "1.0.x", | ||||
|     "d3-scale": "1.0.x", | ||||
|     "d3-selection": "1.3.x", | ||||
|     "eslint": "8.13.0", | ||||
|     "eslint-plugin-compat": "4.0.2", | ||||
|     "eslint-plugin-playwright": "0.9.0", | ||||
|     "eslint-plugin-vue": "9.1.0", | ||||
|     "eslint-plugin-playwright": "0.8.0", | ||||
|     "eslint-plugin-vue": "8.5.0", | ||||
|     "eslint-plugin-you-dont-need-lodash-underscore": "6.12.0", | ||||
|     "eventemitter3": "1.2.0", | ||||
|     "exports-loader": "0.7.0", | ||||
|     "express": "4.13.1", | ||||
|     "file-saver": "2.0.5", | ||||
|     "git-rev-sync": "3.0.2", | ||||
|     "html2canvas": "1.4.1", | ||||
|     "imports-loader": "0.8.0", | ||||
|     "jasmine-core": "4.1.1", | ||||
|     "jasmine-core": "4.0.1", | ||||
|     "jsdoc": "3.5.5", | ||||
|     "karma": "6.3.20", | ||||
|     "karma": "6.3.18", | ||||
|     "karma-chrome-launcher": "3.1.1", | ||||
|     "karma-cli": "2.0.0", | ||||
|     "karma-coverage": "2.2.0", | ||||
|     "karma-coverage": "2.1.1", | ||||
|     "karma-coverage-istanbul-reporter": "3.0.3", | ||||
|     "karma-firefox-launcher": "2.1.2", | ||||
|     "karma-jasmine": "4.0.1", | ||||
| @@ -47,42 +48,42 @@ | ||||
|     "karma-sourcemap-loader": "0.3.8", | ||||
|     "karma-spec-reporter": "0.0.34", | ||||
|     "karma-webpack": "5.0.0", | ||||
|     "lighthouse": "9.6.1", | ||||
|     "lighthouse": "9.5.0", | ||||
|     "location-bar": "3.0.1", | ||||
|     "lodash": "4.17.21", | ||||
|     "mini-css-extract-plugin": "2.6.0", | ||||
|     "moment": "2.29.3", | ||||
|     "moment": "2.29.1", | ||||
|     "moment-duration-format": "2.3.2", | ||||
|     "moment-timezone": "0.5.34", | ||||
|     "node-bourbon": "4.2.3", | ||||
|     "painterro": "1.2.56", | ||||
|     "nyc":"15.1.0", | ||||
|     "plotly.js-basic-dist": "2.12.0", | ||||
|     "plotly.js-gl2d-dist": "2.12.0", | ||||
|     "plotly.js-basic-dist": "2.5.0", | ||||
|     "plotly.js-gl2d-dist": "2.5.0", | ||||
|     "printj": "1.3.1", | ||||
|     "request": "2.88.2", | ||||
|     "resolve-url-loader": "5.0.0", | ||||
|     "sass": "1.52.2", | ||||
|     "sass": "1.49.9", | ||||
|     "sass-loader": "12.6.0", | ||||
|     "sinon": "14.0.0", | ||||
|     "sinon": "13.0.1", | ||||
|     "style-loader": "^1.0.1", | ||||
|     "uuid": "8.3.2", | ||||
|     "uuid": "3.3.3", | ||||
|     "vue": "2.6.14", | ||||
|     "vue-eslint-parser": "9.0.2", | ||||
|     "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.3", | ||||
|     "webpack-dev-middleware": "5.3.1", | ||||
|     "webpack-hot-middleware": "2.25.1", | ||||
|     "webpack-merge": "5.8.0" | ||||
|     "webpack-merge": "5.8.0", | ||||
|     "zepto": "1.2.0" | ||||
|   }, | ||||
|   "scripts": { | ||||
|     "clean": "rm -rf ./dist ./node_modules ./package-lock.json", | ||||
|     "clean-test-lint": "npm run clean; npm install; npm run test; npm run lint", | ||||
|     "start": "node app.js", | ||||
|     "lint": "eslint example src e2e --ext .js,.vue openmct.js --max-warnings=0", | ||||
|     "lint:fix": "eslint example src e2e --ext .js,.vue openmct.js --fix", | ||||
|     "lint": "eslint example src --ext .js,.vue openmct.js", | ||||
|     "lint:fix": "eslint example src --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", | ||||
| @@ -91,23 +92,17 @@ | ||||
|     "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": "npx playwright test", | ||||
|     "test:e2e:ci": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome smoke branding default condition timeConductor clock exampleImagery persistence performance grandsearch notebook/tags", | ||||
|     "test:e2e:ci": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome smoke default condition timeConductor", | ||||
|     "test:e2e:local": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome", | ||||
|     "test:e2e:updatesnapshots": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @snapshot --update-snapshots", | ||||
|     "test:e2e:visual": "percy exec --config ./e2e/.percy.yml -- npx playwright test --config=e2e/playwright-visual.config.js", | ||||
|     "test:e2e: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: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", | ||||
|     "update-copyright-date": "npm run update-about-dialog-copyright && grep -lr --null --include=*.{js,scss,vue,ts,sh,html,md,frag} 'Copyright (c) 20' . | xargs -r0 perl -pi -e 's/Copyright\\s\\(c\\)\\s20\\d\\d\\-20\\d\\d/Copyright \\(c\\)\\ 2014\\-2022/gm'", | ||||
|     "otherdoc": "node docs/gendocs.js --in docs/src --out dist/docs --suppress-toc 'docs/src/index.md|docs/src/process/index.md'", | ||||
|     "docs": "npm run jsdoc ; npm run otherdoc", | ||||
|     "cov:e2e:report":"nyc report --reporter=lcovonly --report-dir=./coverage/e2e", | ||||
|     "cov:e2e:full:publish":"codecov --disable=gcov -f ./coverage/e2e/lcov.info -F e2e-full", | ||||
|     "cov:e2e:ci:publish":"codecov --disable=gcov -f ./coverage/e2e/lcov.info -F e2e-ci", | ||||
|     "cov:unit:publish":"codecov --disable=gcov -f ./coverage/unit/lcov.info -F unit", | ||||
|     "prepare": "npm run build:prod" | ||||
|   }, | ||||
|   "repository": { | ||||
|   | ||||
							
								
								
									
										269
									
								
								src/MCT.js
									
									
									
									
									
								
							
							
						
						
									
										269
									
								
								src/MCT.js
									
									
									
									
									
								
							| @@ -42,7 +42,6 @@ define([ | ||||
|     './plugins/duplicate/plugin', | ||||
|     './plugins/importFromJSONAction/plugin', | ||||
|     './plugins/exportAsJSONAction/plugin', | ||||
|     './ui/components/components', | ||||
|     'vue' | ||||
| ], function ( | ||||
|     EventEmitter, | ||||
| @@ -66,7 +65,6 @@ define([ | ||||
|     DuplicateActionPlugin, | ||||
|     ImportFromJSONAction, | ||||
|     ExportAsJSONAction, | ||||
|     components, | ||||
|     Vue | ||||
| ) { | ||||
|     /** | ||||
| @@ -96,170 +94,155 @@ define([ | ||||
|         }; | ||||
|  | ||||
|         this.destroy = this.destroy.bind(this); | ||||
|         [ | ||||
|             /** | ||||
|             * Tracks current selection state of the application. | ||||
|             * @private | ||||
|             */ | ||||
|             ['selection', () => new Selection(this)], | ||||
|         /** | ||||
|          * Tracks current selection state of the application. | ||||
|          * @private | ||||
|          */ | ||||
|         this.selection = new Selection(this); | ||||
|  | ||||
|             /** | ||||
|              * MCT's time conductor, which may be used to synchronize view contents | ||||
|              * for telemetry- or time-based views. | ||||
|              * @type {module:openmct.TimeConductor} | ||||
|              * @memberof module:openmct.MCT# | ||||
|              * @name conductor | ||||
|              */ | ||||
|             ['time', () => new api.TimeAPI(this)], | ||||
|         /** | ||||
|          * MCT's time conductor, which may be used to synchronize view contents | ||||
|          * for telemetry- or time-based views. | ||||
|          * @type {module:openmct.TimeConductor} | ||||
|          * @memberof module:openmct.MCT# | ||||
|          * @name conductor | ||||
|          */ | ||||
|         this.time = new api.TimeAPI(this); | ||||
|  | ||||
|             /** | ||||
|              * An interface for interacting with the composition of domain objects. | ||||
|              * The composition of a domain object is the list of other domain | ||||
|              * objects it "contains" (for instance, that should be displayed | ||||
|              * beneath it in the tree.) | ||||
|              * | ||||
|              * `composition` may be called as a function, in which case it acts | ||||
|              * as [`composition.get`]{@link module:openmct.CompositionAPI#get}. | ||||
|              * | ||||
|              * @type {module:openmct.CompositionAPI} | ||||
|              * @memberof module:openmct.MCT# | ||||
|              * @name composition | ||||
|              */ | ||||
|             ['composition', () => new api.CompositionAPI(this)], | ||||
|         /** | ||||
|          * An interface for interacting with the composition of domain objects. | ||||
|          * The composition of a domain object is the list of other domain | ||||
|          * objects it "contains" (for instance, that should be displayed | ||||
|          * beneath it in the tree.) | ||||
|          * | ||||
|          * `composition` may be called as a function, in which case it acts | ||||
|          * as [`composition.get`]{@link module:openmct.CompositionAPI#get}. | ||||
|          * | ||||
|          * @type {module:openmct.CompositionAPI} | ||||
|          * @memberof module:openmct.MCT# | ||||
|          * @name composition | ||||
|          */ | ||||
|         this.composition = new api.CompositionAPI(this); | ||||
|  | ||||
|             /** | ||||
|              * Registry for views of domain objects which should appear in the | ||||
|              * main viewing area. | ||||
|              * | ||||
|              * @type {module:openmct.ViewRegistry} | ||||
|              * @memberof module:openmct.MCT# | ||||
|              * @name objectViews | ||||
|              */ | ||||
|             ['objectViews', () => new ViewRegistry()], | ||||
|         /** | ||||
|          * Registry for views of domain objects which should appear in the | ||||
|          * main viewing area. | ||||
|          * | ||||
|          * @type {module:openmct.ViewRegistry} | ||||
|          * @memberof module:openmct.MCT# | ||||
|          * @name objectViews | ||||
|          */ | ||||
|         this.objectViews = new ViewRegistry(); | ||||
|  | ||||
|             /** | ||||
|              * Registry for views which should appear in the Inspector area. | ||||
|              * These views will be chosen based on the selection state. | ||||
|              * | ||||
|              * @type {module:openmct.InspectorViewRegistry} | ||||
|              * @memberof module:openmct.MCT# | ||||
|              * @name inspectorViews | ||||
|              */ | ||||
|             ['inspectorViews', () => new InspectorViewRegistry()], | ||||
|         /** | ||||
|          * Registry for views which should appear in the Inspector area. | ||||
|          * These views will be chosen based on the selection state. | ||||
|          * | ||||
|          * @type {module:openmct.InspectorViewRegistry} | ||||
|          * @memberof module:openmct.MCT# | ||||
|          * @name inspectorViews | ||||
|          */ | ||||
|         this.inspectorViews = new InspectorViewRegistry(); | ||||
|  | ||||
|             /** | ||||
|              * Registry for views which should appear in Edit Properties | ||||
|              * dialogs, and similar user interface elements used for | ||||
|              * modifying domain objects external to its regular views. | ||||
|              * | ||||
|              * @type {module:openmct.ViewRegistry} | ||||
|              * @memberof module:openmct.MCT# | ||||
|              * @name propertyEditors | ||||
|              */ | ||||
|             ['propertyEditors', () => new ViewRegistry()], | ||||
|         /** | ||||
|          * Registry for views which should appear in Edit Properties | ||||
|          * dialogs, and similar user interface elements used for | ||||
|          * modifying domain objects external to its regular views. | ||||
|          * | ||||
|          * @type {module:openmct.ViewRegistry} | ||||
|          * @memberof module:openmct.MCT# | ||||
|          * @name propertyEditors | ||||
|          */ | ||||
|         this.propertyEditors = new ViewRegistry(); | ||||
|  | ||||
|             /** | ||||
|              * Registry for views which should appear in the toolbar area while | ||||
|              * editing. These views will be chosen based on the selection state. | ||||
|              * | ||||
|              * @type {module:openmct.ToolbarRegistry} | ||||
|              * @memberof module:openmct.MCT# | ||||
|              * @name toolbars | ||||
|              */ | ||||
|             ['toolbars', () => new ToolbarRegistry()], | ||||
|         /** | ||||
|          * Registry for views which should appear in the status indicator area. | ||||
|          * @type {module:openmct.ViewRegistry} | ||||
|          * @memberof module:openmct.MCT# | ||||
|          * @name indicators | ||||
|          */ | ||||
|         this.indicators = new ViewRegistry(); | ||||
|  | ||||
|             /** | ||||
|              * Registry for domain object types which may exist within this | ||||
|              * instance of Open MCT. | ||||
|              * | ||||
|              * @type {module:openmct.TypeRegistry} | ||||
|              * @memberof module:openmct.MCT# | ||||
|              * @name types | ||||
|              */ | ||||
|             ['types', () => new api.TypeRegistry()], | ||||
|         /** | ||||
|          * Registry for views which should appear in the toolbar area while | ||||
|          * editing. These views will be chosen based on the selection state. | ||||
|          * | ||||
|          * @type {module:openmct.ToolbarRegistry} | ||||
|          * @memberof module:openmct.MCT# | ||||
|          * @name toolbars | ||||
|          */ | ||||
|         this.toolbars = new ToolbarRegistry(); | ||||
|  | ||||
|             /** | ||||
|              * An interface for interacting with domain objects and the domain | ||||
|              * object hierarchy. | ||||
|              * | ||||
|              * @type {module:openmct.ObjectAPI} | ||||
|              * @memberof module:openmct.MCT# | ||||
|              * @name objects | ||||
|              */ | ||||
|             ['objects', () => new api.ObjectAPI.default(this.types, this)], | ||||
|         /** | ||||
|          * Registry for domain object types which may exist within this | ||||
|          * instance of Open MCT. | ||||
|          * | ||||
|          * @type {module:openmct.TypeRegistry} | ||||
|          * @memberof module:openmct.MCT# | ||||
|          * @name types | ||||
|          */ | ||||
|         this.types = new api.TypeRegistry(); | ||||
|  | ||||
|             /** | ||||
|              * An interface for retrieving and interpreting telemetry data associated | ||||
|              * with a domain object. | ||||
|              * | ||||
|              * @type {module:openmct.TelemetryAPI} | ||||
|              * @memberof module:openmct.MCT# | ||||
|              * @name telemetry | ||||
|              */ | ||||
|             ['telemetry', () => new api.TelemetryAPI.default(this)], | ||||
|         /** | ||||
|          * An interface for interacting with domain objects and the domain | ||||
|          * object hierarchy. | ||||
|          * | ||||
|          * @type {module:openmct.ObjectAPI} | ||||
|          * @memberof module:openmct.MCT# | ||||
|          * @name objects | ||||
|          */ | ||||
|         this.objects = new api.ObjectAPI.default(this.types, this); | ||||
|  | ||||
|             /** | ||||
|              * An interface for creating new indicators and changing them dynamically. | ||||
|              * | ||||
|              * @type {module:openmct.IndicatorAPI} | ||||
|              * @memberof module:openmct.MCT# | ||||
|              * @name indicators | ||||
|              */ | ||||
|             ['indicators', () => new api.IndicatorAPI(this)], | ||||
|         /** | ||||
|          * An interface for retrieving and interpreting telemetry data associated | ||||
|          * with a domain object. | ||||
|          * | ||||
|          * @type {module:openmct.TelemetryAPI} | ||||
|          * @memberof module:openmct.MCT# | ||||
|          * @name telemetry | ||||
|          */ | ||||
|         this.telemetry = new api.TelemetryAPI(this); | ||||
|  | ||||
|             /** | ||||
|              * MCT's user awareness management, to enable user and | ||||
|              * role specific functionality. | ||||
|              * @type {module:openmct.UserAPI} | ||||
|              * @memberof module:openmct.MCT# | ||||
|              * @name user | ||||
|              */ | ||||
|             ['user', () => new api.UserAPI(this)], | ||||
|         /** | ||||
|          * An interface for creating new indicators and changing them dynamically. | ||||
|          * | ||||
|          * @type {module:openmct.IndicatorAPI} | ||||
|          * @memberof module:openmct.MCT# | ||||
|          * @name indicators | ||||
|          */ | ||||
|         this.indicators = new api.IndicatorAPI(this); | ||||
|  | ||||
|             ['notifications', () => new api.NotificationAPI()], | ||||
|         /** | ||||
|          * MCT's user awareness management, to enable user and | ||||
|          * role specific functionality. | ||||
|          * @type {module:openmct.UserAPI} | ||||
|          * @memberof module:openmct.MCT# | ||||
|          * @name user | ||||
|          */ | ||||
|         this.user = new api.UserAPI(this); | ||||
|  | ||||
|             ['editor', () => new api.EditorAPI.default(this)], | ||||
|         this.notifications = new api.NotificationAPI(); | ||||
|  | ||||
|             ['overlays', () => new OverlayAPI.default()], | ||||
|         this.editor = new api.EditorAPI.default(this); | ||||
|  | ||||
|             ['menus', () => new api.MenuAPI(this)], | ||||
|         this.overlays = new OverlayAPI.default(); | ||||
|  | ||||
|             ['actions', () => new api.ActionsAPI(this)], | ||||
|         this.menus = new api.MenuAPI(this); | ||||
|  | ||||
|             ['status', () => new api.StatusAPI(this)], | ||||
|         this.actions = new api.ActionsAPI(this); | ||||
|  | ||||
|             ['priority', () => api.PriorityAPI], | ||||
|         this.status = new api.StatusAPI(this); | ||||
|  | ||||
|             ['router', () => new ApplicationRouter(this)], | ||||
|         this.priority = api.PriorityAPI; | ||||
|  | ||||
|             ['faults', () => new api.FaultManagementAPI.default(this)], | ||||
|         this.router = new ApplicationRouter(this); | ||||
|         this.forms = new api.FormsAPI.default(this); | ||||
|  | ||||
|             ['forms', () => new api.FormsAPI.default(this)], | ||||
|  | ||||
|             ['branding', () => BrandingAPI.default], | ||||
|  | ||||
|             /** | ||||
|              * MCT's annotation API that enables | ||||
|              * human-created comments and categorization linked to data products | ||||
|              * @type {module:openmct.AnnotationAPI} | ||||
|              * @memberof module:openmct.MCT# | ||||
|              * @name annotation | ||||
|              */ | ||||
|             ['annotation', () => new api.AnnotationAPI(this)] | ||||
|         ].forEach(apiEntry => { | ||||
|             const apiName = apiEntry[0]; | ||||
|             const apiObject = apiEntry[1](); | ||||
|  | ||||
|             Object.defineProperty(this, apiName, { | ||||
|                 value: apiObject, | ||||
|                 enumerable: false, | ||||
|                 configurable: false, | ||||
|                 writable: true | ||||
|             }); | ||||
|         }); | ||||
|         this.branding = BrandingAPI.default; | ||||
|  | ||||
|         // 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()); | ||||
| @@ -287,7 +270,6 @@ define([ | ||||
|         this.install(this.plugins.ObjectInterceptors()); | ||||
|         this.install(this.plugins.DeviceClassifier()); | ||||
|         this.install(this.plugins.UserIndicator()); | ||||
|         this.install(this.plugins.Gauge()); | ||||
|     } | ||||
|  | ||||
|     MCT.prototype = Object.create(EventEmitter.prototype); | ||||
| @@ -396,7 +378,6 @@ define([ | ||||
|     }; | ||||
|  | ||||
|     MCT.prototype.plugins = plugins; | ||||
|     MCT.prototype.components = components.default; | ||||
|  | ||||
|     return MCT; | ||||
| }); | ||||
|   | ||||
| @@ -85,6 +85,8 @@ class ActionCollection extends EventEmitter { | ||||
|     } | ||||
|  | ||||
|     destroy() { | ||||
|         super.removeAllListeners(); | ||||
|  | ||||
|         if (!this.skipEnvironmentObservers) { | ||||
|             this.objectUnsubscribes.forEach(unsubscribe => { | ||||
|                 unsubscribe(); | ||||
| @@ -94,7 +96,6 @@ class ActionCollection extends EventEmitter { | ||||
|         } | ||||
|  | ||||
|         this.emit('destroy', this.view); | ||||
|         this.removeAllListeners(); | ||||
|     } | ||||
|  | ||||
|     getVisibleActions() { | ||||
|   | ||||
| @@ -1,277 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import { v4 as uuid } from 'uuid'; | ||||
| import EventEmitter from 'EventEmitter'; | ||||
|  | ||||
| /** | ||||
|  * @readonly | ||||
|  * @enum {String} AnnotationType | ||||
|  * @property {String} NOTEBOOK The notebook annotation type | ||||
|  * @property {String} GEOSPATIAL The geospatial annotation type | ||||
|  * @property {String} PIXEL_SPATIAL The pixel-spatial annotation type | ||||
|  * @property {String} TEMPORAL The temporal annotation type | ||||
|  * @property {String} PLOT_SPATIAL The plot-spatial annotation type | ||||
|  */ | ||||
| const ANNOTATION_TYPES = Object.freeze({ | ||||
|     NOTEBOOK: 'NOTEBOOK', | ||||
|     GEOSPATIAL: 'GEOSPATIAL', | ||||
|     PIXEL_SPATIAL: 'PIXEL_SPATIAL', | ||||
|     TEMPORAL: 'TEMPORAL', | ||||
|     PLOT_SPATIAL: 'PLOT_SPATIAL' | ||||
| }); | ||||
|  | ||||
| /** | ||||
|  * @typedef {Object} Tag | ||||
|  * @property {String} key a unique identifier for the tag | ||||
|  * @property {String} backgroundColor eg. "#cc0000" | ||||
|  * @property {String} foregroundColor eg. "#ffffff" | ||||
|  */ | ||||
| export default class AnnotationAPI extends EventEmitter { | ||||
|     constructor(openmct) { | ||||
|         super(); | ||||
|         this.openmct = openmct; | ||||
|         this.availableTags = {}; | ||||
|  | ||||
|         this.ANNOTATION_TYPES = ANNOTATION_TYPES; | ||||
|  | ||||
|         this.openmct.types.addType('annotation', { | ||||
|             name: 'Annotation', | ||||
|             description: 'A user created note or comment about time ranges, pixel space, and geospatial features.', | ||||
|             creatable: false, | ||||
|             cssClass: 'icon-notebook', | ||||
|             initialize: function (domainObject) { | ||||
|                 domainObject.targets = domainObject.targets || {}; | ||||
|                 domainObject.originalContextPath = domainObject.originalContextPath || ''; | ||||
|                 domainObject.tags = domainObject.tags || []; | ||||
|                 domainObject.contentText = domainObject.contentText || ''; | ||||
|                 domainObject.annotationType = domainObject.annotationType || 'plotspatial'; | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|     * Create the a generic annotation | ||||
|     * @typedef {Object} CreateAnnotationOptions | ||||
|     * @property {String} name a name for the new parameter | ||||
|     * @property {import('../objects/ObjectAPI').DomainObject} domainObject the domain object to create | ||||
|     * @property {ANNOTATION_TYPES} annotationType the type of annotation to create | ||||
|     * @property {Tag[]} tags | ||||
|     * @property {String} contentText | ||||
|     * @property {import('../objects/ObjectAPI').Identifier[]} targets | ||||
|     */ | ||||
|     /** | ||||
|     * @method create | ||||
|     * @param {CreateAnnotationOptions} options | ||||
|     * @returns {Promise<import('../objects/ObjectAPI').DomainObject>} a promise which will resolve when the domain object | ||||
|     *          has been created, or be rejected if it cannot be saved | ||||
|     */ | ||||
|     async create({name, domainObject, annotationType, tags, contentText, targets}) { | ||||
|         if (!Object.keys(this.ANNOTATION_TYPES).includes(annotationType)) { | ||||
|             throw new Error(`Unknown annotation type: ${annotationType}`); | ||||
|         } | ||||
|  | ||||
|         if (!Object.keys(targets).length) { | ||||
|             throw new Error(`At least one target is required to create an annotation`); | ||||
|         } | ||||
|  | ||||
|         const domainObjectKeyString = this.openmct.objects.makeKeyString(domainObject.identifier); | ||||
|         const originalPathObjects = await this.openmct.objects.getOriginalPath(domainObjectKeyString); | ||||
|         const originalContextPath = this.openmct.objects.getRelativePath(originalPathObjects); | ||||
|         const namespace = domainObject.identifier.namespace; | ||||
|         const type = 'annotation'; | ||||
|         const typeDefinition = this.openmct.types.get(type); | ||||
|         const definition = typeDefinition.definition; | ||||
|  | ||||
|         const createdObject = { | ||||
|             name, | ||||
|             type, | ||||
|             identifier: { | ||||
|                 key: uuid(), | ||||
|                 namespace | ||||
|             }, | ||||
|             tags, | ||||
|             annotationType, | ||||
|             contentText, | ||||
|             originalContextPath | ||||
|         }; | ||||
|  | ||||
|         if (definition.initialize) { | ||||
|             definition.initialize(createdObject); | ||||
|         } | ||||
|  | ||||
|         createdObject.targets = targets; | ||||
|         createdObject.originalContextPath = originalContextPath; | ||||
|  | ||||
|         const success = await this.openmct.objects.save(createdObject); | ||||
|         if (success) { | ||||
|             this.emit('annotationCreated', createdObject); | ||||
|  | ||||
|             return createdObject; | ||||
|         } else { | ||||
|             throw new Error('Failed to create object'); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     defineTag(tagKey, tagsDefinition) { | ||||
|         this.availableTags[tagKey] = tagsDefinition; | ||||
|     } | ||||
|  | ||||
|     getAvailableTags() { | ||||
|         if (this.availableTags) { | ||||
|             const rearrangedToArray = Object.keys(this.availableTags).map(tagKey => { | ||||
|                 return { | ||||
|                     id: tagKey, | ||||
|                     ...this.availableTags[tagKey] | ||||
|                 }; | ||||
|             }); | ||||
|  | ||||
|             return rearrangedToArray; | ||||
|         } else { | ||||
|             return []; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     async getAnnotation(query, searchType) { | ||||
|         let foundAnnotation = null; | ||||
|  | ||||
|         const searchResults = (await Promise.all(this.openmct.objects.search(query, null, searchType))).flat(); | ||||
|         if (searchResults) { | ||||
|             foundAnnotation = searchResults[0]; | ||||
|         } | ||||
|  | ||||
|         return foundAnnotation; | ||||
|     } | ||||
|  | ||||
|     async addAnnotationTag(existingAnnotation, targetDomainObject, targetSpecificDetails, annotationType, tag) { | ||||
|         if (!existingAnnotation) { | ||||
|             const targets = {}; | ||||
|             const targetKeyString = this.openmct.objects.makeKeyString(targetDomainObject.identifier); | ||||
|             targets[targetKeyString] = targetSpecificDetails; | ||||
|             const contentText = `${annotationType} tag`; | ||||
|             const annotationCreationArguments = { | ||||
|                 name: contentText, | ||||
|                 domainObject: targetDomainObject, | ||||
|                 annotationType, | ||||
|                 tags: [tag], | ||||
|                 contentText, | ||||
|                 targets | ||||
|             }; | ||||
|             const newAnnotation = await this.create(annotationCreationArguments); | ||||
|  | ||||
|             return newAnnotation; | ||||
|         } else { | ||||
|             const tagArray = [tag, ...existingAnnotation.tags]; | ||||
|             this.openmct.objects.mutate(existingAnnotation, 'tags', tagArray); | ||||
|  | ||||
|             return existingAnnotation; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     removeAnnotationTag(existingAnnotation, tagToRemove) { | ||||
|         if (existingAnnotation && existingAnnotation.tags.includes(tagToRemove)) { | ||||
|             const cleanedArray = existingAnnotation.tags.filter(extantTag => extantTag !== tagToRemove); | ||||
|             this.openmct.objects.mutate(existingAnnotation, 'tags', cleanedArray); | ||||
|         } else { | ||||
|             throw new Error(`Asked to remove tag (${tagToRemove}) that doesn't exist`, existingAnnotation); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     removeAnnotationTags(existingAnnotation) { | ||||
|         // just removes tags on the annotation as we can't really delete objects | ||||
|         if (existingAnnotation && existingAnnotation.tags) { | ||||
|             this.openmct.objects.mutate(existingAnnotation, 'tags', []); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     #getMatchingTags(query) { | ||||
|         if (!query) { | ||||
|             return []; | ||||
|         } | ||||
|  | ||||
|         const matchingTags = Object.keys(this.availableTags).filter(tagKey => { | ||||
|             if (this.availableTags[tagKey] && this.availableTags[tagKey].label) { | ||||
|                 return this.availableTags[tagKey].label.toLowerCase().includes(query.toLowerCase()); | ||||
|             } | ||||
|  | ||||
|             return false; | ||||
|         }); | ||||
|  | ||||
|         return matchingTags; | ||||
|     } | ||||
|  | ||||
|     #addTagMetaInformationToResults(results, matchingTagKeys) { | ||||
|         const tagsAddedToResults = results.map(result => { | ||||
|             const fullTagModels = result.tags.map(tagKey => { | ||||
|                 const tagModel = this.availableTags[tagKey]; | ||||
|                 tagModel.tagID = tagKey; | ||||
|  | ||||
|                 return tagModel; | ||||
|             }); | ||||
|  | ||||
|             return { | ||||
|                 fullTagModels, | ||||
|                 matchingTagKeys, | ||||
|                 ...result | ||||
|             }; | ||||
|         }); | ||||
|  | ||||
|         return tagsAddedToResults; | ||||
|     } | ||||
|  | ||||
|     async #addTargetModelsToResults(results) { | ||||
|         const modelAddedToResults = await Promise.all(results.map(async result => { | ||||
|             const targetModels = await Promise.all(Object.keys(result.targets).map(async (targetID) => { | ||||
|                 const targetModel = await this.openmct.objects.get(targetID); | ||||
|                 const targetKeyString = this.openmct.objects.makeKeyString(targetModel.identifier); | ||||
|                 const originalPathObjects = await this.openmct.objects.getOriginalPath(targetKeyString); | ||||
|  | ||||
|                 return { | ||||
|                     originalPath: originalPathObjects, | ||||
|                     ...targetModel | ||||
|                 }; | ||||
|             })); | ||||
|  | ||||
|             return { | ||||
|                 targetModels, | ||||
|                 ...result | ||||
|             }; | ||||
|         })); | ||||
|  | ||||
|         return modelAddedToResults; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|     * @method searchForTags | ||||
|     * @param {String} query A query to match against tags. E.g., "dr" will match the tags "drilling" and "driving" | ||||
|     * @param {Object} abortController An optional abort method to stop the query | ||||
|     * @returns {Promise} returns a model of matching tags with their target domain objects attached | ||||
|     */ | ||||
|     async searchForTags(query, abortController) { | ||||
|         const matchingTagKeys = this.#getMatchingTags(query); | ||||
|         const searchResults = (await Promise.all(this.openmct.objects.search(matchingTagKeys, abortController, this.openmct.objects.SEARCH_TYPES.TAGS))).flat(); | ||||
|         const appliedTagSearchResults = this.#addTagMetaInformationToResults(searchResults, matchingTagKeys); | ||||
|         const appliedTargetsModels = await this.#addTargetModelsToResults(appliedTagSearchResults); | ||||
|  | ||||
|         return appliedTargetsModels; | ||||
|     } | ||||
| } | ||||
| @@ -1,176 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import { createOpenMct, resetApplicationState } from '../../utils/testing'; | ||||
| import ExampleTagsPlugin from "../../../example/exampleTags/plugin"; | ||||
|  | ||||
| describe("The Annotation API", () => { | ||||
|     let openmct; | ||||
|     let mockObjectProvider; | ||||
|     let mockDomainObject; | ||||
|     let mockAnnotationObject; | ||||
|  | ||||
|     beforeEach((done) => { | ||||
|         openmct = createOpenMct(); | ||||
|         openmct.install(new ExampleTagsPlugin()); | ||||
|         const availableTags = openmct.annotation.getAvailableTags(); | ||||
|         mockDomainObject = { | ||||
|             type: 'notebook', | ||||
|             name: 'fooRabbitNotebook', | ||||
|             identifier: { | ||||
|                 key: 'some-object', | ||||
|                 namespace: 'fooNameSpace' | ||||
|             } | ||||
|         }; | ||||
|         mockAnnotationObject = { | ||||
|             type: 'annotation', | ||||
|             name: 'Some Notebook Annotation', | ||||
|             annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, | ||||
|             tags: [availableTags[0].id, availableTags[1].id], | ||||
|             identifier: { | ||||
|                 key: 'anAnnotationKey', | ||||
|                 namespace: 'fooNameSpace' | ||||
|             }, | ||||
|             targets: { | ||||
|                 'fooNameSpace:some-object': { | ||||
|                     entryId: 'fooBarEntry' | ||||
|                 } | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         mockObjectProvider = jasmine.createSpyObj("mock provider", [ | ||||
|             "create", | ||||
|             "update", | ||||
|             "get" | ||||
|         ]); | ||||
|         // eslint-disable-next-line require-await | ||||
|         mockObjectProvider.get = async (identifier) => { | ||||
|             if (identifier.key === mockDomainObject.identifier.key) { | ||||
|                 return mockDomainObject; | ||||
|             } else if (identifier.key === mockAnnotationObject.identifier.key) { | ||||
|                 return mockAnnotationObject; | ||||
|             } else { | ||||
|                 return null; | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         mockObjectProvider.create.and.returnValue(Promise.resolve(true)); | ||||
|         mockObjectProvider.update.and.returnValue(Promise.resolve(true)); | ||||
|  | ||||
|         openmct.objects.addProvider('fooNameSpace', mockObjectProvider); | ||||
|         openmct.on('start', done); | ||||
|         openmct.startHeadless(); | ||||
|     }); | ||||
|     afterEach(async () => { | ||||
|         openmct.objects.providers = {}; | ||||
|         await resetApplicationState(openmct); | ||||
|     }); | ||||
|     it("is defined", () => { | ||||
|         expect(openmct.annotation).toBeDefined(); | ||||
|     }); | ||||
|  | ||||
|     describe("Creation", () => { | ||||
|         it("can create annotations", async () => { | ||||
|             const annotationCreationArguments = { | ||||
|                 name: 'Test Annotation', | ||||
|                 domainObject: mockDomainObject, | ||||
|                 annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, | ||||
|                 tags: ['sometag'], | ||||
|                 contentText: "fooContext", | ||||
|                 targets: {'fooTarget': {}} | ||||
|             }; | ||||
|             const annotationObject = await openmct.annotation.create(annotationCreationArguments); | ||||
|             expect(annotationObject).toBeDefined(); | ||||
|             expect(annotationObject.type).toEqual('annotation'); | ||||
|         }); | ||||
|         it("fails if annotation is an unknown type", async () => { | ||||
|             try { | ||||
|                 await openmct.annotation.create('Garbage Annotation', mockDomainObject, 'garbageAnnotation', ['sometag'], "fooContext", {'fooTarget': {}}); | ||||
|             } catch (error) { | ||||
|                 expect(error).toBeDefined(); | ||||
|             } | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe("Tagging", () => { | ||||
|         it("can create a tag", async () => { | ||||
|             const annotationObject = await openmct.annotation.addAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag'); | ||||
|             expect(annotationObject).toBeDefined(); | ||||
|             expect(annotationObject.type).toEqual('annotation'); | ||||
|             expect(annotationObject.tags).toContain('aWonderfulTag'); | ||||
|         }); | ||||
|         it("can delete a tag", async () => { | ||||
|             const originalAnnotationObject = await openmct.annotation.addAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag'); | ||||
|             const annotationObject = await openmct.annotation.addAnnotationTag(originalAnnotationObject, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'anotherTagToRemove'); | ||||
|             expect(annotationObject).toBeDefined(); | ||||
|             openmct.annotation.removeAnnotationTag(annotationObject, 'anotherTagToRemove'); | ||||
|             expect(annotationObject.tags).toEqual(['aWonderfulTag']); | ||||
|             openmct.annotation.removeAnnotationTag(annotationObject, 'aWonderfulTag'); | ||||
|             expect(annotationObject.tags).toEqual([]); | ||||
|         }); | ||||
|         it("throws an error if deleting non-existent tag", async () => { | ||||
|             const annotationObject = await openmct.annotation.addAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag'); | ||||
|             expect(annotationObject).toBeDefined(); | ||||
|             expect(() => { | ||||
|                 openmct.annotation.removeAnnotationTag(annotationObject, 'ThisTagShouldNotExist'); | ||||
|             }).toThrow(); | ||||
|         }); | ||||
|         it("can remove all tags", async () => { | ||||
|             const annotationObject = await openmct.annotation.addAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag'); | ||||
|             expect(annotationObject).toBeDefined(); | ||||
|             expect(() => { | ||||
|                 openmct.annotation.removeAnnotationTags(annotationObject); | ||||
|             }).not.toThrow(); | ||||
|             expect(annotationObject.tags).toEqual([]); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe("Search", () => { | ||||
|         let sharedWorkerToRestore; | ||||
|         beforeEach(async () => { | ||||
|             // use local worker | ||||
|             sharedWorkerToRestore = openmct.objects.inMemorySearchProvider.worker; | ||||
|             openmct.objects.inMemorySearchProvider.worker = null; | ||||
|             await openmct.objects.inMemorySearchProvider.index(mockDomainObject); | ||||
|             await openmct.objects.inMemorySearchProvider.index(mockAnnotationObject); | ||||
|         }); | ||||
|         afterEach(() => { | ||||
|             openmct.objects.inMemorySearchProvider.worker = sharedWorkerToRestore; | ||||
|         }); | ||||
|         it("can search for tags", async () => { | ||||
|             const results = await openmct.annotation.searchForTags('S'); | ||||
|             expect(results).toBeDefined(); | ||||
|             expect(results.length).toEqual(1); | ||||
|         }); | ||||
|         it("can get notebook annotations", async () => { | ||||
|             const targetKeyString = openmct.objects.makeKeyString(mockDomainObject.identifier); | ||||
|             const query = { | ||||
|                 targetKeyString, | ||||
|                 entryId: 'fooBarEntry' | ||||
|             }; | ||||
|  | ||||
|             const results = await openmct.annotation.getAnnotation(query, openmct.objects.SEARCH_TYPES.NOTEBOOK_ANNOTATIONS); | ||||
|             expect(results).toBeDefined(); | ||||
|             expect(results.tags.length).toEqual(2); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
| @@ -24,7 +24,6 @@ define([ | ||||
|     './actions/ActionsAPI', | ||||
|     './composition/CompositionAPI', | ||||
|     './Editor', | ||||
|     './faultmanagement/FaultManagementAPI', | ||||
|     './forms/FormsAPI', | ||||
|     './indicators/IndicatorAPI', | ||||
|     './menu/MenuAPI', | ||||
| @@ -35,13 +34,11 @@ define([ | ||||
|     './telemetry/TelemetryAPI', | ||||
|     './time/TimeAPI', | ||||
|     './types/TypeRegistry', | ||||
|     './user/UserAPI', | ||||
|     './annotation/AnnotationAPI' | ||||
|     './user/UserAPI' | ||||
| ], function ( | ||||
|     ActionsAPI, | ||||
|     CompositionAPI, | ||||
|     EditorAPI, | ||||
|     FaultManagementAPI, | ||||
|     FormsAPI, | ||||
|     IndicatorAPI, | ||||
|     MenuAPI, | ||||
| @@ -52,16 +49,14 @@ define([ | ||||
|     TelemetryAPI, | ||||
|     TimeAPI, | ||||
|     TypeRegistry, | ||||
|     UserAPI, | ||||
|     AnnotationAPI | ||||
|     UserAPI | ||||
| ) { | ||||
|     return { | ||||
|         ActionsAPI: ActionsAPI.default, | ||||
|         CompositionAPI: CompositionAPI, | ||||
|         EditorAPI: EditorAPI, | ||||
|         FaultManagementAPI: FaultManagementAPI, | ||||
|         FormsAPI: FormsAPI, | ||||
|         IndicatorAPI: IndicatorAPI.default, | ||||
|         IndicatorAPI: IndicatorAPI, | ||||
|         MenuAPI: MenuAPI.default, | ||||
|         NotificationAPI: NotificationAPI.default, | ||||
|         ObjectAPI: ObjectAPI, | ||||
| @@ -70,7 +65,6 @@ define([ | ||||
|         TelemetryAPI: TelemetryAPI, | ||||
|         TimeAPI: TimeAPI.default, | ||||
|         TypeRegistry: TypeRegistry, | ||||
|         UserAPI: UserAPI.default, | ||||
|         AnnotationAPI: AnnotationAPI.default | ||||
|         UserAPI: UserAPI.default | ||||
|     }; | ||||
| }); | ||||
|   | ||||
| @@ -1,106 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| export default class FaultManagementAPI { | ||||
|     constructor(openmct) { | ||||
|         this.openmct = openmct; | ||||
|     } | ||||
|  | ||||
|     addProvider(provider) { | ||||
|         this.provider = provider; | ||||
|     } | ||||
|  | ||||
|     supportsActions() { | ||||
|         return this.provider?.acknowledgeFault !== undefined && this.provider?.shelveFault !== undefined; | ||||
|     } | ||||
|  | ||||
|     request(domainObject) { | ||||
|         if (!this.provider?.supportsRequest(domainObject)) { | ||||
|             return Promise.reject(); | ||||
|         } | ||||
|  | ||||
|         return this.provider.request(domainObject); | ||||
|     } | ||||
|  | ||||
|     subscribe(domainObject, callback) { | ||||
|         if (!this.provider?.supportsSubscribe(domainObject)) { | ||||
|             return Promise.reject(); | ||||
|         } | ||||
|  | ||||
|         return this.provider.subscribe(domainObject, callback); | ||||
|     } | ||||
|  | ||||
|     acknowledgeFault(fault, ackData) { | ||||
|         return this.provider.acknowledgeFault(fault, ackData); | ||||
|     } | ||||
|  | ||||
|     shelveFault(fault, shelveData) { | ||||
|         return this.provider.shelveFault(fault, shelveData); | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** @typedef {object} Fault | ||||
|  * @property {string} type | ||||
|  * @property {object} fault | ||||
|  * @property {boolean} fault.acknowledged | ||||
|  * @property {object} fault.currentValueInfo | ||||
|  * @property {number} fault.currentValueInfo.value | ||||
|  * @property {string} fault.currentValueInfo.rangeCondition | ||||
|  * @property {string} fault.currentValueInfo.monitoringResult | ||||
|  * @property {string} fault.id | ||||
|  * @property {string} fault.name | ||||
|  * @property {string} fault.namespace | ||||
|  * @property {number} fault.seqNum | ||||
|  * @property {string} fault.severity | ||||
|  * @property {boolean} fault.shelved | ||||
|  * @property {string} fault.shortDescription | ||||
|  * @property {string} fault.triggerTime | ||||
|  * @property {object} fault.triggerValueInfo | ||||
|  * @property {number} fault.triggerValueInfo.value | ||||
|  * @property {string} fault.triggerValueInfo.rangeCondition | ||||
|  * @property {string} fault.triggerValueInfo.monitoringResult | ||||
|  * @example | ||||
|  *  { | ||||
|  *     "type": "", | ||||
|  *     "fault": { | ||||
|  *         "acknowledged": true, | ||||
|  *         "currentValueInfo": { | ||||
|  *             "value": 0, | ||||
|  *             "rangeCondition": "", | ||||
|  *             "monitoringResult": "" | ||||
|  *         }, | ||||
|  *         "id": "", | ||||
|  *         "name": "", | ||||
|  *         "namespace": "", | ||||
|  *         "seqNum": 0, | ||||
|  *         "severity": "", | ||||
|  *         "shelved": true, | ||||
|  *         "shortDescription": "", | ||||
|  *         "triggerTime": "", | ||||
|  *         "triggerValueInfo": { | ||||
|  *             "value": 0, | ||||
|  *             "rangeCondition": "", | ||||
|  *             "monitoringResult": "" | ||||
|  *         } | ||||
|  *     } | ||||
|  * } | ||||
|  */ | ||||
| @@ -1,144 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * License); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an AS IS BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import { | ||||
|     createOpenMct, | ||||
|     resetApplicationState | ||||
| } from '../../utils/testing'; | ||||
|  | ||||
| const faultName = 'super duper fault'; | ||||
| const aFault = { | ||||
|     type: '', | ||||
|     fault: { | ||||
|         acknowledged: true, | ||||
|         currentValueInfo: { | ||||
|             value: 0, | ||||
|             rangeCondition: '', | ||||
|             monitoringResult: '' | ||||
|         }, | ||||
|         id: '', | ||||
|         name: faultName, | ||||
|         namespace: '', | ||||
|         seqNum: 0, | ||||
|         severity: '', | ||||
|         shelved: true, | ||||
|         shortDescription: '', | ||||
|         triggerTime: '', | ||||
|         triggerValueInfo: { | ||||
|             value: 0, | ||||
|             rangeCondition: '', | ||||
|             monitoringResult: '' | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| const faultDomainObject = { | ||||
|     name: 'it is not your fault', | ||||
|     type: 'faultManagement', | ||||
|     identifier: { | ||||
|         key: 'nobodies', | ||||
|         namespace: 'fault' | ||||
|     } | ||||
| }; | ||||
| const aComment = 'THIS is my fault.'; | ||||
| const faultManagementProvider = { | ||||
|     request() { | ||||
|         return Promise.resolve([aFault]); | ||||
|     }, | ||||
|     subscribe(domainObject, callback) { | ||||
|         return () => {}; | ||||
|     }, | ||||
|     supportsRequest(domainObject) { | ||||
|         return domainObject.type === 'faultManagement'; | ||||
|     }, | ||||
|     supportsSubscribe(domainObject) { | ||||
|         return domainObject.type === 'faultManagement'; | ||||
|     }, | ||||
|     acknowledgeFault(fault, { comment = '' }) { | ||||
|         return Promise.resolve({ | ||||
|             success: true | ||||
|         }); | ||||
|     }, | ||||
|     shelveFault(fault, shelveData) { | ||||
|         return Promise.resolve({ | ||||
|             success: true | ||||
|         }); | ||||
|     } | ||||
| }; | ||||
|  | ||||
| describe('The Fault Management API', () => { | ||||
|     let openmct; | ||||
|  | ||||
|     beforeEach(() => { | ||||
|         openmct = createOpenMct(); | ||||
|         openmct.install(openmct.plugins.FaultManagement()); | ||||
|         // openmct.install(openmct.plugins.example.ExampleFaultSource()); | ||||
|         openmct.faults.addProvider(faultManagementProvider); | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     it('allows you to request a fault', async () => { | ||||
|         spyOn(faultManagementProvider, 'supportsRequest').and.callThrough(); | ||||
|  | ||||
|         let faultResponse = await openmct.faults.request(faultDomainObject); | ||||
|  | ||||
|         expect(faultManagementProvider.supportsRequest).toHaveBeenCalledWith(faultDomainObject); | ||||
|         expect(faultResponse[0].fault.name).toEqual(faultName); | ||||
|     }); | ||||
|  | ||||
|     it('allows you to subscribe to a fault', () => { | ||||
|         spyOn(faultManagementProvider, 'subscribe').and.callThrough(); | ||||
|         spyOn(faultManagementProvider, 'supportsSubscribe').and.callThrough(); | ||||
|  | ||||
|         let unsubscribe = openmct.faults.subscribe(faultDomainObject, () => {}); | ||||
|  | ||||
|         expect(unsubscribe).toEqual(jasmine.any(Function)); | ||||
|         expect(faultManagementProvider.supportsSubscribe).toHaveBeenCalledWith(faultDomainObject); | ||||
|         expect(faultManagementProvider.subscribe).toHaveBeenCalledOnceWith(faultDomainObject, jasmine.any(Function)); | ||||
|  | ||||
|     }); | ||||
|  | ||||
|     it('will tell you if the fault management provider supports actions', () => { | ||||
|         expect(openmct.faults.supportsActions()).toBeTrue(); | ||||
|     }); | ||||
|  | ||||
|     it('will allow you to acknowledge a fault', async () => { | ||||
|         spyOn(faultManagementProvider, 'acknowledgeFault').and.callThrough(); | ||||
|  | ||||
|         let ackResponse = await openmct.faults.acknowledgeFault(aFault, aComment); | ||||
|  | ||||
|         expect(faultManagementProvider.acknowledgeFault).toHaveBeenCalledWith(aFault, aComment); | ||||
|         expect(ackResponse.success).toBeTrue(); | ||||
|     }); | ||||
|  | ||||
|     it('will allow you to shelve a fault', async () => { | ||||
|         spyOn(faultManagementProvider, 'shelveFault').and.callThrough(); | ||||
|  | ||||
|         let shelveResponse = await openmct.faults.shelveFault(aFault, aComment); | ||||
|  | ||||
|         expect(faultManagementProvider.shelveFault).toHaveBeenCalledWith(aFault, aComment); | ||||
|         expect(shelveResponse.success).toBeTrue(); | ||||
|     }); | ||||
|  | ||||
| }); | ||||
| @@ -23,13 +23,10 @@ | ||||
| import FormController from './FormController'; | ||||
| import FormProperties from './components/FormProperties.vue'; | ||||
|  | ||||
| import EventEmitter from 'EventEmitter'; | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| export default class FormsAPI extends EventEmitter { | ||||
| export default class FormsAPI { | ||||
|     constructor(openmct) { | ||||
|         super(); | ||||
|  | ||||
|         this.openmct = openmct; | ||||
|         this.formController = new FormController(openmct); | ||||
|     } | ||||
| @@ -110,8 +107,6 @@ export default class FormsAPI extends EventEmitter { | ||||
|         let onDismiss; | ||||
|         let onSave; | ||||
|  | ||||
|         const self = this; | ||||
|  | ||||
|         const promise = new Promise((resolve, reject) => { | ||||
|             onSave = onFormSave(resolve); | ||||
|             onDismiss = onFormDismiss(reject); | ||||
| @@ -120,7 +115,7 @@ export default class FormsAPI extends EventEmitter { | ||||
|         const vm = new Vue({ | ||||
|             components: { FormProperties }, | ||||
|             provide: { | ||||
|                 openmct: self.openmct | ||||
|                 openmct: this.openmct | ||||
|             }, | ||||
|             data() { | ||||
|                 return { | ||||
| @@ -137,7 +132,7 @@ export default class FormsAPI extends EventEmitter { | ||||
|         if (element) { | ||||
|             element.append(formElement); | ||||
|         } else { | ||||
|             overlay = self.openmct.overlays.overlay({ | ||||
|             overlay = this.openmct.overlays.overlay({ | ||||
|                 element: vm.$el, | ||||
|                 size: 'small', | ||||
|                 onDestroy: () => vm.$destroy() | ||||
| @@ -145,7 +140,6 @@ export default class FormsAPI extends EventEmitter { | ||||
|         } | ||||
|  | ||||
|         function onFormPropertyChange(data) { | ||||
|             self.emit('onFormPropertyChange', data); | ||||
|             if (onChange) { | ||||
|                 onChange(data); | ||||
|             } | ||||
|   | ||||
| @@ -1,157 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 js-form"> | ||||
| <div class="c-form"> | ||||
|     <div class="c-overlay__top-bar c-form__top-bar"> | ||||
|         <div class="c-overlay__dialog-title js-form-title">{{ model.title }}</div> | ||||
|         <div class="c-overlay__dialog-title">{{ model.title }}</div> | ||||
|         <div class="c-overlay__dialog-hint hint">All fields marked <span class="req icon-asterisk"></span> are required.</div> | ||||
|     </div> | ||||
|     <form | ||||
| @@ -44,14 +44,18 @@ | ||||
|             > | ||||
|                 {{ section.name }} | ||||
|             </h2> | ||||
|             <FormRow | ||||
|             <div | ||||
|                 v-for="(row, index) in section.rows" | ||||
|                 :key="row.id" | ||||
|                 :css-class="row.cssClass" | ||||
|                 :first="index < 1" | ||||
|                 :row="row" | ||||
|                 @onChange="onChange" | ||||
|             /> | ||||
|                 class="u-contents" | ||||
|             > | ||||
|                 <FormRow | ||||
|                     :css-class="section.cssClass" | ||||
|                     :first="index < 1" | ||||
|                     :row="row" | ||||
|                     @onChange="onChange" | ||||
|                 /> | ||||
|             </div> | ||||
|         </div> | ||||
|     </form> | ||||
|  | ||||
| @@ -60,15 +64,13 @@ | ||||
|             tabindex="0" | ||||
|             :disabled="isInvalid" | ||||
|             class="c-button c-button--major" | ||||
|             aria-label="Save" | ||||
|             @click="onSave" | ||||
|         > | ||||
|             {{ submitLabel }} | ||||
|         </button> | ||||
|         <button | ||||
|             tabindex="0" | ||||
|             class="c-button js-cancel-button" | ||||
|             aria-label="Cancel" | ||||
|             class="c-button" | ||||
|             @click="onDismiss" | ||||
|         > | ||||
|             {{ cancelLabel }} | ||||
| @@ -79,7 +81,7 @@ | ||||
|  | ||||
| <script> | ||||
| import FormRow from "@/api/forms/components/FormRow.vue"; | ||||
| import { v4 as uuid } from 'uuid'; | ||||
| import uuid from 'uuid'; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
|   | ||||
| @@ -23,10 +23,7 @@ | ||||
| <template> | ||||
| <div | ||||
|     class="form-row c-form__row" | ||||
|     :class="[ | ||||
|         { 'first': first }, | ||||
|         cssClass | ||||
|     ]" | ||||
|     :class="[{ 'first': first }]" | ||||
|     @onChange="onChange" | ||||
| > | ||||
|     <div | ||||
| @@ -37,7 +34,7 @@ | ||||
|     </div> | ||||
|     <div | ||||
|         class="c-form-row__state-indicator" | ||||
|         :class="reqClass" | ||||
|         :class="rowClass" | ||||
|     > | ||||
|     </div> | ||||
|     <div | ||||
| @@ -79,22 +76,24 @@ export default { | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|         reqClass() { | ||||
|             let reqClass = 'req'; | ||||
|         rowClass() { | ||||
|             let cssClass = this.cssClass; | ||||
|  | ||||
|             if (!this.row.required) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             cssClass = `${cssClass} req`; | ||||
|  | ||||
|             if (this.visited && this.valid !== undefined) { | ||||
|                 if (this.valid === true) { | ||||
|                     reqClass = 'valid'; | ||||
|                     cssClass = `${cssClass} valid`; | ||||
|                 } else { | ||||
|                     reqClass = 'invalid'; | ||||
|                     cssClass = `${cssClass} invalid`; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             return reqClass; | ||||
|             return cssClass; | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|   | ||||
| @@ -19,47 +19,35 @@ | ||||
| * this source code distribution or the Licensing information page available | ||||
| * at runtime from the About dialog for additional information. | ||||
| *****************************************************************************/ | ||||
|  | ||||
| <template> | ||||
| <div | ||||
|     ref="autoCompleteForm" | ||||
|     class="form-control c-input--autocomplete js-autocomplete" | ||||
| > | ||||
|     <div | ||||
|         class="c-input--autocomplete__wrapper" | ||||
|     > | ||||
| <div class="form-control autocomplete"> | ||||
|     <span class="autocompleteInputAndArrow"> | ||||
|         <input | ||||
|             ref="autoCompleteInput" | ||||
|             v-model="field" | ||||
|             class="c-input--autocomplete__input js-autocomplete__input" | ||||
|             class="autocompleteInput" | ||||
|             type="text" | ||||
|             :placeholder="placeHolderText" | ||||
|             @click="inputClicked()" | ||||
|             @keydown="keyDown($event)" | ||||
|         > | ||||
|         <div | ||||
|             class="icon-arrow-down c-icon-button c-input--autocomplete__afford-arrow js-autocomplete__afford-arrow" | ||||
|         <span | ||||
|             class="icon-arrow-down" | ||||
|             @click="arrowClicked()" | ||||
|         ></div> | ||||
|     </div> | ||||
|         ></span> | ||||
|     </span> | ||||
|     <div | ||||
|         v-if="!hideOptions" | ||||
|         class="c-menu c-input--autocomplete__options" | ||||
|         aria-label="Autocomplete Options" | ||||
|         class="autocompleteOptions" | ||||
|         @blur="hideOptions = true" | ||||
|     > | ||||
|         <ul> | ||||
|         <ul v-if="!hideOptions"> | ||||
|             <li | ||||
|                 v-for="opt in filteredOptions" | ||||
|                 :key="opt.optionId" | ||||
|                 :class="[ | ||||
|                     {'optionPreSelected': optionIndex === opt.optionId}, | ||||
|                     itemCssClass | ||||
|                 ]" | ||||
|                 :style="itemStyle(opt)" | ||||
|                 :class="{'optionPreSelected': optionIndex === opt.optionId}" | ||||
|                 @click="fillInputWithString(opt.name)" | ||||
|                 @mouseover="optionMouseover(opt.optionId)" | ||||
|             > | ||||
|                 {{ opt.name }} | ||||
|                 <span class="optionText">{{ opt.name }}</span> | ||||
|             </li> | ||||
|         </ul> | ||||
|     </div> | ||||
| @@ -77,23 +65,7 @@ export default { | ||||
|     props: { | ||||
|         model: { | ||||
|             type: Object, | ||||
|             required: true, | ||||
|             default() { | ||||
|                 return {}; | ||||
|             } | ||||
|         }, | ||||
|         placeHolderText: { | ||||
|             type: String, | ||||
|             default() { | ||||
|                 return ""; | ||||
|             } | ||||
|         }, | ||||
|         itemCssClass: { | ||||
|             type: String, | ||||
|             required: false, | ||||
|             default() { | ||||
|                 return ""; | ||||
|             } | ||||
|             required: true | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
| @@ -106,40 +78,31 @@ export default { | ||||
|     }, | ||||
|     computed: { | ||||
|         filteredOptions() { | ||||
|             const fullOptions = this.options || []; | ||||
|             const options = this.optionNames || []; | ||||
|             if (this.showFilteredOptions) { | ||||
|                 const optionsFiltered = fullOptions | ||||
|                 return options | ||||
|                     .filter(option => { | ||||
|                         if (option.name && this.field) { | ||||
|                             return option.name.toLowerCase().indexOf(this.field.toLowerCase()) >= 0; | ||||
|                         } | ||||
|  | ||||
|                         return false; | ||||
|                         return option.toLowerCase().indexOf(this.field.toLowerCase()) >= 0; | ||||
|                     }).map((option, index) => { | ||||
|                         return { | ||||
|                             optionId: index, | ||||
|                             name: option.name, | ||||
|                             color: option.color | ||||
|                             name: option | ||||
|                         }; | ||||
|                     }); | ||||
|  | ||||
|                 return optionsFiltered; | ||||
|             } | ||||
|  | ||||
|             const optionsFiltered = fullOptions.map((option, index) => { | ||||
|             return options.map((option, index) => { | ||||
|                 return { | ||||
|                     optionId: index, | ||||
|                     name: option.name, | ||||
|                     color: option.color | ||||
|                     name: option | ||||
|                 }; | ||||
|             }); | ||||
|  | ||||
|             return optionsFiltered; | ||||
|         } | ||||
|     }, | ||||
|     watch: { | ||||
|         field(newValue, oldValue) { | ||||
|             if (newValue !== oldValue) { | ||||
|  | ||||
|                 const data = { | ||||
|                     model: this.model, | ||||
|                     value: newValue | ||||
| @@ -160,17 +123,17 @@ export default { | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.autocompleteInputAndArrow = this.$refs.autoCompleteForm; | ||||
|         this.autocompleteInputElement = this.$refs.autoCompleteInput; | ||||
|         if (this.model.options && this.model.options.length && !this.model.options[0].name) { | ||||
|             // If options is only an array of string. | ||||
|             this.options = this.model.options.map((option) => { | ||||
|                 return { | ||||
|                     name: option | ||||
|                 }; | ||||
|         this.options = this.model.options; | ||||
|         this.autocompleteInputAndArrow = this.$el.getElementsByClassName('autocompleteInputAndArrow')[0]; | ||||
|         this.autocompleteInputElement = this.$el.getElementsByClassName('autocompleteInput')[0]; | ||||
|         if (this.options[0].name) { | ||||
|         // If "options" include name, value pair | ||||
|             this.optionNames = this.options.map((opt) => { | ||||
|                 return opt.name; | ||||
|             }); | ||||
|         } else { | ||||
|             this.options = this.model.options; | ||||
|         // If options is only an array of string. | ||||
|             this.optionNames = this.options; | ||||
|         } | ||||
|     }, | ||||
|     destroyed() { | ||||
| @@ -259,12 +222,6 @@ export default { | ||||
|                     }); | ||||
|                 } | ||||
|             }); | ||||
|         }, | ||||
|         itemStyle(option) { | ||||
|             if (option.color) { | ||||
|  | ||||
|                 return { '--optionIconColor': option.color }; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -40,12 +40,6 @@ | ||||
|         > | ||||
|             {{ name }} | ||||
|         </button> | ||||
|         <button | ||||
|             v-if="removable" | ||||
|             class="c-button icon-trash" | ||||
|             title="Remove file" | ||||
|             @click="removeFile" | ||||
|         ></button> | ||||
|     </span> | ||||
| </span> | ||||
| </template> | ||||
| @@ -69,9 +63,6 @@ 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() { | ||||
| @@ -106,15 +97,6 @@ export default { | ||||
|         }, | ||||
|         selectFile() { | ||||
|             this.$refs.fileInput.click(); | ||||
|         }, | ||||
|         removeFile() { | ||||
|             this.model.value = undefined; | ||||
|             this.fileInfo = undefined; | ||||
|             const data = { | ||||
|                 model: this.model, | ||||
|                 value: undefined | ||||
|             }; | ||||
|             this.$emit('onChange', data); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -28,7 +28,6 @@ | ||||
|     > | ||||
|         <input | ||||
|             v-model="field" | ||||
|             :aria-label="model.name" | ||||
|             type="number" | ||||
|             :min="model.min" | ||||
|             :max="model.max" | ||||
|   | ||||
| @@ -39,7 +39,7 @@ | ||||
| import toggleMixin from '../../toggle-check-box-mixin'; | ||||
| import ToggleSwitch from '@/ui/components/ToggleSwitch.vue'; | ||||
|  | ||||
| import { v4 as uuid } from 'uuid'; | ||||
| import uuid from 'uuid'; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
|   | ||||
| @@ -19,27 +19,27 @@ | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import EventEmitter from "EventEmitter"; | ||||
| import SimpleIndicator from "./SimpleIndicator"; | ||||
|  | ||||
| class IndicatorAPI extends EventEmitter { | ||||
|     constructor(openmct) { | ||||
|         super(); | ||||
|  | ||||
| define([ | ||||
|     './SimpleIndicator', | ||||
|     'lodash' | ||||
| ], function ( | ||||
|     SimpleIndicator, | ||||
|     _ | ||||
| ) { | ||||
|     function IndicatorAPI(openmct) { | ||||
|         this.openmct = openmct; | ||||
|         this.indicatorObjects = []; | ||||
|     } | ||||
|  | ||||
|     getIndicatorObjectsByPriority() { | ||||
|     IndicatorAPI.prototype.getIndicatorObjectsByPriority = function () { | ||||
|         const sortedIndicators = this.indicatorObjects.sort((a, b) => b.priority - a.priority); | ||||
|  | ||||
|         return sortedIndicators; | ||||
|     } | ||||
|     }; | ||||
|  | ||||
|     simpleIndicator() { | ||||
|     IndicatorAPI.prototype.simpleIndicator = function () { | ||||
|         return new SimpleIndicator(this.openmct); | ||||
|     } | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Accepts an indicator object, which is a simple object | ||||
| @@ -62,16 +62,14 @@ class IndicatorAPI extends EventEmitter { | ||||
|      * myIndicator.iconClass("icon-info"); | ||||
|      * | ||||
|      */ | ||||
|     add(indicator) { | ||||
|     IndicatorAPI.prototype.add = function (indicator) { | ||||
|         if (!indicator.priority) { | ||||
|             indicator.priority = this.openmct.priority.DEFAULT; | ||||
|         } | ||||
|  | ||||
|         this.indicatorObjects.push(indicator); | ||||
|     }; | ||||
|  | ||||
|         this.emit('addIndicator', indicator); | ||||
|     } | ||||
|     return IndicatorAPI; | ||||
|  | ||||
| } | ||||
|  | ||||
| export default IndicatorAPI; | ||||
| }); | ||||
|   | ||||
| @@ -20,101 +20,82 @@ | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import EventEmitter from 'EventEmitter'; | ||||
| import indicatorTemplate from './res/indicator-template.html'; | ||||
| import { convertTemplateToHTML } from '@/utils/template/templateHelpers'; | ||||
| define(['zepto', './res/indicator-template.html'], | ||||
|     function ($, indicatorTemplate) { | ||||
|         const DEFAULT_ICON_CLASS = 'icon-info'; | ||||
|  | ||||
| const DEFAULT_ICON_CLASS = 'icon-info'; | ||||
|         function SimpleIndicator(openmct) { | ||||
|             this.openmct = openmct; | ||||
|             this.element = $(indicatorTemplate)[0]; | ||||
|             this.priority = openmct.priority.DEFAULT; | ||||
|  | ||||
| class SimpleIndicator extends EventEmitter { | ||||
|     constructor(openmct) { | ||||
|         super(); | ||||
|             this.textElement = this.element.querySelector('.js-indicator-text'); | ||||
|  | ||||
|         this.openmct = openmct; | ||||
|         this.element = convertTemplateToHTML(indicatorTemplate)[0]; | ||||
|         this.priority = openmct.priority.DEFAULT; | ||||
|  | ||||
|         this.textElement = this.element.querySelector('.js-indicator-text'); | ||||
|  | ||||
|         //Set defaults | ||||
|         this.text('New Indicator'); | ||||
|         this.description(''); | ||||
|         this.iconClass(DEFAULT_ICON_CLASS); | ||||
|  | ||||
|         this.click = this.click.bind(this); | ||||
|  | ||||
|         this.element.addEventListener('click', this.click); | ||||
|         openmct.once('destroy', () => { | ||||
|             this.removeAllListeners(); | ||||
|             this.element.removeEventListener('click', this.click); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     text(text) { | ||||
|         if (text !== undefined && text !== this.textValue) { | ||||
|             this.textValue = text; | ||||
|             this.textElement.innerText = text; | ||||
|  | ||||
|             if (!text) { | ||||
|                 this.element.classList.add('hidden'); | ||||
|             } else { | ||||
|                 this.element.classList.remove('hidden'); | ||||
|             } | ||||
|             //Set defaults | ||||
|             this.text('New Indicator'); | ||||
|             this.description(''); | ||||
|             this.iconClass(DEFAULT_ICON_CLASS); | ||||
|             this.statusClass(''); | ||||
|         } | ||||
|  | ||||
|         return this.textValue; | ||||
|     } | ||||
|         SimpleIndicator.prototype.text = function (text) { | ||||
|             if (text !== undefined && text !== this.textValue) { | ||||
|                 this.textValue = text; | ||||
|                 this.textElement.innerText = text; | ||||
|  | ||||
|     description(description) { | ||||
|         if (description !== undefined && description !== this.descriptionValue) { | ||||
|             this.descriptionValue = description; | ||||
|             this.element.title = description; | ||||
|         } | ||||
|  | ||||
|         return this.descriptionValue; | ||||
|     } | ||||
|  | ||||
|     iconClass(iconClass) { | ||||
|         if (iconClass !== undefined && iconClass !== this.iconClassValue) { | ||||
|             // element.classList is precious and throws errors if you try and add | ||||
|             // or remove empty strings | ||||
|             if (this.iconClassValue) { | ||||
|                 this.element.classList.remove(this.iconClassValue); | ||||
|                 if (!text) { | ||||
|                     this.element.classList.add('hidden'); | ||||
|                 } else { | ||||
|                     this.element.classList.remove('hidden'); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             if (iconClass) { | ||||
|                 this.element.classList.add(iconClass); | ||||
|             return this.textValue; | ||||
|         }; | ||||
|  | ||||
|         SimpleIndicator.prototype.description = function (description) { | ||||
|             if (description !== undefined && description !== this.descriptionValue) { | ||||
|                 this.descriptionValue = description; | ||||
|                 this.element.title = description; | ||||
|             } | ||||
|  | ||||
|             this.iconClassValue = iconClass; | ||||
|         } | ||||
|             return this.descriptionValue; | ||||
|         }; | ||||
|  | ||||
|         return this.iconClassValue; | ||||
|     } | ||||
|         SimpleIndicator.prototype.iconClass = function (iconClass) { | ||||
|             if (iconClass !== undefined && iconClass !== this.iconClassValue) { | ||||
|                 // element.classList is precious and throws errors if you try and add | ||||
|                 // or remove empty strings | ||||
|                 if (this.iconClassValue) { | ||||
|                     this.element.classList.remove(this.iconClassValue); | ||||
|                 } | ||||
|  | ||||
|     statusClass(statusClass) { | ||||
|         if (arguments.length === 1 && statusClass !== this.statusClassValue) { | ||||
|             if (this.statusClassValue) { | ||||
|                 this.element.classList.remove(this.statusClassValue); | ||||
|                 if (iconClass) { | ||||
|                     this.element.classList.add(iconClass); | ||||
|                 } | ||||
|  | ||||
|                 this.iconClassValue = iconClass; | ||||
|             } | ||||
|  | ||||
|             if (statusClass !== undefined) { | ||||
|                 this.element.classList.add(statusClass); | ||||
|             return this.iconClassValue; | ||||
|         }; | ||||
|  | ||||
|         SimpleIndicator.prototype.statusClass = function (statusClass) { | ||||
|             if (statusClass !== undefined && statusClass !== this.statusClassValue) { | ||||
|                 if (this.statusClassValue) { | ||||
|                     this.element.classList.remove(this.statusClassValue); | ||||
|                 } | ||||
|  | ||||
|                 if (statusClass) { | ||||
|                     this.element.classList.add(statusClass); | ||||
|                 } | ||||
|  | ||||
|                 this.statusClassValue = statusClass; | ||||
|             } | ||||
|  | ||||
|             this.statusClassValue = statusClass; | ||||
|         } | ||||
|             return this.statusClassValue; | ||||
|         }; | ||||
|  | ||||
|         return this.statusClassValue; | ||||
|         return SimpleIndicator; | ||||
|     } | ||||
|  | ||||
|     click(event) { | ||||
|         this.emit('click', event); | ||||
|     } | ||||
|  | ||||
|     getElement() { | ||||
|         return this.element; | ||||
|     } | ||||
| } | ||||
|  | ||||
| export default SimpleIndicator; | ||||
| ); | ||||
|   | ||||
| @@ -26,31 +26,29 @@ import { createOpenMct, createMouseEvent, resetApplicationState } from '../../ut | ||||
|  | ||||
| describe ('The Menu API', () => { | ||||
|     let openmct; | ||||
|     let appHolder; | ||||
|     let element; | ||||
|     let menuAPI; | ||||
|     let actionsArray; | ||||
|     let x; | ||||
|     let y; | ||||
|     let result; | ||||
|     let menuElement; | ||||
|  | ||||
|     const x = 8; | ||||
|     const y = 16; | ||||
|  | ||||
|     const menuOptions = { | ||||
|         onDestroy: () => { | ||||
|             console.log('default onDestroy'); | ||||
|         } | ||||
|     }; | ||||
|     let onDestroy; | ||||
|  | ||||
|     beforeEach((done) => { | ||||
|         appHolder = document.createElement('div'); | ||||
|         const 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(); | ||||
|         openmct.startHeadless(appHolder); | ||||
|  | ||||
|         menuAPI = new MenuAPI(openmct); | ||||
|         actionsArray = [ | ||||
| @@ -58,7 +56,7 @@ describe ('The Menu API', () => { | ||||
|                 key: 'test-css-class-1', | ||||
|                 name: 'Test Action 1', | ||||
|                 cssClass: 'icon-clock', | ||||
|                 description: 'This is a test action 1', | ||||
|                 description: 'This is a test action', | ||||
|                 onItemClicked: () => { | ||||
|                     result = 'Test Action 1 Invoked'; | ||||
|                 } | ||||
| @@ -67,165 +65,149 @@ describe ('The Menu API', () => { | ||||
|                 key: 'test-css-class-2', | ||||
|                 name: 'Test Action 2', | ||||
|                 cssClass: 'icon-clock', | ||||
|                 description: 'This is a test action 2', | ||||
|                 description: 'This is a test action', | ||||
|                 onItemClicked: () => { | ||||
|                     result = 'Test Action 2 Invoked'; | ||||
|                 } | ||||
|             } | ||||
|         ]; | ||||
|         x = 8; | ||||
|         y = 16; | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     describe('showMenu method', () => { | ||||
|         beforeAll(() => { | ||||
|             spyOn(menuOptions, 'onDestroy').and.callThrough(); | ||||
|         }); | ||||
|  | ||||
|         it('creates an instance of Menu when invoked', (done) => { | ||||
|             menuOptions.onDestroy = done; | ||||
|  | ||||
|             menuAPI.showMenu(x, y, actionsArray, menuOptions); | ||||
|     describe("showMenu method", () => { | ||||
|         it("creates an instance of Menu when invoked", () => { | ||||
|             menuAPI.showMenu(x, y, actionsArray); | ||||
|  | ||||
|             expect(menuAPI.menuComponent).toBeInstanceOf(Menu); | ||||
|             document.body.click(); | ||||
|         }); | ||||
|  | ||||
|         describe('creates a menu component', () => { | ||||
|             it('with all the actions passed in', (done) => { | ||||
|                 menuOptions.onDestroy = done; | ||||
|         describe("creates a menu component", () => { | ||||
|             let menuComponent; | ||||
|             let vueComponent; | ||||
|  | ||||
|             beforeEach(() => { | ||||
|                 onDestroy = jasmine.createSpy('onDestroy'); | ||||
|  | ||||
|                 const menuOptions = { | ||||
|                     onDestroy | ||||
|                 }; | ||||
|  | ||||
|                 menuAPI.showMenu(x, y, actionsArray, menuOptions); | ||||
|                 menuElement = document.querySelector('.c-menu'); | ||||
|                 expect(menuElement).toBeDefined(); | ||||
|                 vueComponent = menuAPI.menuComponent.component; | ||||
|                 menuComponent = document.querySelector(".c-menu"); | ||||
|  | ||||
|                 const listItems = menuElement.children[0].children; | ||||
|                 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; | ||||
|  | ||||
|                 expect(listItems.length).toEqual(actionsArray.length); | ||||
|                 document.body.click(); | ||||
|             }); | ||||
|  | ||||
|             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]; | ||||
|             it("with click-able menu items, that will invoke the correct callBacks", () => { | ||||
|                 let listItem1 = menuComponent.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', (done) => { | ||||
|                 menuOptions.onDestroy = done; | ||||
|             it("dismisses the menu when action is clicked on", () => { | ||||
|                 let listItem1 = menuComponent.children[0].children[0]; | ||||
|  | ||||
|                 menuAPI.showMenu(x, y, actionsArray, menuOptions); | ||||
|  | ||||
|                 menuElement = document.querySelector('.c-menu'); | ||||
|                 const listItem1 = menuElement.children[0].children[0]; | ||||
|                 listItem1.click(); | ||||
|  | ||||
|                 menuElement = document.querySelector('.c-menu'); | ||||
|                 let menu = document.querySelector('.c-menu'); | ||||
|  | ||||
|                 expect(menuElement).toBeNull(); | ||||
|                 expect(menu).toBeNull(); | ||||
|             }); | ||||
|  | ||||
|             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'); | ||||
|  | ||||
|             it("invokes the destroy method when menu is dismissed", () => { | ||||
|                 document.body.click(); | ||||
|  | ||||
|                 expect(vueComponent.$destroy).toHaveBeenCalled(); | ||||
|             }); | ||||
|  | ||||
|             it('invokes the onDestroy callback if passed in', (done) => { | ||||
|                 let count = 0; | ||||
|                 menuOptions.onDestroy = () => { | ||||
|                     count++; | ||||
|                     expect(count).toEqual(1); | ||||
|                     done(); | ||||
|                 }; | ||||
|  | ||||
|                 menuAPI.showMenu(x, y, actionsArray, menuOptions); | ||||
|  | ||||
|             it("invokes the onDestroy callback if passed in", () => { | ||||
|                 document.body.click(); | ||||
|  | ||||
|                 expect(onDestroy).toHaveBeenCalled(); | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe('superMenu method', () => { | ||||
|         it('creates a superMenu', (done) => { | ||||
|             menuOptions.onDestroy = done; | ||||
|     describe("superMenu method", () => { | ||||
|         it("creates a superMenu", () => { | ||||
|             menuAPI.showSuperMenu(x, y, actionsArray); | ||||
|  | ||||
|             menuAPI.showSuperMenu(x, y, actionsArray, menuOptions); | ||||
|             menuElement = document.querySelector('.c-super-menu__menu'); | ||||
|             const superMenu = document.querySelector('.c-super-menu__menu'); | ||||
|  | ||||
|             expect(menuElement).not.toBeNull(); | ||||
|             document.body.click(); | ||||
|             expect(superMenu).not.toBeNull(); | ||||
|         }); | ||||
|  | ||||
|         it('Mouse over a superMenu shows correct description', (done) => { | ||||
|             menuOptions.onDestroy = done; | ||||
|         it("Mouse over a superMenu shows correct description", (done) => { | ||||
|             menuAPI.showSuperMenu(x, y, actionsArray); | ||||
|  | ||||
|             menuAPI.showSuperMenu(x, y, actionsArray, menuOptions); | ||||
|             menuElement = document.querySelector('.c-super-menu__menu'); | ||||
|  | ||||
|             const superMenuItem = menuElement.querySelector('li'); | ||||
|             const superMenu = document.querySelector('.c-super-menu__menu'); | ||||
|             const superMenuItem = superMenu.querySelector('li'); | ||||
|             const mouseOverEvent = createMouseEvent('mouseover'); | ||||
|  | ||||
|             superMenuItem.dispatchEvent(mouseOverEvent); | ||||
|             const itemDescription = document.querySelector('.l-item-description__description'); | ||||
|  | ||||
|             menuAPI.menuComponent.component.$nextTick(() => { | ||||
|                 expect(menuElement).not.toBeNull(); | ||||
|             setTimeout(() => { | ||||
|                 expect(itemDescription.innerText).toEqual(actionsArray[0].description); | ||||
|  | ||||
|                 document.body.click(); | ||||
|             }); | ||||
|                 expect(superMenu).not.toBeNull(); | ||||
|                 done(); | ||||
|             }, 300); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe('Menu Placements', () => { | ||||
|         it('default menu position BOTTOM_RIGHT', (done) => { | ||||
|             menuOptions.onDestroy = done; | ||||
|     describe("Menu Placements", () => { | ||||
|         it("default menu position BOTTOM_RIGHT", () => { | ||||
|             menuAPI.showMenu(x, y, actionsArray); | ||||
|  | ||||
|             menuAPI.showMenu(x, y, actionsArray, menuOptions); | ||||
|             menuElement = document.querySelector('.c-menu'); | ||||
|             const menu = document.querySelector('.c-menu'); | ||||
|  | ||||
|             const boundingClientRect = menuElement.getBoundingClientRect(); | ||||
|             const boundingClientRect = menu.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; | ||||
|         it("menu position BOTTOM_RIGHT", () => { | ||||
|             const menuOptions = { | ||||
|                 placement: openmct.menus.menuPlacement.BOTTOM_RIGHT | ||||
|             }; | ||||
|  | ||||
|             menuAPI.showMenu(x, y, actionsArray, menuOptions); | ||||
|             menuElement = document.querySelector('.c-menu'); | ||||
|  | ||||
|             const boundingClientRect = menuElement.getBoundingClientRect(); | ||||
|             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); | ||||
|  | ||||
|             document.body.click(); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
|   | ||||
| @@ -12,7 +12,6 @@ | ||||
|                 :key="action.name" | ||||
|                 :class="[action.cssClass, action.isDisabled ? 'disabled' : '']" | ||||
|                 :title="action.description" | ||||
|                 :data-testid="action.testId || false" | ||||
|                 @click="action.onItemClicked" | ||||
|             > | ||||
|                 {{ action.name }} | ||||
| @@ -36,9 +35,8 @@ | ||||
|         <li | ||||
|             v-for="action in options.actions" | ||||
|             :key="action.name" | ||||
|             :class="[action.cssClass, action.isDisabled ? 'disabled' : '']" | ||||
|             :class="action.cssClass" | ||||
|             :title="action.description" | ||||
|             :data-testid="action.testId || false" | ||||
|             @click="action.onItemClicked" | ||||
|         > | ||||
|             {{ action.name }} | ||||
|   | ||||
| @@ -15,7 +15,6 @@ | ||||
|                 :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()" | ||||
| @@ -46,7 +45,6 @@ | ||||
|             :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 { v4 as uuid } from 'uuid'; | ||||
| import uuid from 'uuid'; | ||||
|  | ||||
| class InMemorySearchProvider { | ||||
|     /** | ||||
| @@ -39,10 +39,11 @@ class InMemorySearchProvider { | ||||
|          * If max results is not specified in query, use this as default. | ||||
|          */ | ||||
|         this.DEFAULT_MAX_RESULTS = 100; | ||||
|  | ||||
|         this.openmct = openmct; | ||||
|  | ||||
|         this.indexedIds = {}; | ||||
|         this.indexedCompositions = {}; | ||||
|         this.indexedTags = {}; | ||||
|         this.idsToIndex = []; | ||||
|         this.pendingIndex = {}; | ||||
|         this.pendingRequests = 0; | ||||
| @@ -51,18 +52,11 @@ class InMemorySearchProvider { | ||||
|         /** | ||||
|          * If we don't have SharedWorkers available (e.g., iOS) | ||||
|          */ | ||||
|         this.localIndexedDomainObjects = {}; | ||||
|         this.localIndexedAnnotationsByDomainObject = {}; | ||||
|         this.localIndexedAnnotationsByTag = {}; | ||||
|         this.localIndexedItems = {}; | ||||
|  | ||||
|         this.pendingQueries = {}; | ||||
|         this.onWorkerMessage = this.onWorkerMessage.bind(this); | ||||
|         this.onWorkerMessageError = this.onWorkerMessageError.bind(this); | ||||
|         this.localSearchForObjects = this.localSearchForObjects.bind(this); | ||||
|         this.localSearchForAnnotations = this.localSearchForAnnotations.bind(this); | ||||
|         this.localSearchForTags = this.localSearchForTags.bind(this); | ||||
|         this.localSearchForNotebookAnnotations = this.localSearchForNotebookAnnotations.bind(this); | ||||
|         this.onAnnotationCreation = this.onAnnotationCreation.bind(this); | ||||
|         this.onerror = this.onWorkerError.bind(this); | ||||
|         this.startIndexing = this.startIndexing.bind(this); | ||||
|  | ||||
| @@ -82,39 +76,13 @@ class InMemorySearchProvider { | ||||
|  | ||||
|     startIndexing() { | ||||
|         const rootObject = this.openmct.objects.rootProvider.rootObject; | ||||
|  | ||||
|         this.searchTypes = this.openmct.objects.SEARCH_TYPES; | ||||
|  | ||||
|         this.supportedSearchTypes = [this.searchTypes.OBJECTS, this.searchTypes.ANNOTATIONS, this.searchTypes.NOTEBOOK_ANNOTATIONS, this.searchTypes.TAGS]; | ||||
|  | ||||
|         this.scheduleForIndexing(rootObject.identifier); | ||||
|  | ||||
|         this.indexAnnotations(); | ||||
|  | ||||
|         if (typeof SharedWorker !== 'undefined') { | ||||
|             this.worker = this.startSharedWorker(); | ||||
|         } else { | ||||
|             // we must be on iOS | ||||
|         } | ||||
|  | ||||
|         this.openmct.annotation.on('annotationCreated', this.onAnnotationCreation); | ||||
|  | ||||
|     } | ||||
|  | ||||
|     indexAnnotations() { | ||||
|         const theInMemorySearchProvider = this; | ||||
|         Object.values(this.openmct.objects.providers).forEach(objectProvider => { | ||||
|             if (objectProvider.getAllObjects) { | ||||
|                 const allObjects = objectProvider.getAllObjects(); | ||||
|                 if (allObjects) { | ||||
|                     Object.values(allObjects).forEach(domainObject => { | ||||
|                         if (domainObject.type === 'annotation') { | ||||
|                             theInMemorySearchProvider.scheduleForIndexing(domainObject.identifier); | ||||
|                         } | ||||
|                     }); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -130,60 +98,51 @@ class InMemorySearchProvider { | ||||
|         return intermediateResponse; | ||||
|     } | ||||
|  | ||||
|     search(query, searchType) { | ||||
|     /** | ||||
|      * Query the search provider for results. | ||||
|      * | ||||
|      * @param {String} input the string to search by. | ||||
|      * @param {Number} maxResults max number of results to return. | ||||
|      * @returns {Promise} a promise for a modelResults object. | ||||
|      */ | ||||
|     query(input, maxResults) { | ||||
|         if (!maxResults) { | ||||
|             maxResults = this.DEFAULT_MAX_RESULTS; | ||||
|         } | ||||
|  | ||||
|         const queryId = uuid(); | ||||
|         const pendingQuery = this.getIntermediateResponse(); | ||||
|         this.pendingQueries[queryId] = pendingQuery; | ||||
|         const searchOptions = { | ||||
|             queryId, | ||||
|             searchType, | ||||
|             query, | ||||
|             maxResults: this.DEFAULT_MAX_RESULTS | ||||
|         }; | ||||
|  | ||||
|         if (this.worker) { | ||||
|             this.#dispatchSearchToWorker(searchOptions); | ||||
|             this.dispatchSearch(queryId, input, maxResults); | ||||
|         } else { | ||||
|             this.#localQueryFallBack(searchOptions); | ||||
|             this.localSearch(queryId, input, maxResults); | ||||
|         } | ||||
|  | ||||
|         return pendingQuery.promise; | ||||
|     } | ||||
|  | ||||
|     #localQueryFallBack({queryId, searchType, query, maxResults}) { | ||||
|         if (searchType === this.searchTypes.OBJECTS) { | ||||
|             return this.localSearchForObjects(queryId, query, maxResults); | ||||
|         } else if (searchType === this.searchTypes.ANNOTATIONS) { | ||||
|             return this.localSearchForAnnotations(queryId, query, maxResults); | ||||
|         } else if (searchType === this.searchTypes.NOTEBOOK_ANNOTATIONS) { | ||||
|             return this.localSearchForNotebookAnnotations(queryId, query, maxResults); | ||||
|         } else if (searchType === this.searchTypes.TAGS) { | ||||
|             return this.localSearchForTags(queryId, query, maxResults); | ||||
|         } else { | ||||
|             throw new Error(`🤷♂️ Unknown search type passed: ${searchType}`); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     supportsSearchType(searchType) { | ||||
|         return this.supportedSearchTypes.includes(searchType); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Handle messages from the worker. | ||||
|      * Handle messages from the worker.  Only really knows how to handle search | ||||
|      * results, which are parsed, transformed into a modelResult object, which | ||||
|      * is used to resolve the corresponding promise. | ||||
|      * @private | ||||
|      */ | ||||
|     async onWorkerMessage(event) { | ||||
|         if (event.data.request !== 'search') { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         const pendingQuery = this.pendingQueries[event.data.queryId]; | ||||
|         const modelResults = { | ||||
|             total: event.data.total | ||||
|         }; | ||||
|         modelResults.hits = await Promise.all(event.data.results.map(async (hit) => { | ||||
|             if (hit && hit.keyString) { | ||||
|                 const identifier = this.openmct.objects.parseKeyString(hit.keyString); | ||||
|                 const domainObject = await this.openmct.objects.get(identifier); | ||||
|             const identifier = this.openmct.objects.parseKeyString(hit.keyString); | ||||
|             const domainObject = await this.openmct.objects.get(identifier); | ||||
|  | ||||
|                 return domainObject; | ||||
|             } | ||||
|             return domainObject; | ||||
|         })); | ||||
|  | ||||
|         pendingQuery.resolve(modelResults); | ||||
| @@ -224,8 +183,7 @@ class InMemorySearchProvider { | ||||
|  | ||||
|     /** | ||||
|      * Schedule an id to be indexed at a later date.  If there are less | ||||
|      * pending requests than the maximum allowed, this will kick off an indexing request. | ||||
|      * This is done only when indexing first begins and we need to index a lot of objects. | ||||
|      * pending requests then allowed, will kick off an indexing request. | ||||
|      * | ||||
|      * @private | ||||
|      * @param {identifier} id to be indexed. | ||||
| @@ -258,15 +216,6 @@ class InMemorySearchProvider { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     onAnnotationCreation(annotationObject) { | ||||
|  | ||||
|         const objectProvider = this.openmct.objects.getProvider(annotationObject.identifier); | ||||
|         if (objectProvider === undefined || objectProvider.search === undefined) { | ||||
|             const provider = this; | ||||
|             provider.index(annotationObject); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     onNameMutation(domainObject, name) { | ||||
|         const provider = this; | ||||
|  | ||||
| @@ -274,13 +223,6 @@ class InMemorySearchProvider { | ||||
|         provider.index(domainObject); | ||||
|     } | ||||
|  | ||||
|     onTagMutation(domainObject, newTags) { | ||||
|         domainObject.tags = newTags; | ||||
|         const provider = this; | ||||
|  | ||||
|         provider.index(domainObject); | ||||
|     } | ||||
|  | ||||
|     onCompositionMutation(domainObject, composition) { | ||||
|         const provider = this; | ||||
|         const indexedComposition = domainObject.composition; | ||||
| @@ -317,13 +259,6 @@ class InMemorySearchProvider { | ||||
|                 'composition', | ||||
|                 this.onCompositionMutation.bind(this, domainObject) | ||||
|             ); | ||||
|             if (domainObject.type === 'annotation') { | ||||
|                 this.indexedTags[keyString] = this.openmct.objects.observe( | ||||
|                     domainObject, | ||||
|                     'tags', | ||||
|                     this.onTagMutation.bind(this, domainObject) | ||||
|                 ); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if ((keyString !== 'ROOT')) { | ||||
| @@ -382,83 +317,26 @@ class InMemorySearchProvider { | ||||
|      * @private | ||||
|      * @returns {String} a unique query Id for the query. | ||||
|      */ | ||||
|     #dispatchSearchToWorker({queryId, searchType, query, maxResults}) { | ||||
|     dispatchSearch(queryId, searchInput, maxResults) { | ||||
|         const message = { | ||||
|             request: searchType.toString(), | ||||
|             input: query, | ||||
|             request: 'search', | ||||
|             input: searchInput, | ||||
|             maxResults, | ||||
|             queryId | ||||
|         }; | ||||
|         this.worker.port.postMessage(message); | ||||
|     } | ||||
|  | ||||
|     localIndexTags(keyString, objectToIndex, model) { | ||||
|         // add new tags | ||||
|         model.tags.forEach(tagID => { | ||||
|             if (!this.localIndexedAnnotationsByTag[tagID]) { | ||||
|                 this.localIndexedAnnotationsByTag[tagID] = []; | ||||
|             } | ||||
|  | ||||
|             const existsInIndex = this.localIndexedAnnotationsByTag[tagID].some(indexedObject => { | ||||
|                 return indexedObject.keyString === objectToIndex.keyString; | ||||
|             }); | ||||
|  | ||||
|             if (!existsInIndex) { | ||||
|                 this.localIndexedAnnotationsByTag[tagID].push(objectToIndex); | ||||
|             } | ||||
|  | ||||
|         }); | ||||
|         const tagsToRemoveFromIndex = Object.keys(this.localIndexedAnnotationsByTag).filter(indexedTag => { | ||||
|             return !(model.tags.includes(indexedTag)); | ||||
|         }); | ||||
|         tagsToRemoveFromIndex.forEach(tagToRemoveFromIndex => { | ||||
|             this.localIndexedAnnotationsByTag[tagToRemoveFromIndex] = this.localIndexedAnnotationsByTag[tagToRemoveFromIndex].filter(indexedAnnotation => { | ||||
|                 const shouldKeep = indexedAnnotation.keyString !== keyString; | ||||
|  | ||||
|                 return shouldKeep; | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     localIndexAnnotation(objectToIndex, model) { | ||||
|         Object.keys(model.targets).forEach(targetID => { | ||||
|             if (!this.localIndexedAnnotationsByDomainObject[targetID]) { | ||||
|                 this.localIndexedAnnotationsByDomainObject[targetID] = []; | ||||
|             } | ||||
|  | ||||
|             objectToIndex.targets = model.targets; | ||||
|             objectToIndex.tags = model.tags; | ||||
|             const existsInIndex = this.localIndexedAnnotationsByDomainObject[targetID].some(indexedObject => { | ||||
|                 return indexedObject.keyString === objectToIndex.keyString; | ||||
|             }); | ||||
|  | ||||
|             if (!existsInIndex) { | ||||
|                 this.localIndexedAnnotationsByDomainObject[targetID].push(objectToIndex); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * A local version of the same SharedWorker function | ||||
|      * if we don't have SharedWorkers available (e.g., iOS) | ||||
|      */ | ||||
|     localIndexItem(keyString, model) { | ||||
|         const objectToIndex = { | ||||
|         this.localIndexedItems[keyString] = { | ||||
|             type: model.type, | ||||
|             name: model.name, | ||||
|             keyString | ||||
|         }; | ||||
|         if (model && (model.type === 'annotation')) { | ||||
|             if (model.targets) { | ||||
|                 this.localIndexAnnotation(objectToIndex, model); | ||||
|             } | ||||
|  | ||||
|             if (model.tags) { | ||||
|                 this.localIndexTags(keyString, objectToIndex, model); | ||||
|             } | ||||
|         } else { | ||||
|             this.localIndexedDomainObjects[keyString] = objectToIndex; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -468,122 +346,21 @@ class InMemorySearchProvider { | ||||
|      * Gets search results from the indexedItems based on provided search | ||||
|      * input. Returns matching results from indexedItems | ||||
|      */ | ||||
|     localSearchForObjects(queryId, searchInput, maxResults) { | ||||
|     localSearch(queryId, searchInput, maxResults) { | ||||
|         // This results dictionary will have domain object ID keys which | ||||
|         // point to the value the domain object's score. | ||||
|         let results = []; | ||||
|         let results; | ||||
|         const input = searchInput.trim().toLowerCase(); | ||||
|         const message = { | ||||
|             request: 'searchForObjects', | ||||
|             results: [], | ||||
|             request: 'search', | ||||
|             results: {}, | ||||
|             total: 0, | ||||
|             queryId | ||||
|         }; | ||||
|  | ||||
|         results = Object.values(this.localIndexedDomainObjects).filter((indexedItem) => { | ||||
|         results = Object.values(this.localIndexedItems).filter((indexedItem) => { | ||||
|             return indexedItem.name.toLowerCase().includes(input); | ||||
|         }) || []; | ||||
|  | ||||
|         message.total = results.length; | ||||
|         message.results = results | ||||
|             .slice(0, maxResults); | ||||
|         const eventToReturn = { | ||||
|             data: message | ||||
|         }; | ||||
|         this.onWorkerMessage(eventToReturn); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * A local version of the same SharedWorker function | ||||
|      * if we don't have SharedWorkers available (e.g., iOS) | ||||
|      */ | ||||
|     localSearchForAnnotations(queryId, searchInput, maxResults) { | ||||
|         // This results dictionary will have domain object ID keys which | ||||
|         // point to the value the domain object's score. | ||||
|         let results = []; | ||||
|         const message = { | ||||
|             request: 'searchForAnnotations', | ||||
|             results: [], | ||||
|             total: 0, | ||||
|             queryId | ||||
|         }; | ||||
|  | ||||
|         results = this.localIndexedAnnotationsByDomainObject[searchInput] || []; | ||||
|  | ||||
|         message.total = results.length; | ||||
|         message.results = results | ||||
|             .slice(0, maxResults); | ||||
|         const eventToReturn = { | ||||
|             data: message | ||||
|         }; | ||||
|         this.onWorkerMessage(eventToReturn); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * A local version of the same SharedWorker function | ||||
|      * if we don't have SharedWorkers available (e.g., iOS) | ||||
|      */ | ||||
|     localSearchForTags(queryId, matchingTagKeys, maxResults) { | ||||
|         let results = []; | ||||
|         const message = { | ||||
|             request: 'searchForTags', | ||||
|             results: [], | ||||
|             total: 0, | ||||
|             queryId | ||||
|         }; | ||||
|  | ||||
|         if (matchingTagKeys) { | ||||
|             matchingTagKeys.forEach(matchingTag => { | ||||
|                 const matchingAnnotations = this.localIndexedAnnotationsByTag[matchingTag]; | ||||
|                 if (matchingAnnotations) { | ||||
|                     matchingAnnotations.forEach(matchingAnnotation => { | ||||
|                         const existsInResults = results.some(indexedObject => { | ||||
|                             return matchingAnnotation.keyString === indexedObject.keyString; | ||||
|                         }); | ||||
|                         if (!existsInResults) { | ||||
|                             results.push(matchingAnnotation); | ||||
|                         } | ||||
|                     }); | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         message.total = results.length; | ||||
|         message.results = results | ||||
|             .slice(0, maxResults); | ||||
|         const eventToReturn = { | ||||
|             data: message | ||||
|         }; | ||||
|         this.onWorkerMessage(eventToReturn); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * A local version of the same SharedWorker function | ||||
|      * if we don't have SharedWorkers available (e.g., iOS) | ||||
|      */ | ||||
|     localSearchForNotebookAnnotations(queryId, {entryId, targetKeyString}, maxResults) { | ||||
|         // This results dictionary will have domain object ID keys which | ||||
|         // point to the value the domain object's score. | ||||
|         let results = []; | ||||
|         const message = { | ||||
|             request: 'searchForNotebookAnnotations', | ||||
|             results: [], | ||||
|             total: 0, | ||||
|             queryId | ||||
|         }; | ||||
|  | ||||
|         const matchingAnnotations = this.localIndexedAnnotationsByDomainObject[targetKeyString]; | ||||
|         if (matchingAnnotations) { | ||||
|             results = matchingAnnotations.filter(matchingAnnotation => { | ||||
|                 if (!matchingAnnotation.targets) { | ||||
|                     return false; | ||||
|                 } | ||||
|  | ||||
|                 const target = matchingAnnotation.targets[targetKeyString]; | ||||
|  | ||||
|                 return (target && target.entryId && (target.entryId === entryId)); | ||||
|             }); | ||||
|         } | ||||
|         }); | ||||
|  | ||||
|         message.total = results.length; | ||||
|         message.results = results | ||||
|   | ||||
| @@ -26,27 +26,16 @@ | ||||
| (function () { | ||||
|     // An object composed of domain object IDs and models | ||||
|     // {id: domainObject's ID, name: domainObject's name} | ||||
|     const indexedDomainObjects = {}; | ||||
|     const indexedAnnotationsByDomainObject = {}; | ||||
|     const indexedAnnotationsByTag = {}; | ||||
|     const indexedItems = {}; | ||||
|  | ||||
|     self.onconnect = function (e) { | ||||
|         const port = e.ports[0]; | ||||
|  | ||||
|         port.onmessage = function (event) { | ||||
|             const requestType = event.data.request; | ||||
|             if (requestType === 'index') { | ||||
|             if (event.data.request === 'index') { | ||||
|                 indexItem(event.data.keyString, event.data.model); | ||||
|             } else if (requestType === 'OBJECTS') { | ||||
|                 port.postMessage(searchForObjects(event.data)); | ||||
|             } else if (requestType === 'ANNOTATIONS') { | ||||
|                 port.postMessage(searchForAnnotations(event.data)); | ||||
|             } else if (requestType === 'TAGS') { | ||||
|                 port.postMessage(searchForTags(event.data)); | ||||
|             } else if (requestType === 'NOTEBOOK_ANNOTATIONS') { | ||||
|                 port.postMessage(searchForNotebookAnnotations(event.data)); | ||||
|             } else { | ||||
|                 throw new Error(`Unknown request ${event.data.request}`); | ||||
|             } else if (event.data.request === 'search') { | ||||
|                 port.postMessage(search(event.data)); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
| @@ -59,70 +48,12 @@ | ||||
|         console.error('Error on feed', error); | ||||
|     }; | ||||
|  | ||||
|     function indexAnnotation(objectToIndex, model) { | ||||
|         Object.keys(model.targets).forEach(targetID => { | ||||
|             if (!indexedAnnotationsByDomainObject[targetID]) { | ||||
|                 indexedAnnotationsByDomainObject[targetID] = []; | ||||
|             } | ||||
|  | ||||
|             objectToIndex.targets = model.targets; | ||||
|             objectToIndex.tags = model.tags; | ||||
|             const existsInIndex = indexedAnnotationsByDomainObject[targetID].some(indexedObject => { | ||||
|                 return indexedObject.keyString === objectToIndex.keyString; | ||||
|             }); | ||||
|  | ||||
|             if (!existsInIndex) { | ||||
|                 indexedAnnotationsByDomainObject[targetID].push(objectToIndex); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     function indexTags(keyString, objectToIndex, model) { | ||||
|         // add new tags | ||||
|         model.tags.forEach(tagID => { | ||||
|             if (!indexedAnnotationsByTag[tagID]) { | ||||
|                 indexedAnnotationsByTag[tagID] = []; | ||||
|             } | ||||
|  | ||||
|             const existsInIndex = indexedAnnotationsByTag[tagID].some(indexedObject => { | ||||
|                 return indexedObject.keyString === objectToIndex.keyString; | ||||
|             }); | ||||
|  | ||||
|             if (!existsInIndex) { | ||||
|                 indexedAnnotationsByTag[tagID].push(objectToIndex); | ||||
|             } | ||||
|  | ||||
|         }); | ||||
|         // remove old tags | ||||
|         const tagsToRemoveFromIndex = Object.keys(indexedAnnotationsByTag).filter(indexedTag => { | ||||
|             return !(model.tags.includes(indexedTag)); | ||||
|         }); | ||||
|         tagsToRemoveFromIndex.forEach(tagToRemoveFromIndex => { | ||||
|             indexedAnnotationsByTag[tagToRemoveFromIndex] = indexedAnnotationsByTag[tagToRemoveFromIndex].filter(indexedAnnotation => { | ||||
|                 const shouldKeep = indexedAnnotation.keyString !== keyString; | ||||
|  | ||||
|                 return shouldKeep; | ||||
|             }); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     function indexItem(keyString, model) { | ||||
|         const objectToIndex = { | ||||
|         indexedItems[keyString] = { | ||||
|             type: model.type, | ||||
|             name: model.name, | ||||
|             keyString | ||||
|         }; | ||||
|         if (model && (model.type === 'annotation')) { | ||||
|             if (model.targets) { | ||||
|                 indexAnnotation(objectToIndex, model); | ||||
|             } | ||||
|  | ||||
|             if (model.tags) { | ||||
|                 indexTags(keyString, objectToIndex, model); | ||||
|             } | ||||
|         } else { | ||||
|             indexedDomainObjects[keyString] = objectToIndex; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -134,98 +65,21 @@ | ||||
|      *           * maxResults: The maximum number of search results desired | ||||
|      *           * queryId: an id identifying this query, will be returned. | ||||
|      */ | ||||
|     function searchForObjects(data) { | ||||
|         let results = []; | ||||
|     function search(data) { | ||||
|         // This results dictionary will have domain object ID keys which | ||||
|         // point to the value the domain object's score. | ||||
|         let results; | ||||
|         const input = data.input.trim().toLowerCase(); | ||||
|         const message = { | ||||
|             request: 'searchForObjects', | ||||
|             results: [], | ||||
|             total: 0, | ||||
|             queryId: data.queryId | ||||
|         }; | ||||
|  | ||||
|         results = Object.values(indexedDomainObjects).filter((indexedItem) => { | ||||
|             return indexedItem.name.toLowerCase().includes(input); | ||||
|         }) || []; | ||||
|  | ||||
|         message.total = results.length; | ||||
|         message.results = results | ||||
|             .slice(0, data.maxResults); | ||||
|  | ||||
|         return message; | ||||
|     } | ||||
|  | ||||
|     function searchForAnnotations(data) { | ||||
|         let results = []; | ||||
|         const message = { | ||||
|             request: 'searchForAnnotations', | ||||
|             results: [], | ||||
|             total: 0, | ||||
|             queryId: data.queryId | ||||
|         }; | ||||
|  | ||||
|         results = indexedAnnotationsByDomainObject[data.input] || []; | ||||
|  | ||||
|         message.total = results.length; | ||||
|         message.results = results | ||||
|             .slice(0, data.maxResults); | ||||
|  | ||||
|         return message; | ||||
|     } | ||||
|  | ||||
|     function searchForTags(data) { | ||||
|         let results = []; | ||||
|         const message = { | ||||
|             request: 'searchForTags', | ||||
|             results: [], | ||||
|             total: 0, | ||||
|             queryId: data.queryId | ||||
|         }; | ||||
|  | ||||
|         if (data.input) { | ||||
|             data.input.forEach(matchingTag => { | ||||
|                 const matchingAnnotations = indexedAnnotationsByTag[matchingTag]; | ||||
|                 if (matchingAnnotations) { | ||||
|                     matchingAnnotations.forEach(matchingAnnotation => { | ||||
|                         const existsInResults = results.some(indexedObject => { | ||||
|                             return matchingAnnotation.keyString === indexedObject.keyString; | ||||
|                         }); | ||||
|                         if (!existsInResults) { | ||||
|                             results.push(matchingAnnotation); | ||||
|                         } | ||||
|                     }); | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         message.total = results.length; | ||||
|         message.results = results | ||||
|             .slice(0, data.maxResults); | ||||
|  | ||||
|         return message; | ||||
|     } | ||||
|  | ||||
|     function searchForNotebookAnnotations(data) { | ||||
|         let results = []; | ||||
|         const message = { | ||||
|             request: 'searchForNotebookAnnotations', | ||||
|             request: 'search', | ||||
|             results: {}, | ||||
|             total: 0, | ||||
|             queryId: data.queryId | ||||
|         }; | ||||
|  | ||||
|         const matchingAnnotations = indexedAnnotationsByDomainObject[data.input.targetKeyString]; | ||||
|         if (matchingAnnotations) { | ||||
|             results = matchingAnnotations.filter(matchingAnnotation => { | ||||
|                 if (!matchingAnnotation.targets) { | ||||
|                     return false; | ||||
|                 } | ||||
|  | ||||
|                 const target = matchingAnnotation.targets[data.input.targetKeyString]; | ||||
|  | ||||
|                 return (target && target.entryId && (target.entryId === data.input.entryId)); | ||||
|             }); | ||||
|         } | ||||
|         results = Object.values(indexedItems).filter((indexedItem) => { | ||||
|             return indexedItem.name.toLowerCase().includes(input); | ||||
|         }); | ||||
|  | ||||
|         message.total = results.length; | ||||
|         message.results = results | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -17,16 +17,13 @@ describe("The Object API Search Function", () => { | ||||
|             openmct = createOpenMct(); | ||||
|  | ||||
|             mockObjectProvider = jasmine.createSpyObj("mock object provider", [ | ||||
|                 "search", "supportsSearchType" | ||||
|                 "search" | ||||
|             ]); | ||||
|             anotherMockObjectProvider = jasmine.createSpyObj("another mock object provider", [ | ||||
|                 "search", "supportsSearchType" | ||||
|                 "search" | ||||
|             ]); | ||||
|             openmct.objects.addProvider('objects', mockObjectProvider); | ||||
|             openmct.objects.addProvider('other-objects', anotherMockObjectProvider); | ||||
|             mockObjectProvider.supportsSearchType.and.callFake(() => { | ||||
|                 return true; | ||||
|             }); | ||||
|             mockObjectProvider.search.and.callFake(() => { | ||||
|                 return new Promise(resolve => { | ||||
|                     const mockProviderSearch = { | ||||
| @@ -41,9 +38,6 @@ describe("The Object API Search Function", () => { | ||||
|                     }, MOCK_PROVIDER_SEARCH_DELAY); | ||||
|                 }); | ||||
|             }); | ||||
|             anotherMockObjectProvider.supportsSearchType.and.callFake(() => { | ||||
|                 return true; | ||||
|             }); | ||||
|             anotherMockObjectProvider.search.and.callFake(() => { | ||||
|                 return new Promise(resolve => { | ||||
|                     const anotherMockProviderSearch = { | ||||
| @@ -116,8 +110,8 @@ describe("The Object API Search Function", () => { | ||||
|                 namespace: '' | ||||
|             }); | ||||
|             openmct.objects.addProvider('foo', defaultObjectProvider); | ||||
|             spyOn(openmct.objects.inMemorySearchProvider, "search").and.callThrough(); | ||||
|             spyOn(openmct.objects.inMemorySearchProvider, "localSearchForObjects").and.callThrough(); | ||||
|             spyOn(openmct.objects.inMemorySearchProvider, "query").and.callThrough(); | ||||
|             spyOn(openmct.objects.inMemorySearchProvider, "localSearch").and.callThrough(); | ||||
|  | ||||
|             openmct.on('start', async () => { | ||||
|                 mockIdentifier1 = { | ||||
| @@ -161,7 +155,7 @@ describe("The Object API Search Function", () => { | ||||
|  | ||||
|         it("can provide indexing without a provider", () => { | ||||
|             openmct.objects.search('foo'); | ||||
|             expect(openmct.objects.inMemorySearchProvider.search).toHaveBeenCalled(); | ||||
|             expect(openmct.objects.inMemorySearchProvider.query).toHaveBeenCalled(); | ||||
|         }); | ||||
|  | ||||
|         it("can do partial search", async () => { | ||||
| @@ -183,22 +177,16 @@ describe("The Object API Search Function", () => { | ||||
|         }); | ||||
|  | ||||
|         describe("Without Shared Workers", () => { | ||||
|             let sharedWorkerToRestore; | ||||
|             beforeEach(async () => { | ||||
|                 // use local worker | ||||
|                 sharedWorkerToRestore = openmct.objects.inMemorySearchProvider.worker; | ||||
|                 openmct.objects.inMemorySearchProvider.worker = null; | ||||
|                 // reindex locally | ||||
|                 await openmct.objects.inMemorySearchProvider.index(mockDomainObject1); | ||||
|                 await openmct.objects.inMemorySearchProvider.index(mockDomainObject2); | ||||
|                 await openmct.objects.inMemorySearchProvider.index(mockDomainObject3); | ||||
|             }); | ||||
|             afterEach(() => { | ||||
|                 openmct.objects.inMemorySearchProvider.worker = sharedWorkerToRestore; | ||||
|             }); | ||||
|             it("calls local search", () => { | ||||
|                 openmct.objects.search('foo'); | ||||
|                 expect(openmct.objects.inMemorySearchProvider.localSearchForObjects).toHaveBeenCalled(); | ||||
|                 expect(openmct.objects.inMemorySearchProvider.localSearch).toHaveBeenCalled(); | ||||
|             }); | ||||
|  | ||||
|             it("can do partial search", async () => { | ||||
|   | ||||
| @@ -7,7 +7,6 @@ | ||||
|     <div class="c-overlay__outer"> | ||||
|         <button | ||||
|             v-if="dismissable" | ||||
|             aria-label="Close" | ||||
|             class="c-click-icon c-overlay__close-button icon-x" | ||||
|             @click="destroy" | ||||
|         ></button> | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user