Compare commits
	
		
			55 Commits
		
	
	
		
			moment-bum
			...
			operator-s
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | a181faff4e | ||
|   | 10c26ac39a | ||
|   | 7d3d68277b | ||
|   | a34190f44c | ||
|   | f6f8db3142 | ||
|   | 4619ab23f5 | ||
|   | 8bcbb776c5 | ||
|   | ab93b61530 | ||
|   | 35e2d2c643 | ||
|   | 3663f22807 | ||
|   | d58111c0ce | ||
|   | be8e9ba605 | ||
|   | 0d6359bc58 | ||
|   | 60d759f76c | ||
|   | 28e3d2e066 | ||
|   | 934285b3fb | ||
|   | fe7b368cbd | ||
|   | f8838e5a6e | ||
|   | aabde3df80 | ||
|   | c51cf72fa7 | ||
|   | 72cbd3dd3b | ||
|   | d93ed2b69c | ||
|   | 22e45848af | ||
|   | c94bab7964 | ||
|   | 63363ccbb9 | ||
|   | bdfc99ed03 | ||
|   | 458e9211e9 | ||
|   | 5399e06370 | ||
|   | e76fed4005 | ||
|   | f4af7aa2f4 | ||
|   | ad081c0db7 | ||
|   | 32b1ccff5b | ||
|   | 343ea86f48 | ||
|   | 98e0fc5bfb | ||
|   | fb2cbc72ba | ||
|   | c85e6904a3 | ||
|   | 6918d7d3d9 | ||
|   | 814d8a8380 | ||
|   | 5035403507 | ||
|   | 1fc082afe5 | ||
|   | 441e24a78a | ||
|   | 7ec02258e8 | ||
|   | 45c66b758b | ||
|   | e197d39ce0 | ||
|   | c8a4ff09f8 | ||
|   | 2550d93b1e | ||
|   | 7bcf136f43 | ||
|   | 245d61adda | ||
|   | 18b05b6faf | ||
|   | b950031eae | ||
|   | a72aa10d7b | ||
|   | c5ff94ad7a | ||
|   | e75cc35cb7 | ||
|   | f216bd8769 | ||
|   | 79a430278e | 
| @@ -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.21.1-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,14 +58,10 @@ 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 | ||||
| @@ -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: | ||||
| @@ -220,9 +192,8 @@ workflows: | ||||
|           suite: full | ||||
|     triggers: | ||||
|       - schedule: | ||||
|           cron: "0 0,4,8,12,16,20 * * *" | ||||
|           cron: "0 0 * * *" | ||||
|           filters: | ||||
|             branches: | ||||
|               only: | ||||
|                 - master | ||||
|                 - release/2.0.5 | ||||
|   | ||||
| @@ -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", | ||||
|   | ||||
							
								
								
									
										3
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.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: | ||||
| @@ -41,7 +41,6 @@ assignees: '' | ||||
| - [ ] 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 --> | ||||
|   | ||||
							
								
								
									
										3
									
								
								.github/workflows/e2e-pr.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.github/workflows/e2e-pr.yml
									
									
									
									
										vendored
									
									
								
							| @@ -30,8 +30,7 @@ jobs: | ||||
|       - uses: actions/setup-node@v3 | ||||
|         with: | ||||
|           node-version: '16' | ||||
|       - run: npx playwright@1.23.0 install | ||||
|       - run: npx playwright install chrome-beta | ||||
|       - run: npx playwright@1.21.1 install | ||||
|       - run: npm install | ||||
|       - run: npm run test:e2e:full | ||||
|       - name: Archive test results | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/workflows/e2e-visual.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/e2e-visual.yml
									
									
									
									
										vendored
									
									
								
							| @@ -17,7 +17,7 @@ jobs: | ||||
|       - uses: actions/setup-node@v3 | ||||
|         with: | ||||
|           node-version: '16' | ||||
|       - run: npx playwright@1.23.0 install | ||||
|       - run: npx playwright@1.21.1 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 | ||||
|   | ||||
							
								
								
									
										32
									
								
								app.js
									
									
									
									
									
								
							
							
						
						
									
										32
									
								
								app.js
									
									
									
									
									
								
							| @@ -12,7 +12,6 @@ const express = require('express'); | ||||
| const app = express(); | ||||
| const fs = require('fs'); | ||||
| const request = require('request'); | ||||
| const __DEV__ = !process.env.NODE_ENV || process.env.NODE_ENV === 'development'; | ||||
|  | ||||
| // Defaults | ||||
| options.port = options.port || options.p || 8080; | ||||
| @@ -50,18 +49,14 @@ class WatchRunPlugin { | ||||
| } | ||||
|  | ||||
| const webpack = require('webpack'); | ||||
| let webpackConfig; | ||||
| if (__DEV__) { | ||||
|     webpackConfig = require('./webpack.dev'); | ||||
|     webpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin()); | ||||
|     webpackConfig.entry.openmct = [ | ||||
|         'webpack-hot-middleware/client?reload=true', | ||||
|         webpackConfig.entry.openmct | ||||
|     ]; | ||||
|     webpackConfig.plugins.push(new WatchRunPlugin()); | ||||
| } else { | ||||
|     webpackConfig = require('./webpack.coverage'); | ||||
| } | ||||
| const webpackConfig = require('./webpack.dev.js'); | ||||
| webpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin()); | ||||
| webpackConfig.plugins.push(new WatchRunPlugin()); | ||||
|  | ||||
| webpackConfig.entry.openmct = [ | ||||
|     'webpack-hot-middleware/client?reload=true', | ||||
|     webpackConfig.entry.openmct | ||||
| ]; | ||||
|  | ||||
| const compiler = webpack(webpackConfig); | ||||
|  | ||||
| @@ -73,12 +68,10 @@ app.use(require('webpack-dev-middleware')( | ||||
|     } | ||||
| )); | ||||
|  | ||||
| if (__DEV__) { | ||||
|     app.use(require('webpack-hot-middleware')( | ||||
|         compiler, | ||||
|         {} | ||||
|     )); | ||||
| } | ||||
| app.use(require('webpack-hot-middleware')( | ||||
|     compiler, | ||||
|     {} | ||||
| )); | ||||
|  | ||||
| // Expose index.html for development users. | ||||
| app.get('/', function (req, res) { | ||||
| @@ -89,4 +82,3 @@ app.get('/', function (req, res) { | ||||
| app.listen(options.port, options.host, function () { | ||||
|     console.log('Open MCT application running at %s:%s', options.host, options.port); | ||||
| }); | ||||
|  | ||||
|   | ||||
							
								
								
									
										17
									
								
								codecov.yml
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								codecov.yml
									
									
									
									
									
								
							| @@ -13,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,60 +1,18 @@ | ||||
| /* 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. | ||||
|  */ | ||||
| /* eslint-disable no-undef */ | ||||
|  | ||||
| // This file extends the base functionality of the playwright test framework | ||||
| const base = require('@playwright/test'); | ||||
| const { expect } = require('@playwright/test'); | ||||
| 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)); | ||||
|         page.on('console', msg => messages.push(`[${msg.type()}] ${msg.text()}`)); | ||||
|         await use(page); | ||||
|         messages.forEach( | ||||
|             msg => expect.soft(msg.type(), `Console error detected: ${consoleMessageToString(msg)}`).not.toEqual('error') | ||||
|         ); | ||||
|         await expect.soft(messages.toString()).not.toContain('[error]'); | ||||
|     }, | ||||
|     browser: async ({ playwright, browser }, use, workerInfo) => { | ||||
|         // Use browserless if configured | ||||
|     // Use browserless if configured | ||||
|         if (workerInfo.project.name.match(/browserless/)) { | ||||
|             const vBrowser = await playwright.chromium.connectOverCDP({ | ||||
|                 endpointURL: 'ws://localhost:3003' | ||||
|   | ||||
| @@ -4,30 +4,26 @@ | ||||
|  | ||||
| // eslint-disable-next-line no-unused-vars | ||||
| const { devices } = require('@playwright/test'); | ||||
| const MAX_FAILURES = 5; | ||||
| const NUM_WORKERS = 2; | ||||
|  | ||||
| /** @type {import('@playwright/test').PlaywrightTestConfig} */ | ||||
| const config = { | ||||
|     retries: 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: 1, | ||||
|     testDir: 'tests', | ||||
|     testIgnore: '**/*.perf.spec.js', //Ignore performance tests and define in playwright-perfromance.config.js | ||||
|     timeout: 60 * 1000, | ||||
|     webServer: { | ||||
|         command: 'cross-env NODE_ENV=test npm run start', | ||||
|         url: 'http://localhost:8080/#', | ||||
|         command: 'npm run start', | ||||
|         port: 8080, | ||||
|         timeout: 200 * 1000, | ||||
|         reuseExistingServer: false | ||||
|         reuseExistingServer: !process.env.CI | ||||
|     }, | ||||
|     maxFailures: MAX_FAILURES, //Limits failures to 5 to reduce CI Waste | ||||
|     workers: NUM_WORKERS, //Limit to 2 for CircleCI Agent | ||||
|     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: 'off' | ||||
|         screenshot: 'on', | ||||
|         trace: 'on', | ||||
|         video: 'on' | ||||
|     }, | ||||
|     projects: [ | ||||
|         { | ||||
| @@ -38,7 +34,6 @@ const config = { | ||||
|         }, | ||||
|         { | ||||
|             name: 'MMOC', | ||||
|             testMatch: '**/*.e2e.spec.js', // only run e2e tests | ||||
|             grepInvert: /@snapshot/, | ||||
|             use: { | ||||
|                 browserName: 'chromium', | ||||
| @@ -47,32 +42,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'] | ||||
|     ] | ||||
| }; | ||||
|   | ||||
| @@ -9,13 +9,12 @@ const { devices } = require('@playwright/test'); | ||||
| const config = { | ||||
|     retries: 0, | ||||
|     testDir: 'tests', | ||||
|     testIgnore: '**/*.perf.spec.js', | ||||
|     timeout: 30 * 1000, | ||||
|     webServer: { | ||||
|         command: 'cross-env NODE_ENV=test npm run start', | ||||
|         url: 'http://localhost:8080/#', | ||||
|         command: 'npm run start', | ||||
|         port: 8080, | ||||
|         timeout: 120 * 1000, | ||||
|         reuseExistingServer: true | ||||
|         reuseExistingServer: !process.env.CI | ||||
|     }, | ||||
|     workers: 1, | ||||
|     use: { | ||||
| @@ -23,9 +22,9 @@ const config = { | ||||
|         baseURL: 'http://localhost:8080/', | ||||
|         headless: false, | ||||
|         ignoreHTTPSErrors: true, | ||||
|         screenshot: 'only-on-failure', | ||||
|         trace: 'retain-on-failure', | ||||
|         video: 'off' | ||||
|         screenshot: 'on', | ||||
|         trace: 'on', | ||||
|         video: 'on' | ||||
|     }, | ||||
|     projects: [ | ||||
|         { | ||||
| @@ -36,7 +35,6 @@ const config = { | ||||
|         }, | ||||
|         { | ||||
|             name: 'MMOC', | ||||
|             testMatch: '**/*.e2e.spec.js', // only run e2e tests | ||||
|             grepInvert: /@snapshot/, | ||||
|             use: { | ||||
|                 browserName: 'chromium', | ||||
| @@ -45,59 +43,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,43 +0,0 @@ | ||||
| /* eslint-disable no-undef */ | ||||
| // playwright.config.js | ||||
| // @ts-check | ||||
|  | ||||
| const CI = process.env.CI === 'true'; | ||||
|  | ||||
| /** @type {import('@playwright/test').PlaywrightTestConfig} */ | ||||
| const config = { | ||||
|     retries: 1, //Only for debugging purposes because trace is enabled only on first retry | ||||
|     testDir: 'tests/performance/', | ||||
|     timeout: 60 * 1000, | ||||
|     workers: 1, //Only run in serial with 1 worker | ||||
|     webServer: { | ||||
|         command: 'cross-env NODE_ENV=test npm run start', | ||||
|         url: 'http://localhost:8080/#', | ||||
|         timeout: 200 * 1000, | ||||
|         reuseExistingServer: !CI | ||||
|     }, | ||||
|     use: { | ||||
|         browserName: "chromium", | ||||
|         baseURL: 'http://localhost:8080/', | ||||
|         headless: CI, //Only if running locally | ||||
|         ignoreHTTPSErrors: true, | ||||
|         screenshot: 'off', | ||||
|         trace: 'on-first-retry', | ||||
|         video: 'off' | ||||
|     }, | ||||
|     projects: [ | ||||
|         { | ||||
|             name: 'chrome', | ||||
|             use: { | ||||
|                 browserName: 'chromium' | ||||
|             } | ||||
|         } | ||||
|     ], | ||||
|     reporter: [ | ||||
|         ['list'], | ||||
|         ['junit', { outputFile: 'test-results/results.xml' }], | ||||
|         ['json', { outputFile: 'test-results/results.json' }] | ||||
|     ] | ||||
| }; | ||||
|  | ||||
| module.exports = config; | ||||
| @@ -4,28 +4,29 @@ | ||||
|  | ||||
| /** @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: 'cross-env NODE_ENV=test npm run start', | ||||
|         url: 'http://localhost:8080/#', | ||||
|         command: 'npm run start', | ||||
|         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', | ||||
|         video: 'off' | ||||
|         video: 'on' | ||||
|     }, | ||||
|     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": "[]" | ||||
|         } | ||||
|       ] | ||||
|     } | ||||
|   ] | ||||
| } | ||||
| @@ -43,6 +43,8 @@ test.describe('forms set', () => { | ||||
|         await page.fill('text=Properties Title Notes >> input[type="text"]', ''); | ||||
|         // Press Tab | ||||
|         await page.press('text=Properties Title Notes >> input[type="text"]', 'Tab'); | ||||
|         // Click text=OK Cancel | ||||
|         await page.click('text=OK', { force: true }); | ||||
|  | ||||
|         const okButton = page.locator('text=OK'); | ||||
|  | ||||
|   | ||||
| @@ -36,7 +36,7 @@ test.describe('Branding tests', () => { | ||||
|         await page.click('.l-shell__app-logo'); | ||||
|  | ||||
|         // Verify that the NASA Logo Appears | ||||
|         await expect(page.locator('.c-about__image')).toBeVisible(); | ||||
|         await expect(await page.locator('.c-about__image')).toBeVisible(); | ||||
|  | ||||
|         // Modify the Build information in 'about' Modal | ||||
|         const versionInformationLocator = page.locator('ul.t-info.l-info.s-info'); | ||||
| @@ -58,7 +58,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(); | ||||
|     }); | ||||
| }); | ||||
|   | ||||
| @@ -28,9 +28,7 @@ const { test } = require('../../../fixtures.js'); | ||||
| const { expect } = require('@playwright/test'); | ||||
|  | ||||
| test.describe('Sine Wave Generator', () => { | ||||
|     test('Create new Sine Wave Generator Object and validate create Form Logic', async ({ page, 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 +40,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 +90,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 +151,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, | ||||
|   | ||||
| @@ -1,55 +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 testing our use of the playwright framework as it | ||||
| relates to how we've extended it (i.e. ./e2e/fixtures.js) and assumptions made in our dev environment | ||||
| (app.js and ./e2e/webpack-dev-middleware.js) | ||||
| */ | ||||
|  | ||||
| const { test } = require('../fixtures.js'); | ||||
|  | ||||
| test.describe('fixtures.js tests', () => { | ||||
|     test('Verify that tests fail if console.error is thrown', async ({ page }) => { | ||||
|         test.fail(); | ||||
|         //Go to baseURL | ||||
|         await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|         //Verify that ../fixtures.js detects console log errors | ||||
|         await Promise.all([ | ||||
|             page.evaluate(() => console.error('This should result in a failure')), | ||||
|             page.waitForEvent('console') // always wait for the event to happen while triggering it! | ||||
|         ]); | ||||
|  | ||||
|     }); | ||||
|     test('Verify that tests pass if console.warn is thrown', async ({ page }) => { | ||||
|         //Go to baseURL | ||||
|         await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|         //Verify that ../fixtures.js detects console log errors | ||||
|         await Promise.all([ | ||||
|             page.evaluate(() => console.warn('This should result in a pass')), | ||||
|             page.waitForEvent('console') // always wait for the event to happen while triggering it! | ||||
|         ]); | ||||
|  | ||||
|     }); | ||||
| }); | ||||
| @@ -39,7 +39,6 @@ test.describe('Move item tests', () => { | ||||
|  | ||||
|         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(), | ||||
| @@ -55,7 +54,6 @@ test.describe('Move item tests', () => { | ||||
|         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(), | ||||
| @@ -74,8 +72,10 @@ test.describe('Move item tests', () => { | ||||
|         }); | ||||
|         await page.locator('li.icon-move').click(); | ||||
|         await page.locator('form[name="mctForm"] >> text=My Items').click(); | ||||
|  | ||||
|         await page.locator('text=OK').click(); | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             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(); | ||||
| @@ -90,8 +90,10 @@ test.describe('Move item tests', () => { | ||||
|         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(); | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('text=OK').click() | ||||
|         ]); | ||||
|  | ||||
|         // Finish editing and save Telemetry Table | ||||
|         await page.locator('.c-button--menu.c-button--major.icon-save').click(); | ||||
| @@ -112,7 +114,10 @@ test.describe('Move item tests', () => { | ||||
|  | ||||
|         // 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(); | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('text=OK').click() | ||||
|         ]); | ||||
|  | ||||
|         // Open My Items | ||||
|         await page.locator('text=Open MCT My Items >> span').nth(3).click(); | ||||
|   | ||||
| @@ -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); | ||||
|     }); | ||||
| }); | ||||
| @@ -28,7 +28,9 @@ const { test } = require('../../fixtures.js'); | ||||
| const { 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 +38,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' }); | ||||
|  | ||||
|   | ||||
| @@ -46,22 +46,22 @@ test.describe('Clock Generator', () => { | ||||
|         // Click .icon-arrow-down | ||||
|         await page.locator('.icon-arrow-down').click(); | ||||
|         //verify if the autocomplete dropdown is visible | ||||
|         await expect(page.locator(".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(); | ||||
|  | ||||
|     }); | ||||
| }); | ||||
|   | ||||
| @@ -32,49 +32,49 @@ const { 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 = page.url(); | ||||
|         console.log('conditionSetUrl ' + conditionSetUrl); | ||||
|  | ||||
|         getConditionSetIdentifierFromUrl = conditionSetUrl.split('/').pop().split('?')[0]; | ||||
|         console.debug('getConditionSetIdentifierFromUrl ' + getConditionSetIdentifierFromUrl); | ||||
|         await page.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 | ||||
|         expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy(); | ||||
|         await expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy; | ||||
|  | ||||
|         //Reload Page | ||||
|         await Promise.all([ | ||||
| @@ -85,13 +85,13 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => { | ||||
|         //Re-verify after reload | ||||
|         await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set'); | ||||
|         //Assertions on loaded Condition Set in Inspector | ||||
|         expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy(); | ||||
|         await expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy; | ||||
|  | ||||
|     }); | ||||
|     test('condition set object can be modified on @localStorage', async ({ page }) => { | ||||
|         await page.goto(conditionSetUrl, { waitUntil: 'networkidle' }); | ||||
|  | ||||
|         //Assertions on loaded Condition Set in main view. 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 | ||||
| @@ -111,18 +111,18 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => { | ||||
|  | ||||
|         // Verify Inspector properties | ||||
|         // Verify Inspector has updated Name property | ||||
|         expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy(); | ||||
|         await expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy(); | ||||
|         // Verify Inspector Details has updated Name property | ||||
|         expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy(); | ||||
|         await expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy(); | ||||
|  | ||||
|         // Verify Tree reflects updated Name proprety | ||||
|         // Expand Tree | ||||
|         await page.locator('text=Open MCT My Items >> span >> nth=3').click(); | ||||
|         // Verify Condition Set Object is renamed in Tree | ||||
|         expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); | ||||
|         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'); | ||||
|         expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); | ||||
|         await page.locator('input[type="search"]').fill('Renamed'); | ||||
|         await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); | ||||
|  | ||||
|         //Reload Page | ||||
|         await Promise.all([ | ||||
| @@ -135,43 +135,45 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => { | ||||
|  | ||||
|         // Verify Inspector properties | ||||
|         // Verify Inspector has updated Name property | ||||
|         expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy(); | ||||
|         await expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy(); | ||||
|         // Verify Inspector Details has updated Name property | ||||
|         expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy(); | ||||
|         await expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy(); | ||||
|  | ||||
|         // Verify Tree reflects updated Name proprety | ||||
|         // Expand Tree | ||||
|         await page.locator('text=Open MCT My Items >> span >> nth=3').click(); | ||||
|         // Verify Condition Set Object is renamed in Tree | ||||
|         expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); | ||||
|         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'); | ||||
|         expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); | ||||
|         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 | ||||
|   | ||||
| @@ -29,12 +29,10 @@ but only assume that example imagery is present. | ||||
| const { test } = require('../../../fixtures.js'); | ||||
| const { 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' }); | ||||
|  | ||||
| @@ -44,6 +42,9 @@ test.describe('Example Imagery Object', () => { | ||||
|         // Click text=Example Imagery | ||||
|         await page.click('text=Example Imagery'); | ||||
|  | ||||
|         // Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184 | ||||
|         await page.click('form[name="mctForm"] a:has-text("My Items")'); | ||||
|  | ||||
|         // Click text=OK | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation({waitUntil: 'networkidle'}), | ||||
| @@ -51,58 +52,48 @@ test.describe('Example Imagery Object', () => { | ||||
|             //Wait for Save Banner to appear | ||||
|             page.waitForSelector('.c-message-banner__message') | ||||
|         ]); | ||||
|         // Close Banner | ||||
|         await page.locator('.c-message-banner__close-button').click(); | ||||
|  | ||||
|         //Wait until Save Banner is gone | ||||
|         await page.waitForSelector('.c-message-banner__message', { state: 'detached'}); | ||||
|         await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery'); | ||||
|         await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|     }); | ||||
|  | ||||
|     const backgroundImageSelector = '.c-imagery__main-image__background-image'; | ||||
|     test('Can use Mouse Wheel to zoom in and out of latest image', async ({ page }) => { | ||||
|         const bgImageLocator = await page.locator(backgroundImageSelector); | ||||
|         const deltaYStep = 100; //equivalent to 1x zoom | ||||
|         await 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); | ||||
|         expect(imageMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width); | ||||
|         expect(imageMouseZoomedOut.height).toBeLessThan(imageMouseZoomedIn.height); | ||||
|         expect(imageMouseZoomedOut.width).toBeLessThan(imageMouseZoomedIn.width); | ||||
|     }); | ||||
|  | ||||
|     test('Can adjust image brightness/contrast by dragging the sliders', async ({ page, browserName }) => { | ||||
|         test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox'); | ||||
|         // Open the image filter menu | ||||
|         await page.locator('[role=toolbar] button[title="Brightness and contrast"]').click(); | ||||
|  | ||||
|         // Drag the brightness and contrast sliders around and assert filter values | ||||
|         await dragBrightnessSliderAndAssertFilterValues(page); | ||||
|         await dragContrastSliderAndAssertFilterValues(page); | ||||
|     }); | ||||
|  | ||||
|     test('Can use alt+drag to move around image once zoomed in', async ({ page }) => { | ||||
|         const deltaYStep = 100; //equivalent to 1x zoom | ||||
|         const panHotkey = process.platform === 'linux' ? ['Control', 'Alt'] : ['Alt']; | ||||
|  | ||||
|         await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|         const bgImageLocator = await page.locator(backgroundImageSelector); | ||||
|         await bgImageLocator.hover(); | ||||
|  | ||||
|         // zoom in | ||||
|         await page.mouse.wheel(0, deltaYStep * 2); | ||||
|         await 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 | ||||
| @@ -125,7 +116,7 @@ test.describe('Example Imagery Object', () => { | ||||
|         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(); | ||||
|         const afterRightPanBoundingBox = await bgImageLocator.boundingBox(); | ||||
|         expect(zoomedBoundingBox.x).toBeGreaterThan(afterRightPanBoundingBox.x); | ||||
|  | ||||
|         // pan left | ||||
| @@ -134,7 +125,7 @@ test.describe('Example Imagery Object', () => { | ||||
|         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(); | ||||
|         const afterLeftPanBoundingBox = await bgImageLocator.boundingBox(); | ||||
|         expect(afterRightPanBoundingBox.x).toBeLessThan(afterLeftPanBoundingBox.x); | ||||
|  | ||||
|         // pan up | ||||
| @@ -144,7 +135,7 @@ test.describe('Example Imagery Object', () => { | ||||
|         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(); | ||||
|         const afterUpPanBoundingBox = await bgImageLocator.boundingBox(); | ||||
|         expect(afterUpPanBoundingBox.y).toBeGreaterThan(afterLeftPanBoundingBox.y); | ||||
|  | ||||
|         // pan down | ||||
| @@ -153,82 +144,83 @@ test.describe('Example Imagery Object', () => { | ||||
|         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(); | ||||
|         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 }, testInfo) => { | ||||
|         test.slow(testInfo.project === 'chrome-beta', "This test is slow in chrome-beta"); | ||||
|     test('Can use the reset button to reset the image', async ({ page }) => { | ||||
|         const bgImageLocator = await page.locator(backgroundImageSelector); | ||||
|         // wait for zoom animation to finish | ||||
|         await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|         await bgImageLocator.hover(); | ||||
|  | ||||
|         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 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 bgImageLocator.hover(); | ||||
|         await zoomInBtn.click(); | ||||
|         // wait for zoom animation to finish | ||||
|         await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|         await bgImageLocator.hover(); | ||||
|  | ||||
|         const zoomedInBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|         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 | ||||
|         // FIXME: The zoom is flakey, sometimes not returning to original dimensions | ||||
|         // https://github.com/nasa/openmct/issues/5491 | ||||
|         await expect.poll(async () => { | ||||
|             await zoomResetBtn.click(); | ||||
|             const boundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|         await bgImageLocator.hover(); | ||||
|  | ||||
|             return boundingBox; | ||||
|         }, { | ||||
|             timeout: 10 * 1000 | ||||
|         }).toEqual(initialBoundingBox); | ||||
|         const resetBoundingBox = await bgImageLocator.boundingBox(); | ||||
|         expect.soft(resetBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height); | ||||
|         expect.soft(resetBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width); | ||||
|  | ||||
|         expect.soft(resetBoundingBox.height).toEqual(initialBoundingBox.height); | ||||
|         expect(resetBoundingBox.width).toEqual(initialBoundingBox.width); | ||||
|     }); | ||||
|  | ||||
|     test('Using the zoom features does not pause telemetry', async ({ page }) => { | ||||
|         const bgImageLocator = page.locator(backgroundImageSelector); | ||||
|         const pausePlayButton = page.locator('.c-button.pause-play'); | ||||
|         // wait for zoom animation to finish | ||||
|         await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|         await bgImageLocator.hover(); | ||||
|  | ||||
|         // open the time conductor drop down | ||||
|         await page.locator('button:has-text("Fixed Timespan")').click(); | ||||
|         await page.locator('.c-conductor__controls button.c-mode-button').click(); | ||||
|         // Click local clock | ||||
|         await page.locator('[data-testid="conductor-modeOption-realtime"]').click(); | ||||
|         await page.locator('.icon-clock >> text=Local Clock').click(); | ||||
|  | ||||
|         await expect.soft(pausePlayButton).not.toHaveClass(/is-paused/); | ||||
|         const zoomInBtn = page.locator('.t-btn-zoom-in').nth(0); | ||||
|         const zoomInBtn = page.locator('.t-btn-zoom-in'); | ||||
|         await zoomInBtn.click(); | ||||
|         // wait for zoom animation to finish | ||||
|         await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|         await bgImageLocator.hover(); | ||||
|  | ||||
|         return expect(pausePlayButton).not.toHaveClass(/is-paused/); | ||||
|     }); | ||||
| @@ -241,13 +233,8 @@ test.describe('Example Imagery Object', () => { | ||||
| // ('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' | ||||
|     }); | ||||
|  | ||||
| const backgroundImageSelector = '.c-imagery__main-image__background-image'; | ||||
| test('Example Imagery in Display layout', async ({ page }) => { | ||||
|     // Go to baseURL | ||||
|     await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
| @@ -257,9 +244,9 @@ test('Example Imagery in Display layout', async ({ page, browserName }) => { | ||||
|     // Click text=Example Imagery | ||||
|     await page.click('text=Example Imagery'); | ||||
|  | ||||
|     // Clear and set Image load delay to minimum value | ||||
|     await page.locator('input[type="number"]').fill(''); | ||||
|     await page.locator('input[type="number"]').fill('5000'); | ||||
|     // 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([ | ||||
| @@ -268,14 +255,14 @@ test('Example Imagery in Display layout', async ({ page, browserName }) => { | ||||
|         //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}); | ||||
|     const bgImageLocator = await page.locator(backgroundImageSelector); | ||||
|     await bgImageLocator.hover(); | ||||
|  | ||||
|     // Click previous image button | ||||
|     const previousImageButton = page.locator('.c-nav--prev'); | ||||
|     const previousImageButton = await page.locator('.c-nav--prev'); | ||||
|     await previousImageButton.click(); | ||||
|  | ||||
|     // Verify previous image | ||||
| @@ -284,15 +271,15 @@ test('Example Imagery in Display layout', async ({ page, browserName }) => { | ||||
|  | ||||
|     // Zoom in | ||||
|     const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|     await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|     await bgImageLocator.hover(); | ||||
|     const deltaYStep = 100; // equivalent to 1x zoom | ||||
|     await page.mouse.wheel(0, deltaYStep * 2); | ||||
|     const zoomedBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|     const zoomedBoundingBox = await bgImageLocator.boundingBox(); | ||||
|     const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2; | ||||
|     const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2; | ||||
|  | ||||
|     // Wait for zoom animation to finish | ||||
|     await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|     await bgImageLocator.hover(); | ||||
|     const imageMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|     expect(imageMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height); | ||||
|     expect(imageMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width); | ||||
| @@ -301,26 +288,27 @@ test('Example Imagery in Display layout', async ({ page, browserName }) => { | ||||
|     await page.mouse.move(imageCenterX, imageCenterY); | ||||
|  | ||||
|     // Pan Imagery Hints | ||||
|     console.log('process.platform is ' + process.platform); | ||||
|     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'); | ||||
|     const nextImageButton = await page.locator('.c-nav--next'); | ||||
|     await nextImageButton.click(); | ||||
|  | ||||
|     // Click time conductor mode button | ||||
|     await page.locator('.c-mode-button').click(); | ||||
|     // Click fixed timespan button | ||||
|     await page.locator('.c-button__label >> text=Fixed Timespan').click(); | ||||
|  | ||||
|     // Select local clock mode | ||||
|     await page.locator('[data-testid=conductor-modeOption-realtime]').click(); | ||||
|     // Click local clock | ||||
|     await page.locator('.icon-clock >> text=Local Clock').click(); | ||||
|  | ||||
|     // Zoom in on next image | ||||
|     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 imageNextMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|     expect(imageNextMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height); | ||||
|     expect(imageNextMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width); | ||||
| @@ -331,258 +319,38 @@ test('Example Imagery in Display layout', async ({ page, browserName }) => { | ||||
|     // 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(); | ||||
|     // Wait 20ms to verify no new image has come in | ||||
|     await page.waitForTimeout(21); | ||||
|  | ||||
|     //Get background-image url from background-image css prop | ||||
|     await assertBackgroundImageUrlFromBackgroundCss(page); | ||||
|  | ||||
|     // Open the image filter menu | ||||
|     await page.locator('[role=toolbar] button[title="Brightness and contrast"]').click(); | ||||
|  | ||||
|     // Drag the brightness and contrast sliders around and assert filter values | ||||
|     await dragBrightnessSliderAndAssertFilterValues(page); | ||||
|     await dragContrastSliderAndAssertFilterValues(page); | ||||
| }); | ||||
|  | ||||
| test.describe('Example imagery thumbnails resize in display layouts', () => { | ||||
|     test('Resizing the layout changes thumbnail visibility and size', async ({ page }) => { | ||||
|         await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|         const thumbsWrapperLocator = page.locator('.c-imagery__thumbs-wrapper'); | ||||
|         // Click button:has-text("Create") | ||||
|         await page.locator('button:has-text("Create")').click(); | ||||
|  | ||||
|         // Click li:has-text("Display Layout") | ||||
|         await page.locator('li:has-text("Display Layout")').click(); | ||||
|         const displayLayoutTitleField = page.locator('text=Properties Title Notes Horizontal grid (px) Vertical grid (px) Horizontal size ( >> input[type="text"]'); | ||||
|         await displayLayoutTitleField.click(); | ||||
|  | ||||
|         await displayLayoutTitleField.fill('Thumbnail Display Layout'); | ||||
|  | ||||
|         // Click text=OK | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('text=OK').click() | ||||
|         ]); | ||||
|  | ||||
|         // Click text=Snapshot Save and Finish Editing Save and Continue Editing >> button >> nth=1 | ||||
|         await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click(); | ||||
|  | ||||
|         // Click text=Save and Finish Editing | ||||
|         await page.locator('text=Save and Finish Editing').click(); | ||||
|  | ||||
|         // Click button:has-text("Create") | ||||
|         await page.locator('button:has-text("Create")').click(); | ||||
|  | ||||
|         // Click li:has-text("Example Imagery") | ||||
|         await page.locator('li:has-text("Example Imagery")').click(); | ||||
|  | ||||
|         const imageryTitleField = page.locator('text=Properties Title Notes Images url list (comma separated) Image load delay (milli >> input[type="text"]'); | ||||
|         // Click text=Properties Title Notes Images url list (comma separated) Image load delay (milli >> input[type="text"] | ||||
|         await imageryTitleField.click(); | ||||
|  | ||||
|         // Fill text=Properties Title Notes Images url list (comma separated) Image load delay (milli >> input[type="text"] | ||||
|         await imageryTitleField.fill('Thumbnail Example Imagery'); | ||||
|  | ||||
|         // Click text=OK | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('text=OK').click() | ||||
|         ]); | ||||
|  | ||||
|         // Click text=Thumbnail Example Imagery Imagery Layout Snapshot >> button >> nth=0 | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('text=Thumbnail Example Imagery Imagery Layout Snapshot >> button').first().click() | ||||
|         ]); | ||||
|  | ||||
|         // Edit mode | ||||
|         await page.locator('text=Thumbnail Display Layout Snapshot >> button').nth(3).click(); | ||||
|  | ||||
|         // Click on example imagery to expose toolbar | ||||
|         await page.locator('text=Thumbnail Example Imagery Snapshot Large View').click(); | ||||
|  | ||||
|         // expect thumbnails not be visible when first added | ||||
|         expect.soft(thumbsWrapperLocator.isHidden()).toBeTruthy(); | ||||
|  | ||||
|         // Resize the example imagery vertically to change the thumbnail visibility | ||||
|         /* | ||||
|         The following arbitrary values are added to observe the separate visual | ||||
|         conditions of the thumbnails (hidden, small thumbnails, regular thumbnails). | ||||
|         Specifically, height is set to 50px for small thumbs and 100px for regular | ||||
|         */ | ||||
|         // Click #mct-input-id-103 | ||||
|         await page.locator('#mct-input-id-103').click(); | ||||
|  | ||||
|         // Fill #mct-input-id-103 | ||||
|         await page.locator('#mct-input-id-103').fill('50'); | ||||
|  | ||||
|         expect(thumbsWrapperLocator.isVisible()).toBeTruthy(); | ||||
|         await expect(thumbsWrapperLocator).toHaveClass(/is-small-thumbs/); | ||||
|  | ||||
|         // Resize the example imagery vertically to change the thumbnail visibility | ||||
|         // Click #mct-input-id-103 | ||||
|         await page.locator('#mct-input-id-103').click(); | ||||
|  | ||||
|         // Fill #mct-input-id-103 | ||||
|         await page.locator('#mct-input-id-103').fill('100'); | ||||
|  | ||||
|         expect(thumbsWrapperLocator.isVisible()).toBeTruthy(); | ||||
|         await expect(thumbsWrapperLocator).not.toHaveClass(/is-small-thumbs/); | ||||
|     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]; | ||||
|     }); | ||||
|     let backgroundImageUrl1 = backgroundImageUrl.slice(1, -1); //forgive me, padre | ||||
|     console.log('backgroundImageUrl1 ' + backgroundImageUrl1); | ||||
|  | ||||
|     // sleep 21ms | ||||
|     await page.waitForTimeout(21); | ||||
|  | ||||
|     // Verify next image has updated | ||||
|     let backgroundImageUrlNext = await backgroundImage.evaluate((el) => { | ||||
|         return window.getComputedStyle(el).getPropertyValue('background-image').match(/url\(([^)]+)\)/)[1]; | ||||
|     }); | ||||
|     let backgroundImageUrl2 = backgroundImageUrlNext.slice(1, -1); //forgive me, padre | ||||
|     console.log('backgroundImageUrl2 ' + backgroundImageUrl2); | ||||
|  | ||||
|     // Expect backgroundImageUrl2 to be greater then backgroundImageUrl1 | ||||
|     expect(backgroundImageUrl2 >= backgroundImageUrl1); | ||||
| }); | ||||
|  | ||||
| // test.fixme('Can use Mouse Wheel to zoom in and out of previous image'); | ||||
| // test.fixme('Can use alt+drag to move around image once zoomed in'); | ||||
| // test.fixme('Clicking on the left arrow should pause the imagery and go to previous image'); | ||||
| // test.fixme('If the imagery view is in pause mode, images still come in'); | ||||
| // test.fixme('If the imagery view is not in pause mode, it should be updated when new images come in'); | ||||
| test.describe('Example Imagery in Flexible layout', () => { | ||||
|     test('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.fixme('Can use Mouse Wheel to zoom in and out of previous image'); | ||||
|     test.fixme('Can use alt+drag to move around image once zoomed in'); | ||||
|     test.fixme('Can zoom into the latest image and the real-time/fixed-time imagery will pause'); | ||||
|     test.fixme('Clicking on the left arrow should pause the imagery and go to previous image'); | ||||
|     test.fixme('If the imagery view is in pause mode, it should not be updated when new images come in'); | ||||
|     test.fixme('If the imagery view is not in pause mode, it should be updated when new images come in'); | ||||
| }); | ||||
|  | ||||
| test.describe('Example Imagery in Tabs view', () => { | ||||
| @@ -594,186 +362,3 @@ test.describe('Example Imagery in Tabs view', () => { | ||||
|     test.fixme('If the imagery view is in pause mode, it should not be updated when new images come in'); | ||||
|     test.fixme('If the imagery view is not in pause mode, it should be updated when new images come in'); | ||||
| }); | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function saveTemplate(page) { | ||||
|     await page.locator('.c-button--menu.c-button--major.icon-save').click(); | ||||
|     await page.locator('text=Save and Finish Editing').click(); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Drag the brightness slider to max, min, and midpoint and assert the filter values | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function dragBrightnessSliderAndAssertFilterValues(page) { | ||||
|     const brightnessSlider = 'div.c-image-controls__slider-wrapper.icon-brightness > input'; | ||||
|     const brightnessBoundingBox = await page.locator(brightnessSlider).boundingBox(); | ||||
|     const brightnessMidX = brightnessBoundingBox.x + brightnessBoundingBox.width / 2; | ||||
|     const brightnessMidY = brightnessBoundingBox.y + brightnessBoundingBox.height / 2; | ||||
|  | ||||
|     await page.locator(brightnessSlider).hover({trial: true}); | ||||
|     await page.mouse.down(); | ||||
|     await page.mouse.move(brightnessBoundingBox.x + brightnessBoundingBox.width, brightnessMidY); | ||||
|     await assertBackgroundImageBrightness(page, '500'); | ||||
|     await page.mouse.move(brightnessBoundingBox.x, brightnessMidY); | ||||
|     await assertBackgroundImageBrightness(page, '0'); | ||||
|     await page.mouse.move(brightnessMidX, brightnessMidY); | ||||
|     await assertBackgroundImageBrightness(page, '250'); | ||||
|     await page.mouse.up(); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Drag the contrast slider to max, min, and midpoint and assert the filter values | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function dragContrastSliderAndAssertFilterValues(page) { | ||||
|     const contrastSlider = 'div.c-image-controls__slider-wrapper.icon-contrast > input'; | ||||
|     const contrastBoundingBox = await page.locator(contrastSlider).boundingBox(); | ||||
|     const contrastMidX = contrastBoundingBox.x + contrastBoundingBox.width / 2; | ||||
|     const contrastMidY = contrastBoundingBox.y + contrastBoundingBox.height / 2; | ||||
|  | ||||
|     await page.locator(contrastSlider).hover({trial: true}); | ||||
|     await page.mouse.down(); | ||||
|     await page.mouse.move(contrastBoundingBox.x + contrastBoundingBox.width, contrastMidY); | ||||
|     await assertBackgroundImageContrast(page, '500'); | ||||
|     await page.mouse.move(contrastBoundingBox.x, contrastMidY); | ||||
|     await assertBackgroundImageContrast(page, '0'); | ||||
|     await page.mouse.move(contrastMidX, contrastMidY); | ||||
|     await assertBackgroundImageContrast(page, '250'); | ||||
|     await page.mouse.up(); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Gets the filter:brightness value of the current background-image and | ||||
|  * asserts against an expected value | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  * @param {String} expected The expected brightness value | ||||
|  */ | ||||
| async function assertBackgroundImageBrightness(page, expected) { | ||||
|     const backgroundImage = page.locator('.c-imagery__main-image__background-image'); | ||||
|  | ||||
|     // Get the brightness filter value (i.e: filter: brightness(500%) => "500") | ||||
|     const actual = await backgroundImage.evaluate((el) => { | ||||
|         return el.style.filter.match(/brightness\((\d{1,3})%\)/)[1]; | ||||
|     }); | ||||
|     expect(actual).toBe(expected); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function assertBackgroundImageUrlFromBackgroundCss(page) { | ||||
|     const backgroundImage = page.locator('.c-imagery__main-image__background-image'); | ||||
|     let backgroundImageUrl = await backgroundImage.evaluate((el) => { | ||||
|         return window.getComputedStyle(el).getPropertyValue('background-image').match(/url\(([^)]+)\)/)[1]; | ||||
|     }); | ||||
|     let backgroundImageUrl1 = backgroundImageUrl.slice(1, -1); //forgive me, padre | ||||
|     console.log('backgroundImageUrl1 ' + backgroundImageUrl1); | ||||
|  | ||||
|     let backgroundImageUrl2; | ||||
|     await expect.poll(async () => { | ||||
|         // Verify next image has updated | ||||
|         let backgroundImageUrlNext = await backgroundImage.evaluate((el) => { | ||||
|             return window.getComputedStyle(el).getPropertyValue('background-image').match(/url\(([^)]+)\)/)[1]; | ||||
|         }); | ||||
|         backgroundImageUrl2 = backgroundImageUrlNext.slice(1, -1); //forgive me, padre | ||||
|  | ||||
|         return backgroundImageUrl2; | ||||
|     }, { | ||||
|         message: "verify next image has updated", | ||||
|         timeout: 6 * 1000 | ||||
|     }).not.toBe(backgroundImageUrl1); | ||||
|     console.log('backgroundImageUrl2 ' + backgroundImageUrl2); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function panZoomAndAssertImageProperties(page) { | ||||
|     const panHotkey = process.platform === 'linux' ? ['Control', 'Alt'] : ['Alt']; | ||||
|     const expectedAltText = process.platform === 'linux' ? 'Ctrl+Alt drag to pan' : 'Alt drag to pan'; | ||||
|     const imageryHintsText = await page.locator('.c-imagery__hints').innerText(); | ||||
|     expect(expectedAltText).toEqual(imageryHintsText); | ||||
|     const zoomedBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|     const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2; | ||||
|     const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2; | ||||
|  | ||||
|     // Pan right | ||||
|     await Promise.all(panHotkey.map(x => page.keyboard.down(x))); | ||||
|     await page.mouse.down(); | ||||
|     await page.mouse.move(imageCenterX - 200, imageCenterY, 10); | ||||
|     await page.mouse.up(); | ||||
|     await Promise.all(panHotkey.map(x => page.keyboard.up(x))); | ||||
|     const afterRightPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|     expect(zoomedBoundingBox.x).toBeGreaterThan(afterRightPanBoundingBox.x); | ||||
|  | ||||
|     // Pan left | ||||
|     await Promise.all(panHotkey.map(x => page.keyboard.down(x))); | ||||
|     await page.mouse.down(); | ||||
|     await page.mouse.move(imageCenterX, imageCenterY, 10); | ||||
|     await page.mouse.up(); | ||||
|     await Promise.all(panHotkey.map(x => page.keyboard.up(x))); | ||||
|     const afterLeftPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|     expect(afterRightPanBoundingBox.x).toBeLessThan(afterLeftPanBoundingBox.x); | ||||
|  | ||||
|     // Pan up | ||||
|     await page.mouse.move(imageCenterX, imageCenterY); | ||||
|     await Promise.all(panHotkey.map(x => page.keyboard.down(x))); | ||||
|     await page.mouse.down(); | ||||
|     await page.mouse.move(imageCenterX, imageCenterY + 200, 10); | ||||
|     await page.mouse.up(); | ||||
|     await Promise.all(panHotkey.map(x => page.keyboard.up(x))); | ||||
|     const afterUpPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|     expect(afterUpPanBoundingBox.y).toBeGreaterThanOrEqual(afterLeftPanBoundingBox.y); | ||||
|  | ||||
|     // Pan down | ||||
|     await Promise.all(panHotkey.map(x => page.keyboard.down(x))); | ||||
|     await page.mouse.down(); | ||||
|     await page.mouse.move(imageCenterX, imageCenterY - 200, 10); | ||||
|     await page.mouse.up(); | ||||
|     await Promise.all(panHotkey.map(x => page.keyboard.up(x))); | ||||
|     const afterDownPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|     expect(afterDownPanBoundingBox.y).toBeLessThanOrEqual(afterUpPanBoundingBox.y); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
| */ | ||||
| async function mouseZoomIn(page) { | ||||
|     // Zoom in | ||||
|     const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|     await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|     const deltaYStep = 100; // equivalent to 1x zoom | ||||
|     await page.mouse.wheel(0, deltaYStep * 2); | ||||
|     const zoomedBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|     const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2; | ||||
|     const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2; | ||||
|  | ||||
|     // center the mouse pointer | ||||
|     await page.mouse.move(imageCenterX, imageCenterY); | ||||
|  | ||||
|     // Wait for zoom animation to finish | ||||
|     await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|     const imageMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|     expect(imageMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height); | ||||
|     expect(imageMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Gets the filter:contrast value of the current background-image and | ||||
|  * asserts against an expected value | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  * @param {String} expected The expected contrast value | ||||
|  */ | ||||
| async function assertBackgroundImageContrast(page, expected) { | ||||
|     const backgroundImage = page.locator('.c-imagery__main-image__background-image'); | ||||
|  | ||||
|     // Get the contrast filter value (i.e: filter: contrast(500%) => "500") | ||||
|     const actual = await backgroundImage.evaluate((el) => { | ||||
|         return el.style.filter.match(/contrast\((\d{1,3})%\)/)[1]; | ||||
|     }); | ||||
|     expect(actual).toBe(expected); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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,256 +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(page.locator('.l-browse-bar__object-name')).toContainText(`Unnamed ${CUSTOM_NAME}`); | ||||
|     }); | ||||
|  | ||||
|     test('Can be deleted if there are no locked pages @addInit', async ({ page }) => { | ||||
|         await openContextMenuRestrictedNotebook(page); | ||||
|  | ||||
|         const menuOptions = page.locator('.c-menu ul'); | ||||
|         await expect.soft(menuOptions).toContainText('Remove'); | ||||
|  | ||||
|         const restrictedNotebookTreeObject = page.locator(`a:has-text("Unnamed ${CUSTOM_NAME}")`); | ||||
|  | ||||
|         // notbook tree object exists | ||||
|         expect.soft(await restrictedNotebookTreeObject.count()).toEqual(1); | ||||
|  | ||||
|         // Click Remove Text | ||||
|         await page.locator('text=Remove').click(); | ||||
|  | ||||
|         // Click 'OK' on confirmation window and wait for save banner to appear | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('text=OK').click(), | ||||
|             page.waitForSelector('.c-message-banner__message') | ||||
|         ]); | ||||
|         await page.locator('.c-message-banner__close-button').click(); | ||||
|  | ||||
|         // has been deleted | ||||
|         expect(await restrictedNotebookTreeObject.count()).toEqual(0); | ||||
|     }); | ||||
|  | ||||
|     test('Can be locked if at least one page has one entry @addInit', async ({ page }) => { | ||||
|  | ||||
|         await enterTextEntry(page); | ||||
|  | ||||
|         const commitButton = page.locator('button:has-text("Commit Entries")'); | ||||
|         expect(await commitButton.count()).toEqual(1); | ||||
|     }); | ||||
|  | ||||
| }); | ||||
|  | ||||
| test.describe('Restricted Notebook with at least one entry and with the page locked @addInit', () => { | ||||
|  | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         await startAndAddRestrictedNotebookObject(page); | ||||
|         await enterTextEntry(page); | ||||
|         await lockPage(page); | ||||
|  | ||||
|         // FIXME: Give ample time for the mutation to happen | ||||
|         // https://github.com/nasa/openmct/issues/5409 | ||||
|         // eslint-disable-next-line playwright/no-wait-for-timeout | ||||
|         await page.waitForTimeout(1 * 1000); | ||||
|  | ||||
|         // open sidebar | ||||
|         await page.locator('button.c-notebook__toggle-nav-button').click(); | ||||
|     }); | ||||
|  | ||||
|     test('Locked page should now be in a locked state @addInit', async ({ page }, testInfo) => { | ||||
|         test.fixme(testInfo.project === 'chrome-beta', "Test is unreliable on chrome-beta"); | ||||
|         // main lock message on page | ||||
|         const lockMessage = page.locator('text=This page has been committed and cannot be modified or removed'); | ||||
|         expect.soft(await lockMessage.count()).toEqual(1); | ||||
|  | ||||
|         // lock icon on page in sidebar | ||||
|         const pageLockIcon = page.locator('ul.c-notebook__pages li div.icon-lock'); | ||||
|         expect.soft(await pageLockIcon.count()).toEqual(1); | ||||
|  | ||||
|         // no way to remove a restricted notebook with a locked page | ||||
|         await openContextMenuRestrictedNotebook(page); | ||||
|         const menuOptions = page.locator('.c-menu ul'); | ||||
|  | ||||
|         await expect(menuOptions).not.toContainText('Remove'); | ||||
|     }); | ||||
|  | ||||
|     test('Can still: add page, rename, add entry, delete unlocked pages @addInit', async ({ page }) => { | ||||
|         // Click text=Page Add >> button | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('text=Page Add >> button').click() | ||||
|         ]); | ||||
|         // Click text=Unnamed Page >> nth=1 | ||||
|         await page.locator('text=Unnamed Page').nth(1).click(); | ||||
|         // Press a with modifiers | ||||
|         await page.locator('text=Unnamed Page').nth(1).fill(TEST_TEXT_NAME); | ||||
|  | ||||
|         // expect to be able to rename unlocked pages | ||||
|         const newPageElement = page.locator(`text=${TEST_TEXT_NAME}`); | ||||
|         const newPageCount = await newPageElement.count(); | ||||
|         await newPageElement.press('Enter'); // exit contenteditable state | ||||
|         expect.soft(newPageCount).toEqual(1); | ||||
|  | ||||
|         // enter test text | ||||
|         await enterTextEntry(page); | ||||
|  | ||||
|         // expect new page to be lockable | ||||
|         const commitButton = page.locator('BUTTON:HAS-TEXT("COMMIT ENTRIES")'); | ||||
|         expect.soft(await commitButton.count()).toEqual(1); | ||||
|  | ||||
|         // Click text=Unnamed PageTest Page >> button | ||||
|         await page.locator('text=Unnamed PageTest Page >> button').click(); | ||||
|         // Click text=Delete Page | ||||
|         await page.locator('text=Delete Page').click(); | ||||
|         // Click text=Ok | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('text=Ok').click() | ||||
|         ]); | ||||
|  | ||||
|         // deleted page, should no longer exist | ||||
|         const deletedPageElement = page.locator(`text=${TEST_TEXT_NAME}`); | ||||
|         expect(await deletedPageElement.count()).toEqual(0); | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| test.describe('Restricted Notebook with a page locked and with an embed @addInit', () => { | ||||
|  | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         await startAndAddRestrictedNotebookObject(page); | ||||
|         await dragAndDropEmbed(page); | ||||
|     }); | ||||
|  | ||||
|     test('Allows embeds to be deleted if page unlocked @addInit', async ({ page }) => { | ||||
|         // Click .c-ne__embed__name .c-popup-menu-button | ||||
|         await page.locator('.c-ne__embed__name .c-popup-menu-button').click(); // embed popup menu | ||||
|  | ||||
|         const embedMenu = page.locator('body >> .c-menu'); | ||||
|         await expect(embedMenu).toContainText('Remove This Embed'); | ||||
|     }); | ||||
|  | ||||
|     test('Disallows embeds to be deleted if page locked @addInit', async ({ page }) => { | ||||
|         await lockPage(page); | ||||
|         // Click .c-ne__embed__name .c-popup-menu-button | ||||
|         await page.locator('.c-ne__embed__name .c-popup-menu-button').click(); // embed popup menu | ||||
|  | ||||
|         const embedMenu = page.locator('body >> .c-menu'); | ||||
|         await expect(embedMenu).not.toContainText('Remove This Embed'); | ||||
|     }); | ||||
|  | ||||
| }); | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function startAndAddRestrictedNotebookObject(page) { | ||||
|     // eslint-disable-next-line no-undef | ||||
|     await page.addInitScript({ path: path.join(__dirname, 'addInitRestrictedNotebook.js') }); | ||||
|     //Go to baseURL | ||||
|     await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|     //Click the Create button | ||||
|     await page.click('button:has-text("Create")'); | ||||
|     // Click text=CUSTOME_NAME | ||||
|     await page.click(`text=${CUSTOM_NAME}`); // secondarily tests renamability also | ||||
|     // Click text=OK | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation({waitUntil: 'networkidle'}), | ||||
|         page.click('text=OK') | ||||
|     ]); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function enterTextEntry(page) { | ||||
|     // Click .c-notebook__drag-area | ||||
|     await page.locator(NOTEBOOK_DROP_AREA).click(); | ||||
|  | ||||
|     // enter text | ||||
|     await page.locator('div.c-ne__text').click(); | ||||
|     await page.locator('div.c-ne__text').fill(TEST_TEXT); | ||||
|     await page.locator('div.c-ne__text').press('Enter'); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function dragAndDropEmbed(page) { | ||||
|     // Click button:has-text("Create") | ||||
|     await page.locator('button:has-text("Create")').click(); | ||||
|     // Click li:has-text("Sine Wave Generator") | ||||
|     await page.locator('li:has-text("Sine Wave Generator")').click(); | ||||
|     // Click form[name="mctForm"] >> text=My Items | ||||
|     await page.locator('form[name="mctForm"] >> text=My Items').click(); | ||||
|     // Click text=OK | ||||
|     await page.locator('text=OK').click(); | ||||
|     // Click text=Open MCT My Items >> span >> nth=3 | ||||
|     await page.locator('text=Open MCT My Items >> span').nth(3).click(); | ||||
|     // Click text=Unnamed CUSTOM_NAME | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('text=Unnamed CUSTOM_NAME').click() | ||||
|     ]); | ||||
|  | ||||
|     await page.dragAndDrop('text=UNNAMED SINE WAVE GENERATOR', NOTEBOOK_DROP_AREA); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function lockPage(page) { | ||||
|     const commitButton = page.locator('button:has-text("Commit Entries")'); | ||||
|     await commitButton.click(); | ||||
|  | ||||
|     //Wait until Lock Banner is visible | ||||
|     await page.locator('text=Lock Page').click(); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function openContextMenuRestrictedNotebook(page) { | ||||
|     const myItemsFolder = page.locator('text=Open MCT My Items >> span').nth(3); | ||||
|     const className = await myItemsFolder.getAttribute('class'); | ||||
|     if (!className.includes('c-disclosure-triangle--expanded')) { | ||||
|         await myItemsFolder.click(); | ||||
|     } | ||||
|  | ||||
|     // Click a:has-text("Unnamed CUSTOM_NAME") | ||||
|     await page.locator(`a:has-text("Unnamed ${CUSTOM_NAME}")`).click({ | ||||
|         button: 'right' | ||||
|     }); | ||||
| } | ||||
| @@ -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"); | ||||
|         } | ||||
|  | ||||
|     }); | ||||
| }); | ||||
| @@ -24,9 +24,22 @@ | ||||
| Testsuite for plot autoscale. | ||||
| */ | ||||
|  | ||||
| const { test } = require('../../../fixtures.js'); | ||||
| const { test: _test } = require('../../../fixtures.js'); | ||||
| const { expect } = require('@playwright/test'); | ||||
|  | ||||
| // create a new `test` API that will not append platform details to snapshot | ||||
| // file names, only for the tests in this file, so that the same snapshots will | ||||
| // be used for all platforms. | ||||
| const test = _test.extend({ | ||||
|     _autoSnapshotSuffix: [ | ||||
|         async ({}, use, testInfo) => { | ||||
|             testInfo.snapshotSuffix = ''; | ||||
|             await use(); | ||||
|         }, | ||||
|         { auto: true } | ||||
|     ] | ||||
| }); | ||||
|  | ||||
| test.use({ | ||||
|     viewport: { | ||||
|         width: 1280, | ||||
| @@ -37,7 +50,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(); | ||||
|         await test.setTimeout(120 * 1000); | ||||
|  | ||||
|         await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
| @@ -49,16 +62,16 @@ 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'); | ||||
|  | ||||
|         //Alt Drag Start | ||||
|         await page.keyboard.down('Alt'); | ||||
|  | ||||
|         await canvas.dragTo(canvas, { | ||||
| @@ -72,15 +85,15 @@ test.describe('ExportAsJSON', () => { | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         //Alt Drag End | ||||
|         await page.keyboard.up('Alt'); | ||||
|  | ||||
|         // Ensure the drag worked. | ||||
|         await testYTicks(page, ['0.00', '0.50', '1.00', '1.50', '2.00']); | ||||
|  | ||||
|         await canvas.hover({trial: true}); | ||||
|  | ||||
|         expect(await canvas.screenshot()).toMatchSnapshot('autoscale-canvas-panned'); | ||||
|         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 })) | ||||
|         ]); | ||||
|     }); | ||||
| }); | ||||
|  | ||||
|   | ||||
										
											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: 15 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 21 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 21 KiB | 
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 18 KiB | 
| @@ -30,8 +30,8 @@ const { expect } = require('@playwright/test'); | ||||
|  | ||||
| test.describe('Log plot tests', () => { | ||||
|     test('Log Plot ticks are functionally correct in regular and log mode and after refresh', async ({ page }) => { | ||||
|         //Test.slow decorator is currently broken. Needs to be fixed in https://github.com/nasa/openmct/issues/5374 | ||||
|         test.slow(); | ||||
|         //This is necessary due to the size of the test suite. | ||||
|         await test.setTimeout(120 * 1000); | ||||
|  | ||||
|         await makeOverlayPlot(page); | ||||
|         await testRegularTicks(page); | ||||
| @@ -44,11 +44,20 @@ test.describe('Log plot tests', () => { | ||||
|         await testLogTicks(page); | ||||
|         await saveOverlayPlot(page); | ||||
|         await testLogTicks(page); | ||||
|         //await testLogPlotPixels(page); | ||||
|  | ||||
|         // refresh page and wait for charts and ticks to load | ||||
|         await page.waitForTimeout(1 * 1000); | ||||
|         await page.reload({ waitUntil: 'networkidle'}); | ||||
|         await page.waitForSelector('.gl-plot-chart-area'); | ||||
|         await page.waitForSelector('.gl-plot-y-tick-label'); | ||||
|  | ||||
|         // test log ticks hold up after refresh | ||||
|         await testLogTicks(page); | ||||
|         //await testLogPlotPixels(page); | ||||
|     }); | ||||
|  | ||||
|     // Leaving test as 'TODO' for now. | ||||
|     // NOTE: Not eligible for community contributions. | ||||
|     test.fixme('Verify that log mode option is reflected in import/export JSON', async ({ page }) => { | ||||
|     test.skip('Verify that log mode option is reflected in import/export JSON', async ({ page }) => { | ||||
|         await makeOverlayPlot(page); | ||||
|         await enableEditMode(page); | ||||
|         await enableLogMode(page); | ||||
| @@ -107,14 +116,14 @@ 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 | ||||
|  | ||||
|   | ||||
| @@ -1,155 +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, browserName }) => { | ||||
|         test.fixme(browserName === 'firefox', 'Firefox failing due to console events being missed'); | ||||
|         const errorLogs = []; | ||||
|  | ||||
|         page.on("console", (message) => { | ||||
|             if (message.type() === 'warning' && message.text().includes('Missing domain object')) { | ||||
|                 errorLogs.push(message.text()); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         //Make stacked plot | ||||
|         await makeStackedPlot(page); | ||||
|  | ||||
|         //Gets local storage and deletes the last sine wave generator in the stacked plot | ||||
|         const localStorage = await page.evaluate(() => window.localStorage); | ||||
|         const parsedData = JSON.parse(localStorage.mct); | ||||
|         const keys = Object.keys(parsedData); | ||||
|         const lastKey = keys[keys.length - 1]; | ||||
|  | ||||
|         delete parsedData[lastKey]; | ||||
|  | ||||
|         //Sets local storage with missing object | ||||
|         await page.evaluate( | ||||
|             `window.localStorage.setItem('mct', '${JSON.stringify(parsedData)}')` | ||||
|         ); | ||||
|  | ||||
|         //Reloads page and clicks on stacked plot | ||||
|         await Promise.all([ | ||||
|             page.reload(), | ||||
|             page.waitForLoadState('networkidle') | ||||
|         ]); | ||||
|  | ||||
|         //Verify Main section is there on load | ||||
|         await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Stacked Plot'); | ||||
|  | ||||
|         await page.locator('text=Open MCT My Items >> span').nth(3).click(); | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('text=Unnamed Stacked Plot').first().click() | ||||
|         ]); | ||||
|  | ||||
|         //Check that there is only one stacked item plot with a plot, the missing one will be empty | ||||
|         await expect(page.locator(".c-plot--stacked-container:has(.gl-plot)")).toHaveCount(1); | ||||
|         //Verify that console.warn is thrown | ||||
|         expect(errorLogs).toHaveLength(1); | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| /** | ||||
|  * This is used the create a stacked plot object | ||||
|  * @private | ||||
|  */ | ||||
| async function makeStackedPlot(page) { | ||||
|     // fresh page with time range from 2022-03-29 22:00:00.000Z to 2022-03-29 22:00:30.000Z | ||||
|     await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|     // create stacked plot | ||||
|     await page.locator('button.c-create-button').click(); | ||||
|     await page.locator('li:has-text("Stacked Plot")').click(); | ||||
|  | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation({ waitUntil: 'networkidle'}), | ||||
|         page.locator('text=OK').click(), | ||||
|         //Wait for Save Banner to appear | ||||
|         page.waitForSelector('.c-message-banner__message') | ||||
|     ]); | ||||
|  | ||||
|     // save the stacked plot | ||||
|     await saveStackedPlot(page); | ||||
|  | ||||
|     // create a sinewave generator | ||||
|     await createSineWaveGenerator(page); | ||||
|  | ||||
|     // click on stacked plot | ||||
|     await page.locator('text=Open MCT My Items >> span').nth(3).click(); | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('text=Unnamed Stacked Plot').first().click() | ||||
|     ]); | ||||
|  | ||||
|     // create a second sinewave generator | ||||
|     await createSineWaveGenerator(page); | ||||
|  | ||||
|     // click on stacked plot | ||||
|     await page.locator('text=Open MCT My Items >> span').nth(3).click(); | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('text=Unnamed Stacked Plot').first().click() | ||||
|     ]); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * This is used to save a stacked plot object | ||||
|  * @private | ||||
|  */ | ||||
| async function saveStackedPlot(page) { | ||||
|     // save stacked plot | ||||
|     await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click(); | ||||
|  | ||||
|     await Promise.all([ | ||||
|         page.locator('text=Save and Finish Editing').click(), | ||||
|         //Wait for Save Banner to appear | ||||
|         page.waitForSelector('.c-message-banner__message') | ||||
|     ]); | ||||
|     //Wait until Save Banner is gone | ||||
|     await page.locator('.c-message-banner__close-button').click(); | ||||
|     await page.waitForSelector('.c-message-banner__message', { state: 'detached' }); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * This is used to create a sine wave generator object | ||||
|  * @private | ||||
|  */ | ||||
| async function createSineWaveGenerator(page) { | ||||
|     //Create sine wave generator | ||||
|     await page.locator('button.c-create-button').click(); | ||||
|     await page.locator('li:has-text("Sine Wave Generator")').click(); | ||||
|  | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation({ waitUntil: 'networkidle'}), | ||||
|         page.locator('text=OK').click(), | ||||
|         //Wait for Save Banner to appear | ||||
|         page.waitForSelector('.c-message-banner__message') | ||||
|     ]); | ||||
| } | ||||
| @@ -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); | ||||
|     }); | ||||
| }); | ||||
| @@ -23,9 +23,9 @@ | ||||
| const { test } = require('../../../fixtures.js'); | ||||
| const { 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(); | ||||
|  | ||||
| @@ -73,163 +73,37 @@ test.describe('Time conductor operations', () => { | ||||
| // 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 | ||||
|         //Go to baseURL | ||||
|         await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|         // Switch to real-time mode | ||||
|         await setRealTimeMode(page); | ||||
|         // Click fixed timespan button | ||||
|         await page.locator('.c-button__label >> text=Fixed Timespan').click(); | ||||
|  | ||||
|         // Set start time offset | ||||
|         await setStartOffset(page, startOffset); | ||||
|         // Click local clock | ||||
|         await page.locator('.icon-clock >> text=Local Clock').click(); | ||||
|  | ||||
|         // Click time offset button | ||||
|         await page.locator('.c-conductor__delta-button >> text=00:30:00').click(); | ||||
|  | ||||
|         // Input start time offset | ||||
|         await page.fill('.pr-time-controls__secs', '23'); | ||||
|  | ||||
|         // Click the check button | ||||
|         await page.locator('.icon-check').click(); | ||||
|  | ||||
|         // Verify time was updated on time offset button | ||||
|         await expect(page.locator('data-testid=conductor-start-offset-button')).toContainText('00:30:23'); | ||||
|         await expect(page.locator('.c-conductor__delta-button').first()).toContainText('00:30:23'); | ||||
|  | ||||
|         // Set end time offset | ||||
|         await setEndOffset(page, endOffset); | ||||
|         // Click time offset set preceding now button | ||||
|         await page.locator('.c-conductor__delta-button >> text=00:00:30').click(); | ||||
|  | ||||
|         // Input preceding time offset | ||||
|         await page.fill('.pr-time-controls__secs', '31'); | ||||
|  | ||||
|         // Click the check buttons | ||||
|         await page.locator('.icon-check').click(); | ||||
|  | ||||
|         // 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}`); | ||||
|         await expect(page.locator('.c-conductor__delta-button').nth(1)).toContainText('00:00:31'); | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| /** | ||||
|  * @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,185 +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) { | ||||
|     await page.locator('.c-timer').hover({trial: true}); | ||||
|     const buttonTitle = buttonTitleFromAction(action); | ||||
|     await page.click(`button[title="${buttonTitle}"]`); | ||||
|     assertTimerStateAfterAction(page, action); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Takes in a TimerViewAction and returns the button title | ||||
|  * @param {TimerViewAction} action | ||||
|  */ | ||||
| function buttonTitleFromAction(action) { | ||||
|     switch (action) { | ||||
|     case 'Start': | ||||
|         return 'Start'; | ||||
|     case 'Pause': | ||||
|         return 'Pause'; | ||||
|     case 'Restart at 0': | ||||
|         return 'Reset'; | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Verify the timer state after a timer action has been performed. | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  * @param {TimerAction} action | ||||
|  */ | ||||
| async function assertTimerStateAfterAction(page, action) { | ||||
|     let timerStateClass; | ||||
|     switch (action) { | ||||
|     case 'Start': | ||||
|     case 'Restart at 0': | ||||
|         timerStateClass = "is-started"; | ||||
|         break; | ||||
|     case 'Stop': | ||||
|         timerStateClass = 'is-stopped'; | ||||
|         break; | ||||
|     case 'Pause': | ||||
|         timerStateClass = 'is-paused'; | ||||
|         break; | ||||
|     } | ||||
|  | ||||
|     await expect.soft(page.locator('.c-timer')).toHaveClass(new RegExp(timerStateClass)); | ||||
| } | ||||
							
								
								
									
										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": "[]" | ||||
|         } | ||||
|       ] | ||||
|     } | ||||
|   ] | ||||
| } | ||||
| @@ -45,15 +45,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,86 +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') | ||||
|     ]); | ||||
|  | ||||
|     // save (exit edit mode) | ||||
|     await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click(); | ||||
|     await page.locator('text=Save and Finish Editing').click(); | ||||
|  | ||||
|     // click create button | ||||
|     await page.locator('button:has-text("Create")').click(); | ||||
|  | ||||
|     // add sine wave generator with defaults | ||||
|     await page.locator('li:has-text("Sine Wave Generator")').click(); | ||||
|  | ||||
|     //Add a 5000 ms Delay | ||||
|     await page.locator('[aria-label="Loading Delay \\(ms\\)"]').fill('5000'); | ||||
|  | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('text=OK').click(), | ||||
|         //Wait for Save Banner to appear1 | ||||
|         page.waitForSelector('.c-message-banner__message') | ||||
|     ]); | ||||
|  | ||||
|     // focus the overlay plot | ||||
|     await page.locator('text=Open MCT My Items >> span').nth(3).click(); | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('text=Unnamed Overlay Plot').first().click() | ||||
|     ]); | ||||
|  | ||||
|     await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Overlay Plot'); | ||||
|     //Save localStorage for future test execution | ||||
|     await context.storageState({ path: './e2e/test-data/VisualTestData_storage.json' }); | ||||
| }); | ||||
| @@ -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,7 +21,7 @@ | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import EventEmitter from 'EventEmitter'; | ||||
| import { v4 as uuid } from 'uuid'; | ||||
| import uuid from 'uuid'; | ||||
| import createExampleUser from './exampleUserCreator'; | ||||
|  | ||||
| const STATUSES = [{ | ||||
| @@ -31,7 +31,7 @@ const STATUSES = [{ | ||||
|     iconClassPoll: "icon-status-poll-question-mark" | ||||
| }, { | ||||
|     key: "GO", | ||||
|     label: "Go", | ||||
|     label: "GO", | ||||
|     iconClass: "icon-check", | ||||
|     iconClassPoll: "icon-status-poll-question-mark", | ||||
|     statusClass: "s-status-ok", | ||||
| @@ -39,7 +39,7 @@ const STATUSES = [{ | ||||
|     statusFgColor: "#000" | ||||
| }, { | ||||
|     key: "MAYBE", | ||||
|     label: "Maybe", | ||||
|     label: "MAYBE", | ||||
|     iconClass: "icon-alert-triangle", | ||||
|     iconClassPoll: "icon-status-poll-question-mark", | ||||
|     statusClass: "s-status-warning", | ||||
| @@ -47,7 +47,7 @@ const STATUSES = [{ | ||||
|     statusFgColor: "#000" | ||||
| }, { | ||||
|     key: "NO_GO", | ||||
|     label: "No go", | ||||
|     label: "NO GO", | ||||
|     iconClass: "icon-circle-slash", | ||||
|     iconClassPoll: "icon-status-poll-question-mark", | ||||
|     statusClass: "s-status-error", | ||||
|   | ||||
| @@ -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,42 +1,43 @@ | ||||
| { | ||||
|   "name": "openmct", | ||||
|   "version": "2.1.0-SNAPSHOT", | ||||
|   "version": "2.0.4-SNAPSHOT", | ||||
|   "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.3", | ||||
|     "@playwright/test": "1.21.1", | ||||
|     "@types/eventemitter3": "^1.0.0", | ||||
|     "@types/jasmine": "^4.0.1", | ||||
|     "@types/karma": "^6.3.2", | ||||
|     "@types/lodash": "^4.14.178", | ||||
|     "@types/mocha": "^9.1.0", | ||||
|     "babel-loader": "8.2.5", | ||||
|     "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", | ||||
|     "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", | ||||
|     "eslint": "8.18.0", | ||||
|     "eslint": "8.13.0", | ||||
|     "eslint-plugin-compat": "4.0.2", | ||||
|     "eslint-plugin-playwright": "0.9.0", | ||||
|     "eslint-plugin-vue": "9.1.1", | ||||
|     "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.2.0", | ||||
|     "jasmine-core": "4.1.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", | ||||
| @@ -47,67 +48,61 @@ | ||||
|     "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.1", | ||||
|     "moment": "2.29.4", | ||||
|     "mini-css-extract-plugin": "2.6.0", | ||||
|     "moment": "2.29.3", | ||||
|     "moment-duration-format": "2.3.2", | ||||
|     "moment-timezone": "0.5.34", | ||||
|     "node-bourbon": "4.2.3", | ||||
|     "nyc":"15.1.0", | ||||
|     "painterro": "1.2.78", | ||||
|     "painterro": "1.2.56", | ||||
|     "plotly.js-basic-dist": "2.12.0", | ||||
|     "plotly.js-gl2d-dist": "2.12.0", | ||||
|     "printj": "1.3.1", | ||||
|     "request": "2.88.2", | ||||
|     "resolve-url-loader": "5.0.0", | ||||
|     "sass": "1.52.2", | ||||
|     "sass-loader": "13.0.2", | ||||
|     "sinon": "14.0.0", | ||||
|     "sass": "1.49.9", | ||||
|     "sass-loader": "12.6.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.10.0", | ||||
|     "webpack-dev-middleware": "5.3.3", | ||||
|     "webpack-cli": "4.9.2", | ||||
|     "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": "eslint example src e2e --ext .js,.vue openmct.js", | ||||
|     "lint:fix": "eslint example src e2e --ext .js,.vue openmct.js --fix", | ||||
|     "build:prod": "cross-env webpack --config webpack.prod.js", | ||||
|     "build:dev": "webpack --config webpack.dev.js", | ||||
|     "build:coverage": "webpack --config webpack.coverage.js", | ||||
|     "build:watch": "webpack --config webpack.dev.js --watch", | ||||
|     "info": "npx envinfo --system --browsers --npmPackages --binaries --languages --markdown", | ||||
|     "test": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run", | ||||
|     "test:firefox": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run --browsers=FirefoxHeadless", | ||||
|     "test": "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 branding clock exampleImagery", | ||||
|     "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:updatesnapshots": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome --grep @snapshot --update-snapshots", | ||||
|     "test:e2e:visual": "percy exec --config ./e2e/.percy.yml -- npx playwright test --config=e2e/playwright-visual.config.js 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_ENV=test NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --no-single-run", | ||||
|     "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": { | ||||
|   | ||||
							
								
								
									
										270
									
								
								src/MCT.js
									
									
									
									
									
								
							
							
						
						
									
										270
									
								
								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,156 @@ 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.ScatterPlot()); | ||||
|         this.install(this.plugins.BarChart()); | ||||
|         this.install(this.plugins.TelemetryTable.default()); | ||||
|         this.install(PreviewPlugin.default()); | ||||
|         this.install(LicensesPlugin.default()); | ||||
| @@ -287,7 +271,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 +379,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,14 +49,12 @@ define([ | ||||
|     TelemetryAPI, | ||||
|     TimeAPI, | ||||
|     TypeRegistry, | ||||
|     UserAPI, | ||||
|     AnnotationAPI | ||||
|     UserAPI | ||||
| ) { | ||||
|     return { | ||||
|         ActionsAPI: ActionsAPI.default, | ||||
|         CompositionAPI: CompositionAPI, | ||||
|         EditorAPI: EditorAPI, | ||||
|         FaultManagementAPI: FaultManagementAPI, | ||||
|         FormsAPI: FormsAPI, | ||||
|         IndicatorAPI: IndicatorAPI.default, | ||||
|         MenuAPI: MenuAPI.default, | ||||
| @@ -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(); | ||||
|     }); | ||||
|  | ||||
| }); | ||||
| @@ -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,7 +64,6 @@ | ||||
|             tabindex="0" | ||||
|             :disabled="isInvalid" | ||||
|             class="c-button c-button--major" | ||||
|             aria-label="Save" | ||||
|             @click="onSave" | ||||
|         > | ||||
|             {{ submitLabel }} | ||||
| @@ -68,7 +71,6 @@ | ||||
|         <button | ||||
|             tabindex="0" | ||||
|             class="c-button js-cancel-button" | ||||
|             aria-label="Cancel" | ||||
|             @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 }; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -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: { | ||||
|   | ||||
| @@ -22,7 +22,6 @@ | ||||
|  | ||||
| import EventEmitter from 'EventEmitter'; | ||||
| import indicatorTemplate from './res/indicator-template.html'; | ||||
| import { convertTemplateToHTML } from '@/utils/template/templateHelpers'; | ||||
|  | ||||
| const DEFAULT_ICON_CLASS = 'icon-info'; | ||||
|  | ||||
| @@ -31,7 +30,7 @@ class SimpleIndicator extends EventEmitter { | ||||
|         super(); | ||||
|  | ||||
|         this.openmct = openmct; | ||||
|         this.element = convertTemplateToHTML(indicatorTemplate)[0]; | ||||
|         this.element = compileTemplate(indicatorTemplate)[0]; | ||||
|         this.priority = openmct.priority.DEFAULT; | ||||
|  | ||||
|         this.textElement = this.element.querySelector('.js-indicator-text'); | ||||
| @@ -117,4 +116,11 @@ class SimpleIndicator extends EventEmitter { | ||||
|     } | ||||
| } | ||||
|  | ||||
| function compileTemplate(htmlTemplate) { | ||||
|     const templateNode = document.createElement('template'); | ||||
|     templateNode.innerHTML = htmlTemplate; | ||||
|  | ||||
|     return templateNode.content.cloneNode(true).children; | ||||
| } | ||||
|  | ||||
| export default SimpleIndicator; | ||||
|   | ||||
| @@ -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> | ||||
|   | ||||
| @@ -20,18 +20,122 @@ | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import TelemetryCollection from './TelemetryCollection'; | ||||
| import TelemetryRequestInterceptorRegistry from './TelemetryRequestInterceptor'; | ||||
| import CustomStringFormatter from '../../plugins/displayLayout/CustomStringFormatter'; | ||||
| import TelemetryMetadataManager from './TelemetryMetadataManager'; | ||||
| import TelemetryValueFormatter from './TelemetryValueFormatter'; | ||||
| import DefaultMetadataProvider from './DefaultMetadataProvider'; | ||||
| import objectUtils from 'objectUtils'; | ||||
| import _ from 'lodash'; | ||||
| const { TelemetryCollection } = require("./TelemetryCollection"); | ||||
|  | ||||
| export default class TelemetryAPI { | ||||
| define([ | ||||
|     '../../plugins/displayLayout/CustomStringFormatter', | ||||
|     './TelemetryMetadataManager', | ||||
|     './TelemetryValueFormatter', | ||||
|     './DefaultMetadataProvider', | ||||
|     'objectUtils', | ||||
|     'lodash' | ||||
| ], function ( | ||||
|     CustomStringFormatter, | ||||
|     TelemetryMetadataManager, | ||||
|     TelemetryValueFormatter, | ||||
|     DefaultMetadataProvider, | ||||
|     objectUtils, | ||||
|     _ | ||||
| ) { | ||||
|     /** | ||||
|      * A LimitEvaluator may be used to detect when telemetry values | ||||
|      * have exceeded nominal conditions. | ||||
|      * | ||||
|      * @interface LimitEvaluator | ||||
|      * @memberof module:openmct.TelemetryAPI~ | ||||
|      */ | ||||
|  | ||||
|     constructor(openmct) { | ||||
|     /** | ||||
|      * Check for any limit violations associated with a telemetry datum. | ||||
|      * @method evaluate | ||||
|      * @param {*} datum the telemetry datum to evaluate | ||||
|      * @param {TelemetryProperty} the property to check for limit violations | ||||
|      * @memberof module:openmct.TelemetryAPI~LimitEvaluator | ||||
|      * @returns {module:openmct.TelemetryAPI~LimitViolation} metadata about | ||||
|      *          the limit violation, or undefined if a value is within limits | ||||
|      */ | ||||
|  | ||||
|     /** | ||||
|      * A violation of limits defined for a telemetry property. | ||||
|      * @typedef LimitViolation | ||||
|      * @memberof {module:openmct.TelemetryAPI~} | ||||
|      * @property {string} cssClass the class (or space-separated classes) to | ||||
|      *           apply to display elements for values which violate this limit | ||||
|      * @property {string} name the human-readable name for the limit violation | ||||
|      */ | ||||
|  | ||||
|     /** | ||||
|      * A TelemetryFormatter converts telemetry values for purposes of | ||||
|      * display as text. | ||||
|      * | ||||
|      * @interface TelemetryFormatter | ||||
|      * @memberof module:openmct.TelemetryAPI~ | ||||
|      */ | ||||
|  | ||||
|     /** | ||||
|      * Retrieve the 'key' from the datum and format it accordingly to | ||||
|      * telemetry metadata in domain object. | ||||
|      * | ||||
|      * @method format | ||||
|      * @memberof module:openmct.TelemetryAPI~TelemetryFormatter# | ||||
|      */ | ||||
|  | ||||
|     /** | ||||
|      * Describes a property which would be found in a datum of telemetry | ||||
|      * associated with a particular domain object. | ||||
|      * | ||||
|      * @typedef TelemetryProperty | ||||
|      * @memberof module:openmct.TelemetryAPI~ | ||||
|      * @property {string} key the name of the property in the datum which | ||||
|      *           contains this telemetry value | ||||
|      * @property {string} name the human-readable name for this property | ||||
|      * @property {string} [units] the units associated with this property | ||||
|      * @property {boolean} [temporal] true if this property is a timestamp, or | ||||
|      *           may be otherwise used to order telemetry in a time-like | ||||
|      *           fashion; default is false | ||||
|      * @property {boolean} [numeric] true if the values for this property | ||||
|      *           can be interpreted plainly as numbers; default is true | ||||
|      * @property {boolean} [enumerated] true if this property may have only | ||||
|      *           certain specific values; default is false | ||||
|      * @property {string} [values] for enumerated states, an ordered list | ||||
|      *           of possible values | ||||
|      */ | ||||
|  | ||||
|     /** | ||||
|      * Describes and bounds requests for telemetry data. | ||||
|      * | ||||
|      * @typedef TelemetryRequest | ||||
|      * @memberof module:openmct.TelemetryAPI~ | ||||
|      * @property {string} sort the key of the property to sort by. This may | ||||
|      *           be prefixed with a "+" or a "-" sign to sort in ascending | ||||
|      *           or descending order respectively. If no prefix is present, | ||||
|      *           ascending order will be used. | ||||
|      * @property {*} start the lower bound for values of the sorting property | ||||
|      * @property {*} end the upper bound for values of the sorting property | ||||
|      * @property {string[]} strategies symbolic identifiers for strategies | ||||
|      *           (such as `minmax`) which may be recognized by providers; | ||||
|      *           these will be tried in order until an appropriate provider | ||||
|      *           is found | ||||
|      */ | ||||
|  | ||||
|     /** | ||||
|      * Provides telemetry data. To connect to new data sources, new | ||||
|      * TelemetryProvider implementations should be | ||||
|      * [registered]{@link module:openmct.TelemetryAPI#addProvider}. | ||||
|      * | ||||
|      * @interface TelemetryProvider | ||||
|      * @memberof module:openmct.TelemetryAPI~ | ||||
|      */ | ||||
|  | ||||
|     /** | ||||
|      * An interface for retrieving telemetry data associated with a domain | ||||
|      * object. | ||||
|      * | ||||
|      * @interface TelemetryAPI | ||||
|      * @augments module:openmct.TelemetryAPI~TelemetryProvider | ||||
|      * @memberof module:openmct | ||||
|      */ | ||||
|     function TelemetryAPI(openmct) { | ||||
|         this.openmct = openmct; | ||||
|  | ||||
|         this.formatMapCache = new WeakMap(); | ||||
| @@ -44,14 +148,12 @@ export default class TelemetryAPI { | ||||
|         this.requestProviders = []; | ||||
|         this.subscriptionProviders = []; | ||||
|         this.valueFormatterCache = new WeakMap(); | ||||
|  | ||||
|         this.requestInterceptorRegistry = new TelemetryRequestInterceptorRegistry(); | ||||
|     } | ||||
|  | ||||
|     abortAllRequests() { | ||||
|     TelemetryAPI.prototype.abortAllRequests = function () { | ||||
|         this.requestAbortControllers.forEach((controller) => controller.abort()); | ||||
|         this.requestAbortControllers.clear(); | ||||
|     } | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Return Custom String Formatter | ||||
| @@ -60,9 +162,9 @@ export default class TelemetryAPI { | ||||
|      * @param {string} format custom formatter string (eg: %.4f, <s etc.) | ||||
|      * @returns {CustomStringFormatter} | ||||
|      */ | ||||
|     customStringFormatter(valueMetadata, format) { | ||||
|         return new CustomStringFormatter(this.openmct, valueMetadata, format); | ||||
|     } | ||||
|     TelemetryAPI.prototype.customStringFormatter = function (valueMetadata, format) { | ||||
|         return new CustomStringFormatter.default(this.openmct, valueMetadata, format); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Return true if the given domainObject is a telemetry object.  A telemetry | ||||
| @@ -72,9 +174,9 @@ export default class TelemetryAPI { | ||||
|      * @param {module:openmct.DomainObject} domainObject | ||||
|      * @returns {boolean} true if the object is a telemetry object. | ||||
|      */ | ||||
|     isTelemetryObject(domainObject) { | ||||
|     TelemetryAPI.prototype.isTelemetryObject = function (domainObject) { | ||||
|         return Boolean(this.findMetadataProvider(domainObject)); | ||||
|     } | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Check if this provider can supply telemetry data associated with | ||||
| @@ -86,10 +188,10 @@ export default class TelemetryAPI { | ||||
|      * @returns {boolean} true if telemetry can be provided | ||||
|      * @memberof module:openmct.TelemetryAPI~TelemetryProvider# | ||||
|      */ | ||||
|     canProvideTelemetry(domainObject) { | ||||
|     TelemetryAPI.prototype.canProvideTelemetry = function (domainObject) { | ||||
|         return Boolean(this.findSubscriptionProvider(domainObject)) | ||||
|                 || Boolean(this.findRequestProvider(domainObject)); | ||||
|     } | ||||
|                || Boolean(this.findRequestProvider(domainObject)); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Register a telemetry provider with the telemetry service. This | ||||
| @@ -99,7 +201,7 @@ export default class TelemetryAPI { | ||||
|      * @param {module:openmct.TelemetryAPI~TelemetryProvider} provider the new | ||||
|      *        telemetry provider | ||||
|      */ | ||||
|     addProvider(provider) { | ||||
|     TelemetryAPI.prototype.addProvider = function (provider) { | ||||
|         if (provider.supportsRequest) { | ||||
|             this.requestProviders.unshift(provider); | ||||
|         } | ||||
| @@ -115,54 +217,54 @@ export default class TelemetryAPI { | ||||
|         if (provider.supportsLimits) { | ||||
|             this.limitProviders.unshift(provider); | ||||
|         } | ||||
|     } | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * @private | ||||
|      */ | ||||
|     findSubscriptionProvider() { | ||||
|     TelemetryAPI.prototype.findSubscriptionProvider = function () { | ||||
|         const args = Array.prototype.slice.apply(arguments); | ||||
|         function supportsDomainObject(provider) { | ||||
|             return provider.supportsSubscribe.apply(provider, args); | ||||
|         } | ||||
|  | ||||
|         return this.subscriptionProviders.filter(supportsDomainObject)[0]; | ||||
|     } | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * @private | ||||
|      */ | ||||
|     findRequestProvider(domainObject) { | ||||
|     TelemetryAPI.prototype.findRequestProvider = function (domainObject) { | ||||
|         const args = Array.prototype.slice.apply(arguments); | ||||
|         function supportsDomainObject(provider) { | ||||
|             return provider.supportsRequest.apply(provider, args); | ||||
|         } | ||||
|  | ||||
|         return this.requestProviders.filter(supportsDomainObject)[0]; | ||||
|     } | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * @private | ||||
|      */ | ||||
|     findMetadataProvider(domainObject) { | ||||
|     TelemetryAPI.prototype.findMetadataProvider = function (domainObject) { | ||||
|         return this.metadataProviders.filter(function (p) { | ||||
|             return p.supportsMetadata(domainObject); | ||||
|         })[0]; | ||||
|     } | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * @private | ||||
|      */ | ||||
|     findLimitEvaluator(domainObject) { | ||||
|     TelemetryAPI.prototype.findLimitEvaluator = function (domainObject) { | ||||
|         return this.limitProviders.filter(function (p) { | ||||
|             return p.supportsLimits(domainObject); | ||||
|         })[0]; | ||||
|     } | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * @private | ||||
|      */ | ||||
|     standardizeRequestOptions(options) { | ||||
|     TelemetryAPI.prototype.standardizeRequestOptions = function (options) { | ||||
|         if (!Object.prototype.hasOwnProperty.call(options, 'start')) { | ||||
|             options.start = this.openmct.time.bounds().start; | ||||
|         } | ||||
| @@ -174,47 +276,7 @@ export default class TelemetryAPI { | ||||
|         if (!Object.prototype.hasOwnProperty.call(options, 'domain')) { | ||||
|             options.domain = this.openmct.time.timeSystem().key; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Register a request interceptor that transforms a request via module:openmct.TelemetryAPI.request | ||||
|      * The request will be modifyed when it is received and will be returned in it's modified state | ||||
|      * The request will be transformed only if the interceptor is applicable to that domain object as defined by the RequestInterceptorDef | ||||
|      * | ||||
|      * @param {module:openmct.RequestInterceptorDef} requestInterceptorDef the request interceptor definition to add | ||||
|      * @method addRequestInterceptor | ||||
|      * @memberof module:openmct.TelemetryRequestInterceptorRegistry# | ||||
|      */ | ||||
|     addRequestInterceptor(requestInterceptorDef) { | ||||
|         this.requestInterceptorRegistry.addInterceptor(requestInterceptorDef); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Retrieve the request interceptors for a given domain object. | ||||
|      * @private | ||||
|      */ | ||||
|     #getInterceptorsForRequest(identifier, request) { | ||||
|         return this.requestInterceptorRegistry.getInterceptors(identifier, request); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Invoke interceptors if applicable for a given domain object. | ||||
|      */ | ||||
|     async applyRequestInterceptors(domainObject, request) { | ||||
|         const interceptors = this.#getInterceptorsForRequest(domainObject.identifier, request); | ||||
|  | ||||
|         if (interceptors.length === 0) { | ||||
|             return request; | ||||
|         } | ||||
|  | ||||
|         let modifiedRequest = { ...request }; | ||||
|  | ||||
|         for (let interceptor of interceptors) { | ||||
|             modifiedRequest = await interceptor.invoke(modifiedRequest); | ||||
|         } | ||||
|  | ||||
|         return modifiedRequest; | ||||
|     } | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Request telemetry collection for a domain object. | ||||
| @@ -230,13 +292,13 @@ export default class TelemetryAPI { | ||||
|      *        options for this telemetry collection request | ||||
|      * @returns {TelemetryCollection} a TelemetryCollection instance | ||||
|      */ | ||||
|     requestCollection(domainObject, options = {}) { | ||||
|     TelemetryAPI.prototype.requestCollection = function (domainObject, options = {}) { | ||||
|         return new TelemetryCollection( | ||||
|             this.openmct, | ||||
|             domainObject, | ||||
|             options | ||||
|         ); | ||||
|     } | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Request historical telemetry for a domain object. | ||||
| @@ -253,7 +315,7 @@ export default class TelemetryAPI { | ||||
|      * @returns {Promise.<object[]>} a promise for an array of | ||||
|      *          telemetry data | ||||
|      */ | ||||
|     async request(domainObject) { | ||||
|     TelemetryAPI.prototype.request = function (domainObject) { | ||||
|         if (this.noRequestProviderForAllObjects) { | ||||
|             return Promise.resolve([]); | ||||
|         } | ||||
| @@ -268,7 +330,6 @@ export default class TelemetryAPI { | ||||
|         this.requestAbortControllers.add(abortController); | ||||
|  | ||||
|         this.standardizeRequestOptions(arguments[1]); | ||||
|  | ||||
|         const provider = this.findRequestProvider.apply(this, arguments); | ||||
|         if (!provider) { | ||||
|             this.requestAbortControllers.delete(abortController); | ||||
| @@ -276,8 +337,6 @@ export default class TelemetryAPI { | ||||
|             return this.handleMissingRequestProvider(domainObject); | ||||
|         } | ||||
|  | ||||
|         arguments[1] = await this.applyRequestInterceptors(domainObject, arguments[1]); | ||||
|  | ||||
|         return provider.request.apply(provider, arguments) | ||||
|             .catch((rejected) => { | ||||
|                 if (rejected.name !== 'AbortError') { | ||||
| @@ -289,7 +348,7 @@ export default class TelemetryAPI { | ||||
|             }).finally(() => { | ||||
|                 this.requestAbortControllers.delete(abortController); | ||||
|             }); | ||||
|     } | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Subscribe to realtime telemetry for a specific domain object. | ||||
| @@ -305,7 +364,7 @@ export default class TelemetryAPI { | ||||
|      * @returns {Function} a function which may be called to terminate | ||||
|      *          the subscription | ||||
|      */ | ||||
|     subscribe(domainObject, callback, options) { | ||||
|     TelemetryAPI.prototype.subscribe = function (domainObject, callback, options) { | ||||
|         const provider = this.findSubscriptionProvider(domainObject); | ||||
|  | ||||
|         if (!this.subscribeCache) { | ||||
| @@ -342,7 +401,7 @@ export default class TelemetryAPI { | ||||
|                 delete this.subscribeCache[keyString]; | ||||
|             } | ||||
|         }.bind(this); | ||||
|     } | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Get telemetry metadata for a given domain object.  Returns a telemetry | ||||
| @@ -351,7 +410,7 @@ export default class TelemetryAPI { | ||||
|      * | ||||
|      * @returns {TelemetryMetadataManager} | ||||
|      */ | ||||
|     getMetadata(domainObject) { | ||||
|     TelemetryAPI.prototype.getMetadata = function (domainObject) { | ||||
|         if (!this.metadataCache.has(domainObject)) { | ||||
|             const metadataProvider = this.findMetadataProvider(domainObject); | ||||
|             if (!metadataProvider) { | ||||
| @@ -367,14 +426,14 @@ export default class TelemetryAPI { | ||||
|         } | ||||
|  | ||||
|         return this.metadataCache.get(domainObject); | ||||
|     } | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Return an array of valueMetadatas that are common to all supplied | ||||
|      * telemetry objects and match the requested hints. | ||||
|      * | ||||
|      */ | ||||
|     commonValuesForHints(metadatas, hints) { | ||||
|     TelemetryAPI.prototype.commonValuesForHints = function (metadatas, hints) { | ||||
|         const options = metadatas.map(function (metadata) { | ||||
|             const values = metadata.valuesForHints(hints); | ||||
|  | ||||
| @@ -394,14 +453,14 @@ export default class TelemetryAPI { | ||||
|         }); | ||||
|  | ||||
|         return _.sortBy(options, sortKeys); | ||||
|     } | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Get a value formatter for a given valueMetadata. | ||||
|      * | ||||
|      * @returns {TelemetryValueFormatter} | ||||
|      */ | ||||
|     getValueFormatter(valueMetadata) { | ||||
|     TelemetryAPI.prototype.getValueFormatter = function (valueMetadata) { | ||||
|         if (!this.valueFormatterCache.has(valueMetadata)) { | ||||
|             this.valueFormatterCache.set( | ||||
|                 valueMetadata, | ||||
| @@ -410,7 +469,7 @@ export default class TelemetryAPI { | ||||
|         } | ||||
|  | ||||
|         return this.valueFormatterCache.get(valueMetadata); | ||||
|     } | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Get a value formatter for a given key. | ||||
| @@ -418,9 +477,9 @@ export default class TelemetryAPI { | ||||
|      * | ||||
|      * @returns {Format} | ||||
|      */ | ||||
|     getFormatter(key) { | ||||
|     TelemetryAPI.prototype.getFormatter = function (key) { | ||||
|         return this.formatters.get(key); | ||||
|     } | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Get a format map of all value formatters for a given piece of telemetry | ||||
| @@ -428,7 +487,7 @@ export default class TelemetryAPI { | ||||
|      * | ||||
|      * @returns {Object<String, {TelemetryValueFormatter}>} | ||||
|      */ | ||||
|     getFormatMap(metadata) { | ||||
|     TelemetryAPI.prototype.getFormatMap = function (metadata) { | ||||
|         if (!metadata) { | ||||
|             return {}; | ||||
|         } | ||||
| @@ -443,14 +502,14 @@ export default class TelemetryAPI { | ||||
|         } | ||||
|  | ||||
|         return this.formatMapCache.get(metadata); | ||||
|     } | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Error Handling: Missing Request provider | ||||
|      * | ||||
|      * @returns Promise | ||||
|      */ | ||||
|     handleMissingRequestProvider(domainObject) { | ||||
|     TelemetryAPI.prototype.handleMissingRequestProvider = function (domainObject) { | ||||
|         this.noRequestProviderForAllObjects = this.requestProviders.every(requestProvider => { | ||||
|             const supportsRequest = requestProvider.supportsRequest.apply(requestProvider, arguments); | ||||
|             const hasRequestProvider = Object.prototype.hasOwnProperty.call(requestProvider, 'request') && typeof requestProvider.request === 'function'; | ||||
| @@ -470,18 +529,18 @@ export default class TelemetryAPI { | ||||
|         } | ||||
|  | ||||
|         this.openmct.notifications.error(message); | ||||
|         console.warn(detailMessage); | ||||
|         console.error(detailMessage); | ||||
|  | ||||
|         return Promise.resolve([]); | ||||
|     } | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Register a new telemetry data formatter. | ||||
|      * @param {Format} format the | ||||
|      */ | ||||
|     addFormat(format) { | ||||
|     TelemetryAPI.prototype.addFormat = function (format) { | ||||
|         this.formatters.set(format.key, format); | ||||
|     } | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Get a limit evaluator for this domain object. | ||||
| @@ -499,9 +558,9 @@ export default class TelemetryAPI { | ||||
|      * @method limitEvaluator | ||||
|      * @memberof module:openmct.TelemetryAPI~TelemetryProvider# | ||||
|      */ | ||||
|     limitEvaluator(domainObject) { | ||||
|     TelemetryAPI.prototype.limitEvaluator = function (domainObject) { | ||||
|         return this.getLimitEvaluator(domainObject); | ||||
|     } | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Get a limits for this domain object. | ||||
| @@ -519,9 +578,9 @@ export default class TelemetryAPI { | ||||
|      * @method limits | ||||
|      * @memberof module:openmct.TelemetryAPI~TelemetryProvider# | ||||
|      */ | ||||
|     limitDefinition(domainObject) { | ||||
|     TelemetryAPI.prototype.limitDefinition = function (domainObject) { | ||||
|         return this.getLimits(domainObject); | ||||
|     } | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Get a limit evaluator for this domain object. | ||||
| @@ -539,7 +598,7 @@ export default class TelemetryAPI { | ||||
|      * @method limitEvaluator | ||||
|      * @memberof module:openmct.TelemetryAPI~TelemetryProvider# | ||||
|      */ | ||||
|     getLimitEvaluator(domainObject) { | ||||
|     TelemetryAPI.prototype.getLimitEvaluator = function (domainObject) { | ||||
|         const provider = this.findLimitEvaluator(domainObject); | ||||
|         if (!provider) { | ||||
|             return { | ||||
| @@ -548,7 +607,7 @@ export default class TelemetryAPI { | ||||
|         } | ||||
|  | ||||
|         return provider.getLimitEvaluator(domainObject); | ||||
|     } | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Get a limit definitions for this domain object. | ||||
| @@ -577,7 +636,7 @@ export default class TelemetryAPI { | ||||
|      *  supported colors are purple, red, orange, yellow and cyan | ||||
|      * @memberof module:openmct.TelemetryAPI~TelemetryProvider# | ||||
|      */ | ||||
|     getLimits(domainObject) { | ||||
|     TelemetryAPI.prototype.getLimits = function (domainObject) { | ||||
|         const provider = this.findLimitEvaluator(domainObject); | ||||
|         if (!provider || !provider.getLimits) { | ||||
|             return { | ||||
| @@ -588,104 +647,7 @@ export default class TelemetryAPI { | ||||
|         } | ||||
|  | ||||
|         return provider.getLimits(domainObject); | ||||
|     } | ||||
| } | ||||
|     }; | ||||
|  | ||||
| /** | ||||
|  * A LimitEvaluator may be used to detect when telemetry values | ||||
|  * have exceeded nominal conditions. | ||||
|  * | ||||
|  * @interface LimitEvaluator | ||||
|  * @memberof module:openmct.TelemetryAPI~ | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * Check for any limit violations associated with a telemetry datum. | ||||
|  * @method evaluate | ||||
|  * @param {*} datum the telemetry datum to evaluate | ||||
|  * @param {TelemetryProperty} the property to check for limit violations | ||||
|  * @memberof module:openmct.TelemetryAPI~LimitEvaluator | ||||
|  * @returns {module:openmct.TelemetryAPI~LimitViolation} metadata about | ||||
|  *          the limit violation, or undefined if a value is within limits | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * A violation of limits defined for a telemetry property. | ||||
|  * @typedef LimitViolation | ||||
|  * @memberof {module:openmct.TelemetryAPI~} | ||||
|  * @property {string} cssClass the class (or space-separated classes) to | ||||
|  *           apply to display elements for values which violate this limit | ||||
|  * @property {string} name the human-readable name for the limit violation | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * A TelemetryFormatter converts telemetry values for purposes of | ||||
|  * display as text. | ||||
|  * | ||||
|  * @interface TelemetryFormatter | ||||
|  * @memberof module:openmct.TelemetryAPI~ | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * Retrieve the 'key' from the datum and format it accordingly to | ||||
|  * telemetry metadata in domain object. | ||||
|  * | ||||
|  * @method format | ||||
|  * @memberof module:openmct.TelemetryAPI~TelemetryFormatter# | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * Describes a property which would be found in a datum of telemetry | ||||
|  * associated with a particular domain object. | ||||
|  * | ||||
|  * @typedef TelemetryProperty | ||||
|  * @memberof module:openmct.TelemetryAPI~ | ||||
|  * @property {string} key the name of the property in the datum which | ||||
|  *           contains this telemetry value | ||||
|  * @property {string} name the human-readable name for this property | ||||
|  * @property {string} [units] the units associated with this property | ||||
|  * @property {boolean} [temporal] true if this property is a timestamp, or | ||||
|  *           may be otherwise used to order telemetry in a time-like | ||||
|  *           fashion; default is false | ||||
|  * @property {boolean} [numeric] true if the values for this property | ||||
|  *           can be interpreted plainly as numbers; default is true | ||||
|  * @property {boolean} [enumerated] true if this property may have only | ||||
|  *           certain specific values; default is false | ||||
|  * @property {string} [values] for enumerated states, an ordered list | ||||
|  *           of possible values | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * Describes and bounds requests for telemetry data. | ||||
|  * | ||||
|  * @typedef TelemetryRequest | ||||
|  * @memberof module:openmct.TelemetryAPI~ | ||||
|  * @property {string} sort the key of the property to sort by. This may | ||||
|  *           be prefixed with a "+" or a "-" sign to sort in ascending | ||||
|  *           or descending order respectively. If no prefix is present, | ||||
|  *           ascending order will be used. | ||||
|  * @property {*} start the lower bound for values of the sorting property | ||||
|  * @property {*} end the upper bound for values of the sorting property | ||||
|  * @property {string[]} strategies symbolic identifiers for strategies | ||||
|  *           (such as `minmax`) which may be recognized by providers; | ||||
|  *           these will be tried in order until an appropriate provider | ||||
|  *           is found | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * Provides telemetry data. To connect to new data sources, new | ||||
|  * TelemetryProvider implementations should be | ||||
|  * [registered]{@link module:openmct.TelemetryAPI#addProvider}. | ||||
|  * | ||||
|  * @interface TelemetryProvider | ||||
|  * @memberof module:openmct.TelemetryAPI~ | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * An interface for retrieving telemetry data associated with a domain | ||||
|  * object. | ||||
|  * | ||||
|  * @interface TelemetryAPI | ||||
|  * @augments module:openmct.TelemetryAPI~TelemetryProvider | ||||
|  * @memberof module:openmct | ||||
|  */ | ||||
|     return TelemetryAPI; | ||||
| }); | ||||
|   | ||||
| @@ -21,7 +21,7 @@ | ||||
|  *****************************************************************************/ | ||||
| import { createOpenMct, resetApplicationState } from 'utils/testing'; | ||||
| import TelemetryAPI from './TelemetryAPI'; | ||||
| import TelemetryCollection from './TelemetryCollection'; | ||||
| const { TelemetryCollection } = require("./TelemetryCollection"); | ||||
|  | ||||
| describe('Telemetry API', function () { | ||||
|     let openmct; | ||||
|   | ||||
| @@ -26,7 +26,7 @@ import { LOADED_ERROR, TIMESYSTEM_KEY_NOTIFICATION, TIMESYSTEM_KEY_WARNING } fro | ||||
|  | ||||
| /** Class representing a Telemetry Collection. */ | ||||
|  | ||||
| export default class TelemetryCollection extends EventEmitter { | ||||
| export class TelemetryCollection extends EventEmitter { | ||||
|     /** | ||||
|      * Creates a Telemetry Collection | ||||
|      * | ||||
| @@ -49,7 +49,6 @@ export default class TelemetryCollection extends EventEmitter { | ||||
|         this.pageState = undefined; | ||||
|         this.lastBounds = undefined; | ||||
|         this.requestAbort = undefined; | ||||
|         this.isStrategyLatest = this.options.strategy === 'latest'; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -127,8 +126,7 @@ export default class TelemetryCollection extends EventEmitter { | ||||
|             this.requestAbort = new AbortController(); | ||||
|             options.signal = this.requestAbort.signal; | ||||
|             this.emit('requestStarted'); | ||||
|             const modifiedOptions = await this.openmct.telemetry.applyRequestInterceptors(this.domainObject, options); | ||||
|             historicalData = await historicalProvider.request(this.domainObject, modifiedOptions); | ||||
|             historicalData = await historicalProvider.request(this.domainObject, options); | ||||
|         } catch (error) { | ||||
|             if (error.name !== 'AbortError') { | ||||
|                 console.error('Error requesting telemetry data...'); | ||||
| @@ -170,18 +168,17 @@ export default class TelemetryCollection extends EventEmitter { | ||||
|      * @private | ||||
|      */ | ||||
|     _processNewTelemetry(telemetryData) { | ||||
|         performance.mark('tlm:process:start'); | ||||
|         if (telemetryData === undefined) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         let latestBoundedDatum = this.boundedTelemetry[this.boundedTelemetry.length - 1]; | ||||
|         let data = Array.isArray(telemetryData) ? telemetryData : [telemetryData]; | ||||
|         let parsedValue; | ||||
|         let beforeStartOfBounds; | ||||
|         let afterEndOfBounds; | ||||
|         let added = []; | ||||
|  | ||||
|         // loop through, sort and dedupe | ||||
|         for (let datum of data) { | ||||
|             parsedValue = this.parseTime(datum); | ||||
|             beforeStartOfBounds = parsedValue < this.lastBounds.start; | ||||
| @@ -221,17 +218,7 @@ export default class TelemetryCollection extends EventEmitter { | ||||
|         } | ||||
|  | ||||
|         if (added.length) { | ||||
|             // if latest strategy is requested, we need to check if the value is the latest unmitted value | ||||
|             if (this.isStrategyLatest) { | ||||
|                 this.boundedTelemetry = [this.boundedTelemetry[this.boundedTelemetry.length - 1]]; | ||||
|  | ||||
|                 // if true, then this value has yet to be emitted | ||||
|                 if (this.boundedTelemetry[0] !== latestBoundedDatum) { | ||||
|                     this.emit('add', this.boundedTelemetry); | ||||
|                 } | ||||
|             } else { | ||||
|                 this.emit('add', added); | ||||
|             } | ||||
|             this.emit('add', added); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -291,20 +278,13 @@ export default class TelemetryCollection extends EventEmitter { | ||||
|  | ||||
|             if (startChanged) { | ||||
|                 testDatum[this.timeKey] = bounds.start; | ||||
|  | ||||
|                 // a little more complicated if not latest strategy | ||||
|                 if (!this.isStrategyLatest) { | ||||
|                     // Calculate the new index of the first item within the bounds | ||||
|                     startIndex = _.sortedIndexBy( | ||||
|                         this.boundedTelemetry, | ||||
|                         testDatum, | ||||
|                         datum => this.parseTime(datum) | ||||
|                     ); | ||||
|                     discarded = this.boundedTelemetry.splice(0, startIndex); | ||||
|                 } else if (this.parseTime(testDatum) > this.parseTime(this.boundedTelemetry[0])) { | ||||
|                     discarded = this.boundedTelemetry; | ||||
|                     this.boundedTelemetry = []; | ||||
|                 } | ||||
|                 // Calculate the new index of the first item within the bounds | ||||
|                 startIndex = _.sortedIndexBy( | ||||
|                     this.boundedTelemetry, | ||||
|                     testDatum, | ||||
|                     datum => this.parseTime(datum) | ||||
|                 ); | ||||
|                 discarded = this.boundedTelemetry.splice(0, startIndex); | ||||
|             } | ||||
|  | ||||
|             if (endChanged) { | ||||
| @@ -316,6 +296,7 @@ export default class TelemetryCollection extends EventEmitter { | ||||
|                     datum => this.parseTime(datum) | ||||
|                 ); | ||||
|                 added = this.futureBuffer.splice(0, endIndex); | ||||
|                 this.boundedTelemetry = [...this.boundedTelemetry, ...added]; | ||||
|             } | ||||
|  | ||||
|             if (discarded.length > 0) { | ||||
| @@ -323,13 +304,6 @@ export default class TelemetryCollection extends EventEmitter { | ||||
|             } | ||||
|  | ||||
|             if (added.length > 0) { | ||||
|                 if (!this.isStrategyLatest) { | ||||
|                     this.boundedTelemetry = [...this.boundedTelemetry, ...added]; | ||||
|                 } else { | ||||
|                     added = [added[added.length - 1]]; | ||||
|                     this.boundedTelemetry = added; | ||||
|                 } | ||||
|  | ||||
|                 this.emit('add', added); | ||||
|             } | ||||
|         } else { | ||||
| @@ -348,14 +322,7 @@ export default class TelemetryCollection extends EventEmitter { | ||||
|      * @private | ||||
|      */ | ||||
|     _setTimeSystem(timeSystem) { | ||||
|         let domains = []; | ||||
|         let metadataValue = { format: timeSystem.key }; | ||||
|  | ||||
|         if (this.metadata) { | ||||
|             domains = this.metadata.valuesForHints(['domain']); | ||||
|             metadataValue = this.metadata.value(timeSystem.key) || { format: timeSystem.key }; | ||||
|         } | ||||
|  | ||||
|         let domains = this.metadata.valuesForHints(['domain']); | ||||
|         let domain = domains.find((d) => d.key === timeSystem.key); | ||||
|  | ||||
|         if (domain !== undefined) { | ||||
| @@ -368,6 +335,7 @@ export default class TelemetryCollection extends EventEmitter { | ||||
|             this.openmct.notifications.alert(TIMESYSTEM_KEY_NOTIFICATION); | ||||
|         } | ||||
|  | ||||
|         let metadataValue = this.metadata.value(timeSystem.key) || { format: timeSystem.key }; | ||||
|         let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue); | ||||
|  | ||||
|         this.parseTime = (datum) => { | ||||
| @@ -388,6 +356,7 @@ export default class TelemetryCollection extends EventEmitter { | ||||
|      * @todo handle subscriptions more granually | ||||
|      */ | ||||
|     _reset() { | ||||
|         performance.mark('tlm:reset'); | ||||
|         this.boundedTelemetry = []; | ||||
|         this.futureBuffer = []; | ||||
|  | ||||
|   | ||||
| @@ -121,18 +121,6 @@ define([ | ||||
|         return _.sortBy(matchingMetadata, ...iteratees); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * check out of a given metadata has array values | ||||
|      */ | ||||
|     TelemetryMetadataManager.prototype.isArrayValue = function (metadata) { | ||||
|         const regex = /\[\]$/g; | ||||
|         if (!metadata.format && !metadata.formatString) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         return (metadata.format || metadata.formatString).match(regex) !== null; | ||||
|     }; | ||||
|  | ||||
|     TelemetryMetadataManager.prototype.getFilterableValues = function () { | ||||
|         return this.valueMetadatas.filter(metadatum => metadatum.filters && metadatum.filters.length > 0); | ||||
|     }; | ||||
|   | ||||
| @@ -1,68 +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 TelemetryRequestInterceptorRegistry { | ||||
|     /** | ||||
|      * A TelemetryRequestInterceptorRegistry maintains the definitions for different interceptors that may be invoked on telemetry | ||||
|      * requests. | ||||
|      * @interface TelemetryRequestInterceptorRegistry | ||||
|      * @memberof module:openmct | ||||
|      */ | ||||
|     constructor() { | ||||
|         this.interceptors = []; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @interface TelemetryRequestInterceptorDef | ||||
|      * @property {function} appliesTo function that determines if this interceptor should be called for the given identifier/request | ||||
|      * @property {function} invoke function that transforms the provided request and returns the transformed request | ||||
|      * @property {function} priority the priority for this interceptor. A higher number returned has more weight than a lower number | ||||
|      * @memberof module:openmct TelemetryRequestInterceptorRegistry# | ||||
|      */ | ||||
|  | ||||
|     /** | ||||
|      * Register a new telemetry request interceptor. | ||||
|      * | ||||
|      * @param {module:openmct.RequestInterceptorDef} requestInterceptorDef the interceptor to add | ||||
|      * @method addInterceptor | ||||
|      * @memberof module:openmct.TelemetryRequestInterceptorRegistry# | ||||
|      */ | ||||
|     addInterceptor(interceptorDef) { | ||||
|         //TODO: sort by priority | ||||
|         this.interceptors.push(interceptorDef); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Retrieve all interceptors applicable to a domain object/request. | ||||
|      * @method getInterceptors | ||||
|      * @returns [module:openmct.RequestInterceptorDef] the registered interceptors for this identifier/request | ||||
|      * @memberof module:openmct.TelemetryRequestInterceptorRegistry# | ||||
|      */ | ||||
|     getInterceptors(identifier, request) { | ||||
|         return this.interceptors.filter(interceptor => { | ||||
|             return typeof interceptor.appliesTo === 'function' | ||||
|                 && interceptor.appliesTo(identifier, request); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| @@ -43,23 +43,9 @@ define([ | ||||
|         }; | ||||
|  | ||||
|         this.valueMetadata = valueMetadata; | ||||
|         this.formatter = formatMap.get(valueMetadata.format) || numberFormatter; | ||||
|  | ||||
|         function getNonArrayValue(value) { | ||||
|             //metadata format could have array formats ex. string[]/number[] | ||||
|             const arrayRegex = /\[\]$/g; | ||||
|             if (value && value.match(arrayRegex)) { | ||||
|                 return value.replace(arrayRegex, ''); | ||||
|             } | ||||
|  | ||||
|             return value; | ||||
|         } | ||||
|  | ||||
|         let valueMetadataFormat = getNonArrayValue(valueMetadata.format); | ||||
|  | ||||
|         //Is there an existing formatter for the format specified? If not, default to number format | ||||
|         this.formatter = formatMap.get(valueMetadataFormat) || numberFormatter; | ||||
|  | ||||
|         if (valueMetadataFormat === 'enum') { | ||||
|         if (valueMetadata.format === 'enum') { | ||||
|             this.formatter = {}; | ||||
|             this.enumerations = valueMetadata.enumerations.reduce(function (vm, e) { | ||||
|                 vm.byValue[e.value] = e.string; | ||||
| @@ -91,13 +77,13 @@ define([ | ||||
|         // Check for formatString support once instead of per format call. | ||||
|         if (valueMetadata.formatString) { | ||||
|             const baseFormat = this.formatter.format; | ||||
|             const formatString = getNonArrayValue(valueMetadata.formatString); | ||||
|             const formatString = valueMetadata.formatString; | ||||
|             this.formatter.format = function (value) { | ||||
|                 return printj.sprintf(formatString, baseFormat.call(this, value)); | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         if (valueMetadataFormat === 'string') { | ||||
|         if (valueMetadata.format === 'string') { | ||||
|             this.formatter.parse = function (value) { | ||||
|                 if (value === undefined) { | ||||
|                     return ''; | ||||
| @@ -122,14 +108,7 @@ define([ | ||||
|  | ||||
|     TelemetryValueFormatter.prototype.parse = function (datum) { | ||||
|         if (_.isObject(datum)) { | ||||
|             const objectDatum = datum[this.valueMetadata.source]; | ||||
|             if (Array.isArray(objectDatum)) { | ||||
|                 return objectDatum.map((item) => { | ||||
|                     return this.formatter.parse(item); | ||||
|                 }); | ||||
|             } else { | ||||
|                 return this.formatter.parse(objectDatum); | ||||
|             } | ||||
|             return this.formatter.parse(datum[this.valueMetadata.source]); | ||||
|         } | ||||
|  | ||||
|         return this.formatter.parse(datum); | ||||
| @@ -137,14 +116,7 @@ define([ | ||||
|  | ||||
|     TelemetryValueFormatter.prototype.format = function (datum) { | ||||
|         if (_.isObject(datum)) { | ||||
|             const objectDatum = datum[this.valueMetadata.source]; | ||||
|             if (Array.isArray(objectDatum)) { | ||||
|                 return objectDatum.map((item) => { | ||||
|                     return this.formatter.format(item); | ||||
|                 }); | ||||
|             } else { | ||||
|                 return this.formatter.format(objectDatum); | ||||
|             } | ||||
|             return this.formatter.format(datum[this.valueMetadata.source]); | ||||
|         } | ||||
|  | ||||
|         return this.formatter.format(datum); | ||||
|   | ||||
| @@ -66,13 +66,13 @@ export default class StatusAPI extends EventEmitter { | ||||
|      * @returns {Promise<Boolean>} true if operation was successful, otherwise false. | ||||
|      */ | ||||
|     async setPollQuestion(questionText) { | ||||
|         const canSetPollQuestion = await this.canSetPollQuestion(); | ||||
|  | ||||
|         if (canSetPollQuestion) { | ||||
|         if (this.canSetPollQuestion()) { | ||||
|             const provider = this.#userAPI.getProvider(); | ||||
|  | ||||
|             const result = await provider.setPollQuestion(questionText); | ||||
|  | ||||
|             // TODO re-implement clearing all statuses | ||||
|  | ||||
|             try { | ||||
|                 await this.resetAllStatuses(); | ||||
|             } catch (error) { | ||||
| @@ -124,8 +124,11 @@ export default class StatusAPI extends EventEmitter { | ||||
|  | ||||
|         if (provider.getStatusForRole) { | ||||
|             const status = await provider.getStatusForRole(role); | ||||
|  | ||||
|             return status; | ||||
|             if (status !== undefined) { | ||||
|                 return status; | ||||
|             } else { | ||||
|                 return undefined; | ||||
|             } | ||||
|         } else { | ||||
|             this.#userAPI.error("User provider does not support role status"); | ||||
|         } | ||||
| @@ -168,7 +171,7 @@ export default class StatusAPI extends EventEmitter { | ||||
|      */ | ||||
|     async resetStatusForRole(role) { | ||||
|         const provider = this.#userAPI.getProvider(); | ||||
|         const defaultStatus = await this.getDefaultStatusForRole(role); | ||||
|         const defaultStatus = await this.getDefaultStatus(); | ||||
|  | ||||
|         if (provider.setStatusForRole) { | ||||
|             return provider.setStatusForRole(role, defaultStatus); | ||||
| @@ -275,10 +278,10 @@ export default class StatusAPI extends EventEmitter { | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @typedef {import('./UserProvider')} UserProvider | ||||
|  * @typedef {import('./UserAPI').UserProvider} UserProvider | ||||
|  */ | ||||
| /** | ||||
|  * @typedef {import('./StatusUserProvider')} StatusUserProvider | ||||
|  * @typedef {import('./UserAPI').StatusUserProvider} StatusUserProvider | ||||
|  */ | ||||
| /** | ||||
|  * The PollQuestion type | ||||
|   | ||||
| @@ -1,103 +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 User Status API", () => { | ||||
|     let openmct; | ||||
|     let userProvider; | ||||
|     let mockUser; | ||||
|  | ||||
|     beforeEach(() => { | ||||
|         userProvider = jasmine.createSpyObj("userProvider", [ | ||||
|             "setPollQuestion", | ||||
|             "getPollQuestion", | ||||
|             "getCurrentUser", | ||||
|             "getPossibleStatuses", | ||||
|             "getAllStatusRoles", | ||||
|             "canSetPollQuestion", | ||||
|             "isLoggedIn", | ||||
|             "on" | ||||
|         ]); | ||||
|         openmct = createOpenMct(); | ||||
|         mockUser = new openmct.user.User("test-user", "A test user"); | ||||
|         userProvider.getCurrentUser.and.returnValue(Promise.resolve(mockUser)); | ||||
|         userProvider.getPossibleStatuses.and.returnValue(Promise.resolve([])); | ||||
|         userProvider.getAllStatusRoles.and.returnValue(Promise.resolve([])); | ||||
|         userProvider.canSetPollQuestion.and.returnValue(Promise.resolve(false)); | ||||
|         userProvider.isLoggedIn.and.returnValue(true); | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     describe("the poll question", () => { | ||||
|         it('can be set via a user status provider if supported', () => { | ||||
|             openmct.user.setProvider(userProvider); | ||||
|             userProvider.canSetPollQuestion.and.returnValue(Promise.resolve(true)); | ||||
|  | ||||
|             return openmct.user.status.setPollQuestion('This is a poll question').then(() => { | ||||
|                 expect(userProvider.setPollQuestion).toHaveBeenCalledWith('This is a poll question'); | ||||
|             }); | ||||
|         }); | ||||
|         // fit('emits an event when the poll question changes', () => { | ||||
|         //     const pollQuestionChangeCallback = jasmine.createSpy('pollQuestionChangeCallback'); | ||||
|         //     let pollQuestionListener; | ||||
|  | ||||
|         //     userProvider.canSetPollQuestion.and.returnValue(Promise.resolve(true)); | ||||
|         //     userProvider.on.and.callFake((eventName, listener) => { | ||||
|         //         if (eventName === 'pollQuestionChange') { | ||||
|         //             pollQuestionListener = listener; | ||||
|         //         } | ||||
|         //     }); | ||||
|  | ||||
|         //     openmct.user.on('pollQuestionChange', pollQuestionChangeCallback); | ||||
|  | ||||
|         //     openmct.user.setProvider(userProvider); | ||||
|  | ||||
|         //     return openmct.user.status.setPollQuestion('This is a poll question').then(() => { | ||||
|         //         expect(pollQuestionListener).toBeDefined(); | ||||
|         //         pollQuestionListener(); | ||||
|         //         expect(pollQuestionChangeCallback).toHaveBeenCalled(); | ||||
|  | ||||
|         //         const pollQuestion = pollQuestionChangeCallback.calls.mostRecent().args[0]; | ||||
|         //         expect(pollQuestion.question).toBe('This is a poll question'); | ||||
|  | ||||
|         //         openmct.user.off('pollQuestionChange', pollQuestionChangeCallback); | ||||
|         //     }); | ||||
|         // }); | ||||
|         it('cannot be set if the user is not permitted', () => { | ||||
|             openmct.user.setProvider(userProvider); | ||||
|             userProvider.canSetPollQuestion.and.returnValue(Promise.resolve(false)); | ||||
|  | ||||
|             return openmct.user.status.setPollQuestion('This is a poll question').catch((error) => { | ||||
|                 expect(error).toBeInstanceOf(Error); | ||||
|             }).finally(() => { | ||||
|                 expect(userProvider.setPollQuestion).not.toHaveBeenCalled(); | ||||
|             }); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
| @@ -33,7 +33,7 @@ function replaceDotsWithUnderscores(filename) { | ||||
|  | ||||
| import {saveAs} from 'saveAs'; | ||||
| import html2canvas from 'html2canvas'; | ||||
| import { v4 as uuid } from 'uuid'; | ||||
| import uuid from 'uuid'; | ||||
|  | ||||
| class ImageExporter { | ||||
|     constructor(openmct) { | ||||
| @@ -51,7 +51,7 @@ class ImageExporter { | ||||
|         const overlays = this.openmct.overlays; | ||||
|         const dialog = overlays.dialog({ | ||||
|             iconClass: 'info', | ||||
|             message: 'Capturing image, please wait...', | ||||
|             message: 'Caputuring an image', | ||||
|             buttons: [ | ||||
|                 { | ||||
|                     label: 'Cancel', | ||||
|   | ||||
| @@ -52,6 +52,7 @@ export default (agent, document) => { | ||||
|     if (agent.isMobile()) { | ||||
|         const mediaQuery = window.matchMedia("(orientation: landscape)"); | ||||
|         function eventHandler(event) { | ||||
|             console.log("changed"); | ||||
|             if (event.matches) { | ||||
|                 body.classList.remove("portrait"); | ||||
|                 body.classList.add("landscape"); | ||||
|   | ||||
| @@ -197,7 +197,7 @@ export default { | ||||
|             } | ||||
|         }, | ||||
|         setUnit() { | ||||
|             this.unit = this.valueMetadata ? this.valueMetadata.unit : ''; | ||||
|             this.unit = this.valueMetadata.unit || ''; | ||||
|         }, | ||||
|         firstNonDomainAttribute(metadata) { | ||||
|             return metadata | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user