Compare commits
	
		
			38 Commits
		
	
	
		
			mct5263-up
			...
			mutation-p
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 5e1d558cf7 | ||
|   | 488cd82ae1 | ||
|   | d85be3b88e | ||
|   | 4d48cf3180 | ||
|   | 413cb13edf | ||
|   | 70115be727 | ||
|   | 97f5528dfc | ||
|   | 0e1cc5dc30 | ||
|   | 0062191416 | ||
|   | eedc523078 | ||
|   | db97acb61e | ||
|   | 43a4bf9606 | ||
|   | 0f352087f5 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 8ce15521de | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | c0b0c44dc2 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | b9a644cd4f | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 1afc5ef245 | ||
|   | 34442c53c6 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 451ca075fe | ||
|   | 84f1a61a8d | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | ea041aaaf9 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | ac9420bfa1 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 0ebab10578 | ||
|   | cbdb9fc437 | ||
|   | 63d2246345 | ||
|   | 78002f0a24 | ||
|   | f08fd58486 | ||
|   | 730272e165 | ||
|   | 0f0c6a7b17 | ||
|   | 370e6a0c37 | ||
|   | 815506cf17 | ||
|   | bdb1867c73 | ||
|   | e288fdffea | ||
|   | 194060f30a | ||
|   | 45bc317a59 | ||
|   | e103ea44d8 | ||
|   | d13d7dc8f3 | ||
|   | 05e3303828 | 
| @@ -2,7 +2,7 @@ version: 2.1 | ||||
| executors: | ||||
|   pw-focal-development: | ||||
|     docker: | ||||
|       - image: mcr.microsoft.com/playwright:v1.21.1-focal | ||||
|       - image: mcr.microsoft.com/playwright:v1.23.0-focal | ||||
|     environment: | ||||
|       NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed | ||||
| parameters: | ||||
| @@ -12,7 +12,7 @@ parameters: | ||||
|     type: boolean | ||||
| commands: | ||||
|   build_and_install: | ||||
|     description: "All steps used to build and install. Will not work on node10" | ||||
|     description: "All steps used to build and install. Will use cache if found" | ||||
|     parameters: | ||||
|       node-version: | ||||
|         type: string | ||||
| @@ -58,10 +58,14 @@ commands: | ||||
|           ls -latR >> /tmp/artifacts/dir.txt | ||||
|       - store_artifacts: | ||||
|           path: /tmp/artifacts/ | ||||
|   upload_code_covio: | ||||
|     description: "Command to upload code coverage reports to codecov.io" | ||||
|     steps: | ||||
|         - run: curl -Os https://uploader.codecov.io/latest/linux/codecov;chmod +x codecov;./codecov | ||||
|   generate_e2e_code_cov_report: | ||||
|    description: "Generate e2e code coverage artifacts and publish to codecov.io. Needed to that we can ignore the exit code status of the npm run test" | ||||
|    parameters: | ||||
|       suite: | ||||
|         type: string | ||||
|    steps: | ||||
|     - run: npm run cov:e2e:report  | ||||
|     - run: npm run cov:e2e:<<parameters.suite>>:publish        | ||||
| orbs: | ||||
|   node: circleci/node@4.9.0 | ||||
|   browser-tools: circleci/browser-tools@1.3.0 | ||||
| @@ -114,12 +118,13 @@ jobs: | ||||
|             - browser-tools/install-chrome: | ||||
|                 replace-existing: false | ||||
|       - run: npm run test -- --browsers=<<parameters.browser>> | ||||
|       - run: npm run cov:unit:publish | ||||
|       - save_cache_cmd: | ||||
|           node-version: <<parameters.node-version>> | ||||
|       - store_test_results: | ||||
|           path: dist/reports/tests/ | ||||
|       - store_artifacts: | ||||
|           path: dist/reports/ | ||||
|           path: coverage | ||||
|       - generate_and_store_version_and_filesystem_artifacts | ||||
|   e2e-test: | ||||
|     parameters: | ||||
| @@ -132,11 +137,22 @@ jobs: | ||||
|     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>>           | ||||
|       - 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: | ||||
| @@ -151,19 +167,19 @@ jobs: | ||||
|           path: test-results/results.xml | ||||
|       - store_artifacts: | ||||
|           path: test-results | ||||
|       - generate_and_store_version_and_filesystem_artifacts       | ||||
|       - 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: node16-lint | ||||
|           node-version: lts/gallium | ||||
|       - unit-test: | ||||
|           name: node14-chrome | ||||
|           name: node14-lint | ||||
|           node-version: lts/fermium | ||||
|       - unit-test: | ||||
|           name: node16-chrome | ||||
|           node-version: lts/gallium | ||||
|           browser: ChromeHeadless | ||||
|           post-steps: | ||||
|             - upload_code_covio | ||||
|       - unit-test: | ||||
|           name: node18-chrome | ||||
|           node-version: "18" | ||||
|   | ||||
							
								
								
									
										3
									
								
								.github/workflows/e2e-pr.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.github/workflows/e2e-pr.yml
									
									
									
									
										vendored
									
									
								
							| @@ -30,7 +30,8 @@ jobs: | ||||
|       - uses: actions/setup-node@v3 | ||||
|         with: | ||||
|           node-version: '16' | ||||
|       - run: npx playwright@1.21.1 install | ||||
|       - run: npx playwright@1.23.0 install | ||||
|       - run: npx playwright install chrome-beta | ||||
|       - run: npm install | ||||
|       - run: npm run test:e2e:full | ||||
|       - name: Archive test results | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/workflows/e2e-visual.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/e2e-visual.yml
									
									
									
									
										vendored
									
									
								
							| @@ -17,7 +17,7 @@ jobs: | ||||
|       - uses: actions/setup-node@v3 | ||||
|         with: | ||||
|           node-version: '16' | ||||
|       - run: npx playwright@1.21.1 install | ||||
|       - run: npx playwright@1.23.0 install | ||||
|       - run: npm install | ||||
|       - name: Run the e2e visual tests | ||||
|         run: npm run test:e2e:visual | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/workflows/prcop-config.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/prcop-config.json
									
									
									
									
										vendored
									
									
								
							| @@ -3,7 +3,7 @@ | ||||
|     { | ||||
|       "name": "descriptionRegexp", | ||||
|       "config": { | ||||
|         "regexp": "x] Testing instructions", | ||||
|         "regexp": "[x|X]] Testing instructions", | ||||
|         "errorMessage": ":police_officer: PR Description does not confirm that associated issue(s) contain Testing instructions" | ||||
|       } | ||||
|     }, | ||||
|   | ||||
							
								
								
									
										22
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										22
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -15,8 +15,6 @@ | ||||
| *.idea | ||||
| *.iml | ||||
|  | ||||
| # External dependencies | ||||
|  | ||||
| # Build output | ||||
| target | ||||
| dist | ||||
| @@ -24,30 +22,24 @@ dist | ||||
| # Mac OS X Finder | ||||
| .DS_Store | ||||
|  | ||||
| # Closed source libraries | ||||
| closed-lib | ||||
|  | ||||
| # Node, Bower dependencies | ||||
| node_modules | ||||
| bower_components | ||||
|  | ||||
| # Protractor logs | ||||
| protractor/logs | ||||
|  | ||||
| # npm-debug log | ||||
| npm-debug.log | ||||
|  | ||||
| # karma reports | ||||
| report.*.json | ||||
|  | ||||
| # Lighthouse reports | ||||
| .lighthouseci | ||||
|  | ||||
| # e2e test artifacts | ||||
| test-results | ||||
| allure-results | ||||
| html-test-results | ||||
|  | ||||
| package-lock.json | ||||
|  | ||||
| #codecov artifacts | ||||
| # codecov artifacts | ||||
| .nyc_output | ||||
| coverage | ||||
| codecov | ||||
|  | ||||
| # :( | ||||
| package-lock.json | ||||
|   | ||||
							
								
								
									
										32
									
								
								app.js
									
									
									
									
									
								
							
							
						
						
									
										32
									
								
								app.js
									
									
									
									
									
								
							| @@ -12,6 +12,7 @@ const express = require('express'); | ||||
| const app = express(); | ||||
| const fs = require('fs'); | ||||
| const request = require('request'); | ||||
| const __DEV__ = !process.env.NODE_ENV || process.env.NODE_ENV === 'development'; | ||||
|  | ||||
| // Defaults | ||||
| options.port = options.port || options.p || 8080; | ||||
| @@ -49,14 +50,18 @@ class WatchRunPlugin { | ||||
| } | ||||
|  | ||||
| const webpack = require('webpack'); | ||||
| const webpackConfig = require('./webpack.dev.js'); | ||||
| webpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin()); | ||||
| webpackConfig.plugins.push(new WatchRunPlugin()); | ||||
|  | ||||
| webpackConfig.entry.openmct = [ | ||||
|     'webpack-hot-middleware/client?reload=true', | ||||
|     webpackConfig.entry.openmct | ||||
| ]; | ||||
| let webpackConfig; | ||||
| if (__DEV__) { | ||||
|     webpackConfig = require('./webpack.dev'); | ||||
|     webpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin()); | ||||
|     webpackConfig.entry.openmct = [ | ||||
|         'webpack-hot-middleware/client?reload=true', | ||||
|         webpackConfig.entry.openmct | ||||
|     ]; | ||||
|     webpackConfig.plugins.push(new WatchRunPlugin()); | ||||
| } else { | ||||
|     webpackConfig = require('./webpack.coverage'); | ||||
| } | ||||
|  | ||||
| const compiler = webpack(webpackConfig); | ||||
|  | ||||
| @@ -68,10 +73,12 @@ app.use(require('webpack-dev-middleware')( | ||||
|     } | ||||
| )); | ||||
|  | ||||
| app.use(require('webpack-hot-middleware')( | ||||
|     compiler, | ||||
|     {} | ||||
| )); | ||||
| if (__DEV__) { | ||||
|     app.use(require('webpack-hot-middleware')( | ||||
|         compiler, | ||||
|         {} | ||||
|     )); | ||||
| } | ||||
|  | ||||
| // Expose index.html for development users. | ||||
| app.get('/', function (req, res) { | ||||
| @@ -82,3 +89,4 @@ app.get('/', function (req, res) { | ||||
| app.listen(options.port, options.host, function () { | ||||
|     console.log('Open MCT application running at %s:%s', options.host, options.port); | ||||
| }); | ||||
|  | ||||
|   | ||||
							
								
								
									
										17
									
								
								codecov.yml
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								codecov.yml
									
									
									
									
									
								
							| @@ -13,17 +13,16 @@ coverage: | ||||
|   round: down | ||||
|   range: "66...100" | ||||
|  | ||||
| ignore: | ||||
|  | ||||
| parsers: | ||||
|   gcov: | ||||
|     branch_detection: | ||||
|       conditional: true | ||||
|       loop: true | ||||
|       method: false | ||||
|       macro: false | ||||
| flags: | ||||
|   unit: | ||||
|     carryforward: true  | ||||
|   e2e-ci: | ||||
|     carryforward: true | ||||
|   e2e-full: | ||||
|     carryforward: true     | ||||
|  | ||||
| comment: | ||||
|   layout: "reach,diff,flags,files,footer" | ||||
|   behavior: default | ||||
|   require_changes: false | ||||
|   show_carryforward_flags: true | ||||
							
								
								
									
										122
									
								
								e2e/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								e2e/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,122 @@ | ||||
| # e2e testing | ||||
|  | ||||
| This document captures information specific to the e2e testing of Open MCT. For general information about testing, please see [the Open MCT README](https://github.com/nasa/openmct/blob/master/README.md#tests). | ||||
|  | ||||
| ## Overview | ||||
|  | ||||
| This document is designed to capture on the What, Why, and How's of writing and running e2e tests in Open MCT. | ||||
|  | ||||
| ### About e2e testing | ||||
|  | ||||
| e2e testing is an industry-standard approach to automating the testing of web-based UIs such as Open MCT. Broadly speaking, e2e tests differentiate themselves from unit tests by preferring replication of real user interactions over execution of raw JavaScript functions. | ||||
|  | ||||
| Historically, the abstraction necessary to replicate real user behavior meant that: | ||||
|  | ||||
| - e2e tests were "expensive" due to how much code each test executed. The closer a test replicates the user, the more code is needed run during test execution. Unit tests could run smaller units of code more effeciently. | ||||
| - e2e tests were flaky due to network conditions or the underlying protocols associated with testing a browser. | ||||
| - e2e frameworks relied on a browser communication standard which lacked the observability and controls necessary needed to reach the code paths possible with unit and integration tests. | ||||
| - e2e frameworks provided insufficient debug information on test failure | ||||
|  | ||||
| However, as the web ecosystem has matured to the point where mission-critical UIs can be written for the web (Open MCT), the e2e testing tools have matured as well. There are now fewer "trade-offs" when choosing to write an e2e test over any other type of test. | ||||
|  | ||||
| Modern e2e frameworks: | ||||
|  | ||||
| - Bypass the surface layer of the web-application-under-test and use a raw debugging protocol to observe and control application and browser state. | ||||
| - These new browser-internal protocols enable near-instant, bi-directional communication between test code and the browser, speeding up test execution and making the tests as reliable as the application itself. | ||||
| - Provide test debug tooling which enables developers to pinpoint failure | ||||
|  | ||||
| Furthermore, the abstraction necessary to run e2e tests as a user enables them to be extended to run within a variety of contexts. This matches the extensible design of Open MCT.  | ||||
|  | ||||
| A single e2e test in Open MCT is extended to run: | ||||
|  | ||||
| - Against a matrix of browser versions. | ||||
| - Against a matrix of OS platforms. | ||||
| - Against a local development version of Open MCT. | ||||
| - A version of Open MCT loaded as a dependency (VIPER, VISTA, etc) | ||||
| - Against a variety of data sources or telemetry endpoints. | ||||
|  | ||||
| ### Why Playwright? | ||||
|  | ||||
| [Playwright](https://playwright.dev/) was chosen as our e2e framework because it solves a few VIPER Mission needs: | ||||
| 1. First-class support for Automated Performance Testing | ||||
| 2. Official Chrome, Chrome Canary, and iPad Capabilities | ||||
| 3. Support for Browserless.io | ||||
| 4. Ability to generate code coverage reports | ||||
|  | ||||
| ## Getting Started | ||||
|  | ||||
| ### Getting started with Playwright | ||||
|  | ||||
| ### Getting started with Open MCT's implementation of Playwright | ||||
|  | ||||
| ## Types of Testing | ||||
|  | ||||
| ### (TBD) Visual Testing | ||||
|  | ||||
| - Visual tests leverage [Percy](https://percy.io/). | ||||
| - Visual tests should be written within the `./tests/visual` folder so that they can be ignored during git clones to avoid leaking credentials when executing percy cli | ||||
|  | ||||
| #### (TBD) How to write a good visual test | ||||
|  | ||||
| ### (TBD) Snapshot Testing | ||||
|  | ||||
| <https://playwright.dev/docs/test-snapshots> | ||||
|  | ||||
| ### (TBD) Mobile Testing | ||||
|  | ||||
| ### (TBD) Performance Testing | ||||
|  | ||||
| ### (FUTURE) Component Testing | ||||
|  | ||||
| - Component testing is currrently possible in Playwright but not enabled on this project. For more, please see: <https://playwright.dev/docs/test-components> | ||||
|  | ||||
| ## Architecture, Test Design and Best Practices | ||||
|  | ||||
| ### (TBD) Architecture | ||||
|  | ||||
| #### (TBD)  Continuous Integration | ||||
|  | ||||
| - Test maturation | ||||
| - Difference between full and e2e-ci suites | ||||
| - Platforms | ||||
|  | ||||
| ### (TBD) Multi-browser and Multi-operating system | ||||
|  | ||||
| - Where is it tested | ||||
| - What's supported | ||||
|  | ||||
| ### (TBD) Test Design | ||||
|  | ||||
| - Re-usable tests for VISTA, VIPER, etc. | ||||
|  | ||||
| #### Annotations | ||||
|  | ||||
| - Annotations are a great way of organizing tests outside of a file structure. | ||||
| - Current list of annotations: | ||||
|   - `@ipad` - Mobile execution possible with Playwright's iPad support. | ||||
|   - `@gds` - Executes a GDS Test Case. Used to track in VIPER Mission. | ||||
|   - `@addInit` - Initializes the browser with an injected and artificial state. Useful for non-default plugins. | ||||
|   - `@localStorage` - Captures or generates session storage to manipulate browser state. Useful for excluding in tests which require a persistent backend (i.e. CouchDB). | ||||
|   - `@snapshot` - Uses Playwright's snapshot functionality to record a copy of the DOM for direct comparison. Must be run inside of a container. | ||||
|  | ||||
| ### (TBD) Best Practices | ||||
|  | ||||
| ### (TBD) Reporting | ||||
|  | ||||
| ### (TBD) Code Coverage | ||||
|  | ||||
| Code coverage is collected during test execution and reported with [nyc](https://github.com/istanbuljs/nyc) and [codecov.io](https://about.codecov.io/) | ||||
|  | ||||
| ## Other | ||||
|  | ||||
| ### FAQ | ||||
|  | ||||
| - How does this help NASA missions? | ||||
| - When should I write an e2e test instead of a unit test? | ||||
| - When should I write a functional vs visual test? | ||||
| - How is Open MCT extending default Playwright functionality? | ||||
|  | ||||
| ### Troubleshooting | ||||
|  | ||||
| - Why is my test failing on CI and not locally? | ||||
| - How can I view the failing tests on CI? | ||||
							
								
								
									
										18
									
								
								e2e/commonActions.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								e2e/commonActions.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| /** | ||||
|  * Wait for all animations within the given element and subtrees to finish | ||||
|  * See: https://github.com/microsoft/playwright/issues/15660#issuecomment-1184911658 | ||||
|  * @param {import('@playwright/test').Locator} locator | ||||
|  */ | ||||
| function waitForAnimations(locator) { | ||||
|     return locator | ||||
|         .evaluate((element) => | ||||
|             Promise.all( | ||||
|                 element | ||||
|                     .getAnimations({ subtree: true }) | ||||
|                     .map((animation) => animation.finished))); | ||||
| } | ||||
|  | ||||
| // eslint-disable-next-line no-undef | ||||
| module.exports = { | ||||
|     waitForAnimations | ||||
| }; | ||||
| @@ -1,18 +1,60 @@ | ||||
| /* eslint-disable no-undef */ | ||||
| /* 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. | ||||
|  */ | ||||
|  | ||||
| // 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.type()}] ${msg.text()}`)); | ||||
|         page.on('console', (msg) => messages.push(msg)); | ||||
|         await use(page); | ||||
|         await expect.soft(messages.toString()).not.toContain('[error]'); | ||||
|         messages.forEach( | ||||
|             msg => expect.soft(msg.type(), `Console error detected: ${consoleMessageToString(msg)}`).not.toEqual('error') | ||||
|         ); | ||||
|     }, | ||||
|     browser: async ({ playwright, browser }, use, workerInfo) => { | ||||
|     // Use browserless if configured | ||||
|         // Use browserless if configured | ||||
|         if (workerInfo.project.name.match(/browserless/)) { | ||||
|             const vBrowser = await playwright.chromium.connectOverCDP({ | ||||
|                 endpointURL: 'ws://localhost:3003' | ||||
|   | ||||
| @@ -4,28 +4,30 @@ | ||||
|  | ||||
| // 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: 1, | ||||
|     retries: 3, //Retries 3 times for a total of 4. When running sharded and with maxFailures = 5, this should ensure that flake is managed without failing the full suite | ||||
|     testDir: 'tests', | ||||
|     testIgnore: '**/*.perf.spec.js', //Ignore performance tests and define in playwright-perfromance.config.js | ||||
|     timeout: 60 * 1000, | ||||
|     webServer: { | ||||
|         command: 'npm run start', | ||||
|         port: 8080, | ||||
|         command: 'cross-env NODE_ENV=test npm run start', | ||||
|         url: 'http://localhost:8080/#', | ||||
|         timeout: 200 * 1000, | ||||
|         reuseExistingServer: !process.env.CI | ||||
|         reuseExistingServer: false | ||||
|     }, | ||||
|     maxFailures: process.env.CI ? 5 : undefined, //Limits failures to 5 to reduce CI Waste | ||||
|     workers: 2, //Limit to 2 for CircleCI Agent | ||||
|     maxFailures: MAX_FAILURES, //Limits failures to 5 to reduce CI Waste | ||||
|     workers: NUM_WORKERS, //Limit to 2 for CircleCI Agent | ||||
|     use: { | ||||
|         baseURL: 'http://localhost:8080/', | ||||
|         headless: true, | ||||
|         ignoreHTTPSErrors: true, | ||||
|         screenshot: 'only-on-failure', | ||||
|         trace: 'on-first-retry', | ||||
|         video: 'on-first-retry' | ||||
|         video: 'off' | ||||
|     }, | ||||
|     projects: [ | ||||
|         { | ||||
| @@ -36,6 +38,7 @@ const config = { | ||||
|         }, | ||||
|         { | ||||
|             name: 'MMOC', | ||||
|             testMatch: '**/*.e2e.spec.js', // only run e2e tests | ||||
|             grepInvert: /@snapshot/, | ||||
|             use: { | ||||
|                 browserName: 'chromium', | ||||
| @@ -44,20 +47,30 @@ const config = { | ||||
|                     height: 1440 | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         /*{ | ||||
|             name: 'ipad', | ||||
|         }, | ||||
|         { | ||||
|             name: 'firefox', | ||||
|             testMatch: '**/*.e2e.spec.js', // only run e2e tests | ||||
|             grepInvert: /@snapshot/, | ||||
|             use: { | ||||
|                 browserName: 'webkit', | ||||
|                 ...devices['iPad (gen 7) landscape'] // Complete List https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json | ||||
|                 browserName: 'firefox' | ||||
|             } | ||||
|         }*/ | ||||
|         }, | ||||
|         { | ||||
|             name: 'chrome-beta', //Only Chrome Beta is available on ubuntu -- not chrome canary | ||||
|             testMatch: '**/*.e2e.spec.js', // only run e2e tests | ||||
|             grepInvert: /@snapshot/, | ||||
|             use: { | ||||
|                 browserName: 'chromium', | ||||
|                 channel: 'chrome-beta' | ||||
|             } | ||||
|         } | ||||
|     ], | ||||
|     reporter: [ | ||||
|         ['list'], | ||||
|         ['html', { | ||||
|             open: 'never', | ||||
|             outputFolder: '../test-results/html/' | ||||
|             outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840 | ||||
|         }], | ||||
|         ['junit', { outputFile: 'test-results/results.xml' }], | ||||
|         ['github'] | ||||
|   | ||||
| @@ -12,10 +12,10 @@ const config = { | ||||
|     testIgnore: '**/*.perf.spec.js', | ||||
|     timeout: 30 * 1000, | ||||
|     webServer: { | ||||
|         command: 'npm run start', | ||||
|         port: 8080, | ||||
|         command: 'cross-env NODE_ENV=test npm run start', | ||||
|         url: 'http://localhost:8080/#', | ||||
|         timeout: 120 * 1000, | ||||
|         reuseExistingServer: !process.env.CI | ||||
|         reuseExistingServer: true | ||||
|     }, | ||||
|     workers: 1, | ||||
|     use: { | ||||
| @@ -25,7 +25,7 @@ const config = { | ||||
|         ignoreHTTPSErrors: true, | ||||
|         screenshot: 'only-on-failure', | ||||
|         trace: 'retain-on-failure', | ||||
|         video: 'retain-on-failure' | ||||
|         video: 'off' | ||||
|     }, | ||||
|     projects: [ | ||||
|         { | ||||
| @@ -36,6 +36,7 @@ const config = { | ||||
|         }, | ||||
|         { | ||||
|             name: 'MMOC', | ||||
|             testMatch: '**/*.e2e.spec.js', // only run e2e tests | ||||
|             grepInvert: /@snapshot/, | ||||
|             use: { | ||||
|                 browserName: 'chromium', | ||||
| @@ -44,20 +45,58 @@ 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: '../test-results' | ||||
|             outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840 | ||||
|         }] | ||||
|     ] | ||||
| }; | ||||
|   | ||||
| @@ -2,22 +2,24 @@ | ||||
| // playwright.config.js | ||||
| // @ts-check | ||||
|  | ||||
| const CI = process.env.CI === 'true'; | ||||
|  | ||||
| /** @type {import('@playwright/test').PlaywrightTestConfig} */ | ||||
| const config = { | ||||
|     retries: 1, //Only for debugging purposes | ||||
|     retries: 1, //Only for debugging purposes because trace is enabled only on first retry | ||||
|     testDir: 'tests/performance/', | ||||
|     timeout: 60 * 1000, | ||||
|     workers: 1, //Only run in serial with 1 worker | ||||
|     webServer: { | ||||
|         command: 'npm run start', | ||||
|         port: 8080, | ||||
|         command: 'cross-env NODE_ENV=test npm run start', | ||||
|         url: 'http://localhost:8080/#', | ||||
|         timeout: 200 * 1000, | ||||
|         reuseExistingServer: !process.env.CI | ||||
|         reuseExistingServer: !CI | ||||
|     }, | ||||
|     use: { | ||||
|         browserName: "chromium", | ||||
|         baseURL: 'http://localhost:8080/', | ||||
|         headless: Boolean(process.env.CI), //Only if running locally | ||||
|         headless: CI, //Only if running locally | ||||
|         ignoreHTTPSErrors: true, | ||||
|         screenshot: 'off', | ||||
|         trace: 'on-first-retry', | ||||
|   | ||||
| @@ -9,8 +9,8 @@ const config = { | ||||
|     timeout: 90 * 1000, | ||||
|     workers: 1, // visual tests should never run in parallel due to test pollution | ||||
|     webServer: { | ||||
|         command: 'npm run start', | ||||
|         port: 8080, | ||||
|         command: 'cross-env NODE_ENV=test npm run start', | ||||
|         url: 'http://localhost:8080/#', | ||||
|         timeout: 200 * 1000, | ||||
|         reuseExistingServer: !process.env.CI | ||||
|     }, | ||||
| @@ -21,7 +21,7 @@ const config = { | ||||
|         ignoreHTTPSErrors: true, | ||||
|         screenshot: 'on', | ||||
|         trace: 'off', | ||||
|         video: 'on' | ||||
|         video: 'off' | ||||
|     }, | ||||
|     reporter: [ | ||||
|         ['list'], | ||||
|   | ||||
							
								
								
									
										22
									
								
								e2e/test-data/VisualTestData_storage.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								e2e/test-data/VisualTestData_storage.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| { | ||||
|   "cookies": [], | ||||
|   "origins": [ | ||||
|     { | ||||
|       "origin": "http://localhost:8080", | ||||
|       "localStorage": [ | ||||
|         { | ||||
|           "name": "tcHistory", | ||||
|           "value": "{\"utc\":[{\"start\":1654548551471,\"end\":1654550351471}]}" | ||||
|         }, | ||||
|         { | ||||
|           "name": "mct", | ||||
|           "value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"527856c0-cced-4b64-bb19-f943432326d0\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1654550352296,\"modified\":1654550352296},\"527856c0-cced-4b64-bb19-f943432326d0\":{\"identifier\":{\"key\":\"527856c0-cced-4b64-bb19-f943432326d0\",\"namespace\":\"\"},\"name\":\"Unnamed Overlay Plot\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"ce88ce37-8bb9-45e1-a85b-bb7e3c8453b9\",\"namespace\":\"\"}],\"configuration\":{\"series\":[{\"identifier\":{\"key\":\"ce88ce37-8bb9-45e1-a85b-bb7e3c8453b9\",\"namespace\":\"\"}}],\"yAxis\":{},\"xAxis\":{}},\"modified\":1654550353356,\"location\":\"mine\",\"persisted\":1654550353357},\"ce88ce37-8bb9-45e1-a85b-bb7e3c8453b9\":{\"name\":\"Unnamed Sine Wave Generator\",\"type\":\"generator\",\"identifier\":{\"key\":\"ce88ce37-8bb9-45e1-a85b-bb7e3c8453b9\",\"namespace\":\"\"},\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":\"5000\"},\"modified\":1654550353350,\"location\":\"527856c0-cced-4b64-bb19-f943432326d0\",\"persisted\":1654550353350}}" | ||||
|         }, | ||||
|         { | ||||
|           "name": "mct-tree-expanded", | ||||
|           "value": "[\"/browse/mine\"]" | ||||
|         } | ||||
|       ] | ||||
|     } | ||||
|   ] | ||||
| } | ||||
							
								
								
									
										22
									
								
								e2e/test-data/recycled_local_storage.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								e2e/test-data/recycled_local_storage.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| { | ||||
|   "cookies": [], | ||||
|   "origins": [ | ||||
|     { | ||||
|       "origin": "http://localhost:8080", | ||||
|       "localStorage": [ | ||||
|         { | ||||
|           "name": "tcHistory", | ||||
|           "value": "{\"utc\":[{\"start\":1654537164464,\"end\":1654538964464},{\"start\":1652301954635,\"end\":1652303754635}]}" | ||||
|         }, | ||||
|         { | ||||
|           "name": "mct", | ||||
|           "value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1654538965703,\"modified\":1654538965703},\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"73f2d9ae-d1f3-4561-b7fc-ecd5df557249\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1652303755999,\"location\":\"mine\",\"persisted\":1652303756002},\"2d02a680-eb7e-4645-bba2-dd298f76efb8\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"4291d80c-303c-4d8d-85e1-10f012b864fb\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1654538965702,\"location\":\"mine\",\"persisted\":1654538965702}}" | ||||
|         }, | ||||
|         { | ||||
|           "name": "mct-tree-expanded", | ||||
|           "value": "[]" | ||||
|         } | ||||
|       ] | ||||
|     } | ||||
|   ] | ||||
| } | ||||
| @@ -36,7 +36,7 @@ test.describe('Branding tests', () => { | ||||
|         await page.click('.l-shell__app-logo'); | ||||
|  | ||||
|         // Verify that the NASA Logo Appears | ||||
|         await expect(await page.locator('.c-about__image')).toBeVisible(); | ||||
|         await expect(page.locator('.c-about__image')).toBeVisible(); | ||||
|  | ||||
|         // Modify the Build information in 'about' Modal | ||||
|         const versionInformationLocator = page.locator('ul.t-info.l-info.s-info'); | ||||
| @@ -58,6 +58,7 @@ 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,7 +28,9 @@ const { test } = require('../../../fixtures.js'); | ||||
| const { expect } = require('@playwright/test'); | ||||
|  | ||||
| test.describe('Sine Wave Generator', () => { | ||||
|     test('Create new Sine Wave Generator Object and validate create Form Logic', async ({ page }) => { | ||||
|     test('Create new Sine Wave Generator Object and validate create Form Logic', async ({ page, browserName }) => { | ||||
|         test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox'); | ||||
|  | ||||
|         //Go to baseURL | ||||
|         await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
| @@ -40,44 +42,45 @@ test.describe('Sine Wave Generator', () => { | ||||
|  | ||||
|         // Verify that the each required field has required indicator | ||||
|         // Title | ||||
|         await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(['c-form-row__state-indicator  req']); | ||||
|         await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(/req/); | ||||
|  | ||||
|         // Verify that the Notes row does not have a required indicator | ||||
|         await expect(page.locator('.c-form__section div:nth-child(3) .form-row .c-form-row__state-indicator')).not.toContain('.req'); | ||||
|         await page.locator('textarea[type="text"]').fill('Optional Note Text'); | ||||
|  | ||||
|         // Period | ||||
|         await expect(page.locator('.c-form__section div:nth-child(4) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator  req']); | ||||
|         await expect(page.locator('div:nth-child(4) .c-form-row__state-indicator')).toHaveClass(/req/); | ||||
|  | ||||
|         // Amplitude | ||||
|         await expect(page.locator('.c-form__section div:nth-child(5) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator  req']); | ||||
|         await expect(page.locator('div:nth-child(5) .c-form-row__state-indicator')).toHaveClass(/req/); | ||||
|  | ||||
|         // Offset | ||||
|         await expect(page.locator('.c-form__section div:nth-child(6) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator  req']); | ||||
|         await expect(page.locator('div:nth-child(6) .c-form-row__state-indicator')).toHaveClass(/req/); | ||||
|  | ||||
|         // Data Rate | ||||
|         await expect(page.locator('.c-form__section div:nth-child(7) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator  req']); | ||||
|         await expect(page.locator('div:nth-child(7) .c-form-row__state-indicator')).toHaveClass(/req/); | ||||
|  | ||||
|         // Phase | ||||
|         await expect(page.locator('.c-form__section div:nth-child(8) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator  req']); | ||||
|         await expect(page.locator('div:nth-child(8) .c-form-row__state-indicator')).toHaveClass(/req/); | ||||
|  | ||||
|         // Randomness | ||||
|         await expect(page.locator('.c-form__section div:nth-child(9) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator  req']); | ||||
|         await expect(page.locator('div:nth-child(9) .c-form-row__state-indicator')).toHaveClass(/req/); | ||||
|  | ||||
|         // Verify that by removing value from required text field shows invalid indicator | ||||
|         await page.locator('text=Properties Title Notes Period Amplitude Offset Data Rate (hz) Phase (radians) Ra >> input[type="text"]').fill(''); | ||||
|         await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(['c-form-row__state-indicator  req invalid']); | ||||
|         await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(/invalid/); | ||||
|  | ||||
|         // Verify that by adding value to empty required text field changes invalid to valid indicator | ||||
|         await page.locator('text=Properties Title Notes Period Amplitude Offset Data Rate (hz) Phase (radians) Ra >> input[type="text"]').fill('non empty'); | ||||
|         await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(['c-form-row__state-indicator  req valid']); | ||||
|         await page.locator('text=Properties Title Notes Period Amplitude Offset Data Rate (hz) Phase (radians) Ra >> input[type="text"]').fill('New Sine Wave Generator'); | ||||
|         await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(/valid/); | ||||
|  | ||||
|         // Verify that by removing value from required number field shows invalid indicator | ||||
|         await page.locator('.field.control.l-input-sm input').first().fill(''); | ||||
|         await expect(page.locator('.c-form__section div:nth-child(4) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator  req invalid']); | ||||
|         await expect(page.locator('div:nth-child(4) .c-form-row__state-indicator')).toHaveClass(/invalid/); | ||||
|  | ||||
|         // Verify that by adding value to empty required number field changes invalid to valid indicator | ||||
|         await page.locator('.field.control.l-input-sm input').first().fill('3'); | ||||
|         await expect(page.locator('.c-form__section div:nth-child(4) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator  req valid']); | ||||
|         await expect(page.locator('div:nth-child(4) .c-form-row__state-indicator')).toHaveClass(/valid/); | ||||
|  | ||||
|         // Verify that can change value of number field by up/down arrows keys | ||||
|         // Click .field.control.l-input-sm input >> nth=0 | ||||
| @@ -90,57 +93,6 @@ test.describe('Sine Wave Generator', () => { | ||||
|         const value = await page.locator('.field.control.l-input-sm input').first().inputValue(); | ||||
|         await expect(value).toBe('6'); | ||||
|  | ||||
|         // Click .c-form-row__state-indicator.grows | ||||
|         await page.locator('.c-form-row__state-indicator.grows').click(); | ||||
|  | ||||
|         // Click text=Properties Title Notes Period Amplitude Offset Data Rate (hz) Phase (radians) Ra >> input[type="text"] | ||||
|         await page.locator('text=Properties Title Notes Period Amplitude Offset Data Rate (hz) Phase (radians) Ra >> input[type="text"]').click(); | ||||
|  | ||||
|         // Click .c-form-row__state-indicator >> nth=0 | ||||
|         await page.locator('.c-form-row__state-indicator').first().click(); | ||||
|  | ||||
|         // Fill text=Properties Title Notes Period Amplitude Offset Data Rate (hz) Phase (radians) Ra >> input[type="text"] | ||||
|         await page.locator('text=Properties Title Notes Period Amplitude Offset Data Rate (hz) Phase (radians) Ra >> input[type="text"]').fill('New Sine Wave Generator'); | ||||
|  | ||||
|         // Double click div:nth-child(4) .form-row .c-form-row__controls | ||||
|         await page.locator('div:nth-child(4) .form-row .c-form-row__controls').dblclick(); | ||||
|  | ||||
|         // Click .field.control.l-input-sm input >> nth=0 | ||||
|         await page.locator('.field.control.l-input-sm input').first().click(); | ||||
|  | ||||
|         // Click div:nth-child(4) .form-row .c-form-row__state-indicator | ||||
|         await page.locator('div:nth-child(4) .form-row .c-form-row__state-indicator').click(); | ||||
|  | ||||
|         // Click .field.control.l-input-sm input >> nth=0 | ||||
|         await page.locator('.field.control.l-input-sm input').first().click(); | ||||
|  | ||||
|         // Click .field.control.l-input-sm input >> nth=0 | ||||
|         await page.locator('.field.control.l-input-sm input').first().click(); | ||||
|  | ||||
|         // Click div:nth-child(5) .form-row .c-form-row__controls .form-control .field input | ||||
|         await page.locator('div:nth-child(5) .form-row .c-form-row__controls .form-control .field input').click(); | ||||
|  | ||||
|         // Click div:nth-child(5) .form-row .c-form-row__controls .form-control .field input | ||||
|         await page.locator('div:nth-child(5) .form-row .c-form-row__controls .form-control .field input').click(); | ||||
|  | ||||
|         // Click div:nth-child(5) .form-row .c-form-row__controls .form-control .field input | ||||
|         await page.locator('div:nth-child(5) .form-row .c-form-row__controls .form-control .field input').click(); | ||||
|  | ||||
|         // Click div:nth-child(6) .form-row .c-form-row__controls .form-control .field input | ||||
|         await page.locator('div:nth-child(6) .form-row .c-form-row__controls .form-control .field input').click(); | ||||
|  | ||||
|         // Double click div:nth-child(7) .form-row .c-form-row__controls .form-control .field input | ||||
|         await page.locator('div:nth-child(7) .form-row .c-form-row__controls .form-control .field input').dblclick(); | ||||
|  | ||||
|         // Click div:nth-child(7) .form-row .c-form-row__state-indicator | ||||
|         await page.locator('div:nth-child(7) .form-row .c-form-row__state-indicator').click(); | ||||
|  | ||||
|         // Click div:nth-child(7) .form-row .c-form-row__controls .form-control .field input | ||||
|         await page.locator('div:nth-child(7) .form-row .c-form-row__controls .form-control .field input').click(); | ||||
|  | ||||
|         // Fill div:nth-child(7) .form-row .c-form-row__controls .form-control .field input | ||||
|         await page.locator('div:nth-child(7) .form-row .c-form-row__controls .form-control .field input').fill('3'); | ||||
|  | ||||
|         //Click text=OK | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
| @@ -151,7 +103,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 | ||||
|         // Verify canvas rendered and can be interacted with | ||||
|         await page.locator('canvas').nth(1).click({ | ||||
|             position: { | ||||
|                 x: 341, | ||||
|   | ||||
							
								
								
									
										55
									
								
								e2e/tests/framework.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								e2e/tests/framework.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* | ||||
| This test suite is dedicated to testing our use of the playwright framework as it | ||||
| relates to how we've extended it (i.e. ./e2e/fixtures.js) and assumptions made in our dev environment | ||||
| (app.js and ./e2e/webpack-dev-middleware.js) | ||||
| */ | ||||
|  | ||||
| const { test } = require('../fixtures.js'); | ||||
|  | ||||
| test.describe('fixtures.js tests', () => { | ||||
|     test('Verify that tests fail if console.error is thrown', async ({ page }) => { | ||||
|         test.fail(); | ||||
|         //Go to baseURL | ||||
|         await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|         //Verify that ../fixtures.js detects console log errors | ||||
|         await Promise.all([ | ||||
|             page.evaluate(() => console.error('This should result in a failure')), | ||||
|             page.waitForEvent('console') // always wait for the event to happen while triggering it! | ||||
|         ]); | ||||
|  | ||||
|     }); | ||||
|     test('Verify that tests pass if console.warn is thrown', async ({ page }) => { | ||||
|         //Go to baseURL | ||||
|         await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|         //Verify that ../fixtures.js detects console log errors | ||||
|         await Promise.all([ | ||||
|             page.evaluate(() => console.warn('This should result in a pass')), | ||||
|             page.waitForEvent('console') // always wait for the event to happen while triggering it! | ||||
|         ]); | ||||
|  | ||||
|     }); | ||||
| }); | ||||
| @@ -40,9 +40,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); | ||||
|  | ||||
|         // 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(), | ||||
| @@ -59,9 +56,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(folder2); | ||||
|  | ||||
|         // Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184 | ||||
|         await page.click('form[name="mctForm"] a:has-text("My Items")'); | ||||
|  | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('text=OK').click(), | ||||
| @@ -97,9 +91,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(telemetryTable); | ||||
|  | ||||
|         // Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184 | ||||
|         await page.click('form[name="mctForm"] a:has-text("My Items")'); | ||||
|  | ||||
|         await page.locator('text=OK').click(); | ||||
|  | ||||
|         // Finish editing and save Telemetry Table | ||||
|   | ||||
| @@ -28,9 +28,7 @@ const { test } = require('../../fixtures.js'); | ||||
| const { expect } = require('@playwright/test'); | ||||
| const path = require('path'); | ||||
|  | ||||
| // https://github.com/nasa/openmct/issues/4323#issuecomment-1067282651 | ||||
|  | ||||
| test.describe('Persistence operations', () => { | ||||
| test.describe('Persistence operations @addInit', () => { | ||||
|     // add non persistable root item | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         // eslint-disable-next-line no-undef | ||||
| @@ -38,6 +36,10 @@ test.describe('Persistence operations', () => { | ||||
|     }); | ||||
|  | ||||
|     test('Persistability should be respected in the create form location field', async ({ page }) => { | ||||
|         test.info().annotations.push({ | ||||
|             type: 'issue', | ||||
|             description: 'https://github.com/nasa/openmct/issues/4323' | ||||
|         }); | ||||
|         // Go to baseURL | ||||
|         await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|   | ||||
| @@ -33,7 +33,7 @@ let conditionSetUrl; | ||||
| let getConditionSetIdentifierFromUrl; | ||||
|  | ||||
| test.describe.serial('Condition Set CRUD Operations on @localStorage', () => { | ||||
|     test.beforeAll(async ({ browser }) => { | ||||
|     test.beforeAll(async ({ browser}) => { | ||||
|         const context = await browser.newContext(); | ||||
|         const page = await context.newPage(); | ||||
|         //Go to baseURL | ||||
| @@ -52,30 +52,29 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => { | ||||
|         ]); | ||||
|  | ||||
|         //Save localStorage for future test execution | ||||
|         await context.storageState({ path: './e2e/tests/recycled_storage.json' }); | ||||
|         await context.storageState({ path: './e2e/test-data/recycled_local_storage.json' }); | ||||
|  | ||||
|         //Set object identifier from url | ||||
|         conditionSetUrl = await page.url(); | ||||
|         conditionSetUrl = page.url(); | ||||
|         console.log('conditionSetUrl ' + conditionSetUrl); | ||||
|  | ||||
|         getConditionSetIdentifierFromUrl = await conditionSetUrl.split('/').pop().split('?')[0]; | ||||
|         getConditionSetIdentifierFromUrl = conditionSetUrl.split('/').pop().split('?')[0]; | ||||
|         console.debug('getConditionSetIdentifierFromUrl ' + getConditionSetIdentifierFromUrl); | ||||
|         await page.close(); | ||||
|     }); | ||||
|     test.afterAll(async ({ browser }) => { | ||||
|         await browser.close(); | ||||
|     }); | ||||
|  | ||||
|     //Load localStorage for subsequent tests | ||||
|     test.use({ storageState: './e2e/tests/recycled_storage.json' }); | ||||
|     test.use({ storageState: './e2e/test-data/recycled_local_storage.json' }); | ||||
|     //Begin suite of tests again localStorage | ||||
|     test('Condition set object properties persist in main view and inspector', async ({ page }) => { | ||||
|     test('Condition set object properties persist in main view and inspector @localStorage', async ({ page }) => { | ||||
|         //Navigate to baseURL with injected localStorage | ||||
|         await page.goto(conditionSetUrl, { waitUntil: 'networkidle' }); | ||||
|  | ||||
|         //Assertions on loaded Condition Set in main view | ||||
|         //Assertions on loaded Condition Set in main view. This is a stateful transition step after page.goto() | ||||
|         await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set'); | ||||
|  | ||||
|         //Assertions on loaded Condition Set in Inspector | ||||
|         await expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy; | ||||
|         expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy(); | ||||
|  | ||||
|         //Reload Page | ||||
|         await Promise.all([ | ||||
| @@ -86,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 | ||||
|         await expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy; | ||||
|         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 | ||||
|         //Assertions on loaded Condition Set in main view. This is a stateful transition step after page.goto() | ||||
|         await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set'); | ||||
|  | ||||
|         //Update the Condition Set properties | ||||
| @@ -112,18 +111,18 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => { | ||||
|  | ||||
|         // Verify Inspector properties | ||||
|         // Verify Inspector has updated Name property | ||||
|         await expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy(); | ||||
|         expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy(); | ||||
|         // Verify Inspector Details has updated Name property | ||||
|         await expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy(); | ||||
|         expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy(); | ||||
|  | ||||
|         // Verify Tree reflects updated Name proprety | ||||
|         // Expand Tree | ||||
|         await page.locator('text=Open MCT My Items >> span >> nth=3').click(); | ||||
|         // Verify Condition Set Object is renamed in Tree | ||||
|         await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); | ||||
|         expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); | ||||
|         // Verify Search Tree reflects renamed Name property | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Renamed'); | ||||
|         await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); | ||||
|         expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); | ||||
|  | ||||
|         //Reload Page | ||||
|         await Promise.all([ | ||||
| @@ -136,40 +135,42 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => { | ||||
|  | ||||
|         // Verify Inspector properties | ||||
|         // Verify Inspector has updated Name property | ||||
|         await expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy(); | ||||
|         expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy(); | ||||
|         // Verify Inspector Details has updated Name property | ||||
|         await expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy(); | ||||
|         expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy(); | ||||
|  | ||||
|         // Verify Tree reflects updated Name proprety | ||||
|         // Expand Tree | ||||
|         await page.locator('text=Open MCT My Items >> span >> nth=3').click(); | ||||
|         // Verify Condition Set Object is renamed in Tree | ||||
|         await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); | ||||
|         expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); | ||||
|         // Verify Search Tree reflects renamed Name property | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Renamed'); | ||||
|         await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); | ||||
|         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' }); | ||||
|  | ||||
|         const numberOfConditionSetsToStart = await page.locator('a:has-text("Unnamed Condition Set Condition Set")').count(); | ||||
|         //Expect Unnamed Condition Set to be visible in Main View | ||||
|         //Assertions on loaded Condition Set in main view. This is a stateful transition step after page.goto() | ||||
|         await expect(page.locator('a:has-text("Unnamed Condition Set Condition Set") >> nth=0')).toBeVisible(); | ||||
|  | ||||
|         const numberOfConditionSetsToStart = await page.locator('a:has-text("Unnamed Condition Set Condition Set")').count(); | ||||
|  | ||||
|         // Search for Unnamed Condition Set | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Unnamed Condition Set'); | ||||
|         // Click Search Result | ||||
|         await page.locator('[aria-label="OpenMCT Search"] >> text=Unnamed Condition Set').first().click(); | ||||
|         // Click hamburger button | ||||
|         await page.locator('[title="More options"]').click(); | ||||
|  | ||||
|         // Click text=Remove | ||||
|         await page.locator('text=Remove').click(); | ||||
|  | ||||
|         await page.locator('text=OK').click(); | ||||
|  | ||||
|         //Expect Unnamed Condition Set to be removed in Main View | ||||
|         const numberOfConditionSetsAtEnd = await page.locator('a:has-text("Unnamed Condition Set Condition Set")').count(); | ||||
|  | ||||
|         expect(numberOfConditionSetsAtEnd).toEqual(numberOfConditionSetsToStart - 1); | ||||
|  | ||||
|         //Feature? | ||||
|   | ||||
| @@ -28,8 +28,12 @@ but only assume that example imagery is present. | ||||
|  | ||||
| const { test } = require('../../../fixtures.js'); | ||||
| const { expect } = require('@playwright/test'); | ||||
| const { waitForAnimations } = require('../../../commonActions.js'); | ||||
|  | ||||
| test.describe('Example Imagery', () => { | ||||
| const backgroundImageSelector = '.c-imagery__main-image__background-image'; | ||||
|  | ||||
| //The following block of tests verifies the basic functionality of example imagery and serves as a template for Imagery objects embedded in other objects. | ||||
| test.describe('Example Imagery Object', () => { | ||||
|  | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         //Go to baseURL | ||||
| @@ -41,9 +45,6 @@ test.describe('Example Imagery', () => { | ||||
|         // Click text=Example Imagery | ||||
|         await page.click('text=Example Imagery'); | ||||
|  | ||||
|         // Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184 | ||||
|         await page.click('form[name="mctForm"] a:has-text("My Items")'); | ||||
|  | ||||
|         // Click text=OK | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation({waitUntil: 'networkidle'}), | ||||
| @@ -51,48 +52,58 @@ test.describe('Example Imagery', () => { | ||||
|             //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 = page.locator(backgroundImageSelector); | ||||
|         const deltaYStep = 100; //equivalent to 1x zoom | ||||
|         await bgImageLocator.hover({trial: true}); | ||||
|         await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|         const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|         // zoom in | ||||
|         await bgImageLocator.hover({trial: true}); | ||||
|         await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|         await page.mouse.wheel(0, deltaYStep * 2); | ||||
|         // wait for zoom animation to finish | ||||
|         await bgImageLocator.hover({trial: true}); | ||||
|         await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|         const imageMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|         // zoom out | ||||
|         await bgImageLocator.hover({trial: true}); | ||||
|         await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|         await page.mouse.wheel(0, -deltaYStep); | ||||
|         // wait for zoom animation to finish | ||||
|         await bgImageLocator.hover({trial: true}); | ||||
|         await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|         const imageMouseZoomedOut = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|  | ||||
|         expect(imageMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height); | ||||
|         expect(imageMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width); | ||||
|         expect(imageMouseZoomedOut.height).toBeLessThan(imageMouseZoomedIn.height); | ||||
|         expect(imageMouseZoomedOut.width).toBeLessThan(imageMouseZoomedIn.width); | ||||
|     }); | ||||
|  | ||||
|     test('Can adjust image brightness/contrast by dragging the sliders', async ({ page, browserName }) => { | ||||
|         test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox'); | ||||
|         // Open the image filter menu | ||||
|         await page.locator('[role=toolbar] button[title="Brightness and contrast"]').click(); | ||||
|  | ||||
|         // Drag the brightness and contrast sliders around and assert filter values | ||||
|         await dragBrightnessSliderAndAssertFilterValues(page); | ||||
|         await dragContrastSliderAndAssertFilterValues(page); | ||||
|     }); | ||||
|  | ||||
|     test('Can use alt+drag to move around image once zoomed in', async ({ page }) => { | ||||
|         const deltaYStep = 100; //equivalent to 1x zoom | ||||
|         const panHotkey = process.platform === 'linux' ? ['Control', 'Alt'] : ['Alt']; | ||||
|  | ||||
|         const bgImageLocator = page.locator(backgroundImageSelector); | ||||
|         await bgImageLocator.hover({trial: true}); | ||||
|         await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|  | ||||
|         // zoom in | ||||
|         await page.mouse.wheel(0, deltaYStep * 2); | ||||
|         await bgImageLocator.hover({trial: true}); | ||||
|         const zoomedBoundingBox = await bgImageLocator.boundingBox(); | ||||
|         await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|         const zoomedBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|         const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2; | ||||
|         const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2; | ||||
|         // move to the right | ||||
| @@ -115,7 +126,7 @@ test.describe('Example Imagery', () => { | ||||
|         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 bgImageLocator.boundingBox(); | ||||
|         const afterRightPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|         expect(zoomedBoundingBox.x).toBeGreaterThan(afterRightPanBoundingBox.x); | ||||
|  | ||||
|         // pan left | ||||
| @@ -124,7 +135,7 @@ test.describe('Example Imagery', () => { | ||||
|         await page.mouse.move(imageCenterX, imageCenterY, 10); | ||||
|         await page.mouse.up(); | ||||
|         await Promise.all(panHotkey.map(x => page.keyboard.up(x))); | ||||
|         const afterLeftPanBoundingBox = await bgImageLocator.boundingBox(); | ||||
|         const afterLeftPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|         expect(afterRightPanBoundingBox.x).toBeLessThan(afterLeftPanBoundingBox.x); | ||||
|  | ||||
|         // pan up | ||||
| @@ -134,7 +145,7 @@ test.describe('Example Imagery', () => { | ||||
|         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 bgImageLocator.boundingBox(); | ||||
|         const afterUpPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|         expect(afterUpPanBoundingBox.y).toBeGreaterThan(afterLeftPanBoundingBox.y); | ||||
|  | ||||
|         // pan down | ||||
| @@ -143,85 +154,71 @@ test.describe('Example Imagery', () => { | ||||
|         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 bgImageLocator.boundingBox(); | ||||
|         const afterDownPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|         expect(afterDownPanBoundingBox.y).toBeLessThan(afterUpPanBoundingBox.y); | ||||
|  | ||||
|     }); | ||||
|  | ||||
|     test('Can use + - buttons to zoom on the image', async ({ page }) => { | ||||
|         const bgImageLocator = page.locator(backgroundImageSelector); | ||||
|         await bgImageLocator.hover({trial: true}); | ||||
|         const zoomInBtn = page.locator('.t-btn-zoom-in').nth(0); | ||||
|         const zoomOutBtn = page.locator('.t-btn-zoom-out').nth(0); | ||||
|         const initialBoundingBox = await bgImageLocator.boundingBox(); | ||||
|         // Get initial image dimensions | ||||
|         const initialBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|  | ||||
|         await zoomInBtn.click(); | ||||
|         await zoomInBtn.click(); | ||||
|         // wait for zoom animation to finish | ||||
|         await bgImageLocator.hover({trial: true}); | ||||
|         const zoomedInBoundingBox = await bgImageLocator.boundingBox(); | ||||
|         // Zoom in twice via button | ||||
|         await zoomIntoImageryByButton(page); | ||||
|         await zoomIntoImageryByButton(page); | ||||
|  | ||||
|         // Get and assert zoomed in image dimensions | ||||
|         const zoomedInBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|         expect(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height); | ||||
|         expect(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width); | ||||
|  | ||||
|         await zoomOutBtn.click(); | ||||
|         // wait for zoom animation to finish | ||||
|         await bgImageLocator.hover({trial: true}); | ||||
|         const zoomedOutBoundingBox = await bgImageLocator.boundingBox(); | ||||
|         // Zoom out once via button | ||||
|         await zoomOutOfImageryByButton(page); | ||||
|  | ||||
|         // Get and assert zoomed out image dimensions | ||||
|         const zoomedOutBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|         expect(zoomedOutBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height); | ||||
|         expect(zoomedOutBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width); | ||||
|  | ||||
|         // Zoom out again via button, assert against the initial image dimensions | ||||
|         await zoomOutOfImageryByButton(page); | ||||
|         const finalBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|         expect(finalBoundingBox).toEqual(initialBoundingBox); | ||||
|     }); | ||||
|  | ||||
|     test('Can use the reset button to reset the image', async ({ page }) => { | ||||
|         const bgImageLocator = page.locator(backgroundImageSelector); | ||||
|         // wait for zoom animation to finish | ||||
|         await bgImageLocator.hover({trial: true}); | ||||
|     test('Can use the reset button to reset the image', async ({ page }, testInfo) => { | ||||
|         test.slow(testInfo.project === 'chrome-beta', "This test is slow in chrome-beta"); | ||||
|         // Get initial image dimensions | ||||
|         const initialBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|  | ||||
|         const zoomInBtn = page.locator('.t-btn-zoom-in').nth(0); | ||||
|         const zoomResetBtn = page.locator('.t-btn-zoom-reset').nth(0); | ||||
|         const initialBoundingBox = await bgImageLocator.boundingBox(); | ||||
|         // Zoom in twice via button | ||||
|         await zoomIntoImageryByButton(page); | ||||
|         await zoomIntoImageryByButton(page); | ||||
|  | ||||
|         await zoomInBtn.click(); | ||||
|         // wait for zoom animation to finish | ||||
|         await bgImageLocator.hover({trial: true}); | ||||
|         await zoomInBtn.click(); | ||||
|         // wait for zoom animation to finish | ||||
|         await bgImageLocator.hover({trial: true}); | ||||
|  | ||||
|         const zoomedInBoundingBox = await bgImageLocator.boundingBox(); | ||||
|         // Get and assert zoomed in image dimensions | ||||
|         const zoomedInBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|         expect.soft(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height); | ||||
|         expect.soft(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width); | ||||
|  | ||||
|         await zoomResetBtn.click(); | ||||
|         // wait for zoom animation to finish | ||||
|         await bgImageLocator.hover({trial: true}); | ||||
|  | ||||
|         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); | ||||
|         // Reset pan and zoom and assert against initial image dimensions | ||||
|         await resetImageryPanAndZoom(page); | ||||
|         const finalBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|         expect(finalBoundingBox).toEqual(initialBoundingBox); | ||||
|     }); | ||||
|  | ||||
|     test('Using the zoom features does not pause telemetry', async ({ page }) => { | ||||
|         const bgImageLocator = page.locator(backgroundImageSelector); | ||||
|         const pausePlayButton = page.locator('.c-button.pause-play'); | ||||
|         // wait for zoom animation to finish | ||||
|         await bgImageLocator.hover({trial: true}); | ||||
|  | ||||
|         // open the time conductor drop down | ||||
|         await page.locator('button:has-text("Fixed Timespan")').click(); | ||||
|  | ||||
|         // Click local clock | ||||
|         await page.locator('[data-testid="conductor-modeOption-realtime"]').click(); | ||||
|  | ||||
|         await expect.soft(pausePlayButton).not.toHaveClass(/is-paused/); | ||||
|         const zoomInBtn = page.locator('.t-btn-zoom-in').nth(0); | ||||
|         await zoomInBtn.click(); | ||||
|         // wait for zoom animation to finish | ||||
|         await bgImageLocator.hover({trial: true}); | ||||
|  | ||||
|         return expect(pausePlayButton).not.toHaveClass(/is-paused/); | ||||
|         // Zoom in via button | ||||
|         await zoomIntoImageryByButton(page); | ||||
|         await expect(pausePlayButton).not.toHaveClass(/is-paused/); | ||||
|     }); | ||||
|  | ||||
| }); | ||||
| @@ -232,8 +229,8 @@ test.describe('Example Imagery', () => { | ||||
| // ('Clicking on the left arrow should pause the imagery and go to previous image'); | ||||
| // ('If the imagery view is in pause mode, it should not be updated when new images come in'); | ||||
| // ('If the imagery view is not in pause mode, it should be updated when new images come in'); | ||||
| const backgroundImageSelector = '.c-imagery__main-image__background-image'; | ||||
| test('Example Imagery in Display layout', async ({ page }) => { | ||||
| test('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' | ||||
| @@ -249,10 +246,8 @@ test('Example Imagery in Display layout', async ({ page }) => { | ||||
|     await page.click('text=Example Imagery'); | ||||
|  | ||||
|     // Clear and set Image load delay to minimum value | ||||
|     // FIXME: Update the value to 5000 ms when this bug is fixed. | ||||
|     // See: https://github.com/nasa/openmct/issues/5265 | ||||
|     await page.locator('input[type="number"]').fill(''); | ||||
|     await page.locator('input[type="number"]').fill('0'); | ||||
|     await page.locator('input[type="number"]').fill('5000'); | ||||
|  | ||||
|     // Click text=OK | ||||
|     await Promise.all([ | ||||
| @@ -265,8 +260,7 @@ test('Example Imagery in Display layout', async ({ page }) => { | ||||
|     // Wait until Save Banner is gone | ||||
|     await page.waitForSelector('.c-message-banner__message', { state: 'detached'}); | ||||
|     await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery'); | ||||
|     const bgImageLocator = page.locator(backgroundImageSelector); | ||||
|     await bgImageLocator.hover({trial: true}); | ||||
|     await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|  | ||||
|     // Click previous image button | ||||
|     const previousImageButton = page.locator('.c-nav--prev'); | ||||
| @@ -278,15 +272,15 @@ test('Example Imagery in Display layout', async ({ page }) => { | ||||
|  | ||||
|     // Zoom in | ||||
|     const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|     await bgImageLocator.hover({trial: true}); | ||||
|     await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|     const deltaYStep = 100; // equivalent to 1x zoom | ||||
|     await page.mouse.wheel(0, deltaYStep * 2); | ||||
|     const zoomedBoundingBox = await bgImageLocator.boundingBox(); | ||||
|     const zoomedBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|     const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2; | ||||
|     const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2; | ||||
|  | ||||
|     // Wait for zoom animation to finish | ||||
|     await bgImageLocator.hover({trial: true}); | ||||
|     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); | ||||
| @@ -310,11 +304,11 @@ test('Example Imagery in Display layout', async ({ page }) => { | ||||
|     await page.locator('[data-testid=conductor-modeOption-realtime]').click(); | ||||
|  | ||||
|     // Zoom in on next image | ||||
|     await bgImageLocator.hover({trial: true}); | ||||
|     await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|     await page.mouse.wheel(0, deltaYStep * 2); | ||||
|  | ||||
|     // Wait for zoom animation to finish | ||||
|     await bgImageLocator.hover({trial: true}); | ||||
|     await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|     const imageNextMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox(); | ||||
|     expect(imageNextMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height); | ||||
|     expect(imageNextMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width); | ||||
| @@ -331,9 +325,9 @@ test('Example Imagery in Display layout', async ({ page }) => { | ||||
|  | ||||
|         return newImageCount; | ||||
|     }, { | ||||
|         message: "verify that new images still stream in", | ||||
|         message: "verify that old images are discarded", | ||||
|         timeout: 6 * 1000 | ||||
|     }).toBeGreaterThan(imageCount); | ||||
|     }).toBe(imageCount); | ||||
|  | ||||
|     // Verify selected image is still displayed | ||||
|     await expect(selectedImage).toBeVisible(); | ||||
| @@ -342,31 +336,17 @@ test('Example Imagery in Display layout', async ({ page }) => { | ||||
|     await page.locator('.pause-play').click(); | ||||
|  | ||||
|     //Get background-image url from background-image css prop | ||||
|     const backgroundImage = page.locator('.c-imagery__main-image__background-image'); | ||||
|     let backgroundImageUrl = await backgroundImage.evaluate((el) => { | ||||
|         return window.getComputedStyle(el).getPropertyValue('background-image').match(/url\(([^)]+)\)/)[1]; | ||||
|     }); | ||||
|     let backgroundImageUrl1 = backgroundImageUrl.slice(1, -1); //forgive me, padre | ||||
|     console.log('backgroundImageUrl1 ' + backgroundImageUrl1); | ||||
|     await assertBackgroundImageUrlFromBackgroundCss(page); | ||||
|  | ||||
|     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 | ||||
|     // Open the image filter menu | ||||
|     await page.locator('[role=toolbar] button[title="Brightness and contrast"]').click(); | ||||
|  | ||||
|         return backgroundImageUrl2; | ||||
|     }, { | ||||
|         message: "verify next image has updated", | ||||
|         timeout: 6 * 1000 | ||||
|     }).not.toBe(backgroundImageUrl1); | ||||
|     console.log('backgroundImageUrl2 ' + backgroundImageUrl2); | ||||
|     // 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' }); | ||||
|  | ||||
| @@ -454,13 +434,143 @@ test.describe('Example imagery thumbnails resize in display layouts', () => { | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| // 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.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('Example Imagery in Flexible layout', async ({ page, browserName }) => { | ||||
|         test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox'); | ||||
|         test.info().annotations.push({ | ||||
|             type: 'issue', | ||||
|             description: 'https://github.com/nasa/openmct/issues/5326' | ||||
|         }); | ||||
|  | ||||
|         // Go to baseURL | ||||
|         await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|         // Click the Create button | ||||
|         await page.click('button:has-text("Create")'); | ||||
|  | ||||
|         // Click text=Example Imagery | ||||
|         await page.click('text=Example Imagery'); | ||||
|  | ||||
|         // Clear and set Image load delay (milliseconds) | ||||
|         await page.click('input[type="number"]', {clickCount: 3}); | ||||
|         await page.type('input[type="number"]', "20"); | ||||
|  | ||||
|         // Click text=OK | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation({waitUntil: 'networkidle'}), | ||||
|             page.click('text=OK'), | ||||
|             //Wait for Save Banner to appear | ||||
|             page.waitForSelector('.c-message-banner__message') | ||||
|         ]); | ||||
|         // Wait until Save Banner is gone | ||||
|         await page.waitForSelector('.c-message-banner__message', { state: 'detached'}); | ||||
|         await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery'); | ||||
|         await page.locator(backgroundImageSelector).hover({trial: true}); | ||||
|  | ||||
|         // Click the Create button | ||||
|         await page.click('button:has-text("Create")'); | ||||
|  | ||||
|         // Click text=Flexible Layout | ||||
|         await page.click('text=Flexible Layout'); | ||||
|  | ||||
|         // Assert Flexable layout | ||||
|         await expect(page.locator('.js-form-title')).toHaveText('Create a New Flexible Layout'); | ||||
|  | ||||
|         await page.locator('form[name="mctForm"] >> text=My Items').click(); | ||||
|  | ||||
|         // Click My Items | ||||
|         await Promise.all([ | ||||
|             page.locator('text=OK').click(), | ||||
|             page.waitForNavigation({waitUntil: 'networkidle'}) | ||||
|         ]); | ||||
|  | ||||
|         // Click My Items | ||||
|         await page.locator('.c-disclosure-triangle').click(); | ||||
|  | ||||
|         // Right click example imagery | ||||
|         await page.click(('text=Unnamed Example Imagery'), { button: 'right' }); | ||||
|  | ||||
|         // Click move | ||||
|         await page.locator('.icon-move').click(); | ||||
|  | ||||
|         // Click triangle to open sub menu | ||||
|         await page.locator('.c-form__section .c-disclosure-triangle').click(); | ||||
|  | ||||
|         // Click Flexable Layout | ||||
|         await page.click('.c-overlay__outer >> text=Unnamed Flexible Layout'); | ||||
|  | ||||
|         // Click text=OK | ||||
|         await page.locator('text=OK').click(); | ||||
|  | ||||
|         // Save template | ||||
|         await saveTemplate(page); | ||||
|  | ||||
|         // Zoom in | ||||
|         await mouseZoomIn(page); | ||||
|  | ||||
|         // Center the mouse pointer | ||||
|         const zoomedBoundingBox = await await page.locator(backgroundImageSelector).boundingBox(); | ||||
|         const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2; | ||||
|         const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2; | ||||
|         await page.mouse.move(imageCenterX, imageCenterY); | ||||
|  | ||||
|         // Pan zoom | ||||
|         await panZoomAndAssertImageProperties(page); | ||||
|  | ||||
|         // Click previous image button | ||||
|         const previousImageButton = page.locator('.c-nav--prev'); | ||||
|         await previousImageButton.click(); | ||||
|  | ||||
|         // Verify previous image | ||||
|         const selectedImage = page.locator('.selected'); | ||||
|         await expect(selectedImage).toBeVisible(); | ||||
|  | ||||
|         // Click time conductor mode button | ||||
|         await page.locator('.c-mode-button').click(); | ||||
|  | ||||
|         // Select local clock mode | ||||
|         await page.locator('[data-testid=conductor-modeOption-realtime]').click(); | ||||
|  | ||||
|         // Zoom in on next image | ||||
|         await mouseZoomIn(page); | ||||
|  | ||||
|         // Click previous image button | ||||
|         await previousImageButton.click(); | ||||
|  | ||||
|         // Verify previous image | ||||
|         await expect(selectedImage).toBeVisible(); | ||||
|  | ||||
|         const imageCount = await page.locator('.c-imagery__thumb').count(); | ||||
|         await expect.poll(async () => { | ||||
|             const newImageCount = await page.locator('.c-imagery__thumb').count(); | ||||
|  | ||||
|             return newImageCount; | ||||
|         }, { | ||||
|             message: "verify that old images are discarded", | ||||
|             timeout: 6 * 1000 | ||||
|         }).toBe(imageCount); | ||||
|  | ||||
|         // Verify selected image is still displayed | ||||
|         await expect(selectedImage).toBeVisible(); | ||||
|  | ||||
|         // Unpause imagery | ||||
|         await page.locator('.pause-play').click(); | ||||
|  | ||||
|         //Get background-image url from background-image css prop | ||||
|         await assertBackgroundImageUrlFromBackgroundCss(page); | ||||
|  | ||||
|         // Open the image filter menu | ||||
|         await page.locator('[role=toolbar] button[title="Brightness and contrast"]').click(); | ||||
|  | ||||
|         // Drag the brightness and contrast sliders around and assert filter values | ||||
|         await dragBrightnessSliderAndAssertFilterValues(page); | ||||
|         await dragContrastSliderAndAssertFilterValues(page); | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| test.describe('Example Imagery in Tabs view', () => { | ||||
| @@ -472,3 +582,236 @@ 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); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Use the '+' button to zoom in. Hovers first if the toolbar is not visible | ||||
|  * and waits for the zoom animation to finish afterwards. | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function zoomIntoImageryByButton(page) { | ||||
|     // FIXME: There should only be one set of imagery buttons, but there are two? | ||||
|     const zoomInBtn = page.locator("[role='toolbar'][aria-label='Image controls'] .t-btn-zoom-in").nth(0); | ||||
|     const backgroundImage = page.locator(backgroundImageSelector); | ||||
|     if (!(await zoomInBtn.isVisible())) { | ||||
|         await backgroundImage.hover({trial: true}); | ||||
|     } | ||||
|  | ||||
|     await zoomInBtn.click(); | ||||
|     await waitForAnimations(backgroundImage); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Use the '-' button to zoom out. Hovers first if the toolbar is not visible | ||||
|  * and waits for the zoom animation to finish afterwards. | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function zoomOutOfImageryByButton(page) { | ||||
|     // FIXME: There should only be one set of imagery buttons, but there are two? | ||||
|     const zoomOutBtn = page.locator("[role='toolbar'][aria-label='Image controls'] .t-btn-zoom-out").nth(0); | ||||
|     const backgroundImage = page.locator(backgroundImageSelector); | ||||
|     if (!(await zoomOutBtn.isVisible())) { | ||||
|         await backgroundImage.hover({trial: true}); | ||||
|     } | ||||
|  | ||||
|     await zoomOutBtn.click(); | ||||
|     await waitForAnimations(backgroundImage); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Use the reset button to reset image pan and zoom. Hovers first if the toolbar is not visible | ||||
|  * and waits for the zoom animation to finish afterwards. | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function resetImageryPanAndZoom(page) { | ||||
|     // FIXME: There should only be one set of imagery buttons, but there are two? | ||||
|     const panZoomResetBtn = page.locator("[role='toolbar'][aria-label='Image controls'] .t-btn-zoom-reset").nth(0); | ||||
|     const backgroundImage = page.locator(backgroundImageSelector); | ||||
|     if (!(await panZoomResetBtn.isVisible())) { | ||||
|         await backgroundImage.hover({trial: true}); | ||||
|     } | ||||
|  | ||||
|     await panZoomResetBtn.click(); | ||||
|     await waitForAnimations(backgroundImage); | ||||
| } | ||||
|   | ||||
| @@ -27,14 +27,155 @@ const path = require('path'); | ||||
| const TEST_TEXT = 'Testing text for entries.'; | ||||
| const TEST_TEXT_NAME = 'Test Page'; | ||||
| const CUSTOM_NAME = 'CUSTOM_NAME'; | ||||
| const COMMIT_BUTTON_TEXT = 'button:has-text("Commit Entries")'; | ||||
| const SINE_WAVE_GENERATOR = 'text=Unnamed Sine Wave Generator'; | ||||
| const NOTEBOOK_DROP_AREA = '.c-notebook__drag-area'; | ||||
|  | ||||
| test.describe('Restricted Notebook', () => { | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         await startAndAddRestrictedNotebookObject(page); | ||||
|     }); | ||||
|  | ||||
|     test('Can be renamed @addInit', async ({ page }) => { | ||||
|         await expect(page.locator('.l-browse-bar__object-name')).toContainText(`Unnamed ${CUSTOM_NAME}`); | ||||
|     }); | ||||
|  | ||||
|     test('Can be deleted if there are no locked pages @addInit', async ({ page }) => { | ||||
|         await openContextMenuRestrictedNotebook(page); | ||||
|  | ||||
|         const menuOptions = page.locator('.c-menu ul'); | ||||
|         await expect.soft(menuOptions).toContainText('Remove'); | ||||
|  | ||||
|         const restrictedNotebookTreeObject = page.locator(`a:has-text("Unnamed ${CUSTOM_NAME}")`); | ||||
|  | ||||
|         // notbook tree object exists | ||||
|         expect.soft(await restrictedNotebookTreeObject.count()).toEqual(1); | ||||
|  | ||||
|         // Click Remove Text | ||||
|         await page.locator('text=Remove').click(); | ||||
|  | ||||
|         // Click 'OK' on confirmation window and wait for save banner to appear | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('text=OK').click(), | ||||
|             page.waitForSelector('.c-message-banner__message') | ||||
|         ]); | ||||
|  | ||||
|         // has been deleted | ||||
|         expect(await restrictedNotebookTreeObject.count()).toEqual(0); | ||||
|     }); | ||||
|  | ||||
|     test('Can be locked if at least one page has one entry @addInit', async ({ page }) => { | ||||
|  | ||||
|         await enterTextEntry(page); | ||||
|  | ||||
|         const commitButton = page.locator('button:has-text("Commit Entries")'); | ||||
|         expect(await commitButton.count()).toEqual(1); | ||||
|     }); | ||||
|  | ||||
| }); | ||||
|  | ||||
| test.describe('Restricted Notebook with at least one entry and with the page locked @addInit', () => { | ||||
|  | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         await startAndAddRestrictedNotebookObject(page); | ||||
|         await enterTextEntry(page); | ||||
|         await lockPage(page); | ||||
|  | ||||
|         // FIXME: Give ample time for the mutation to happen | ||||
|         // https://github.com/nasa/openmct/issues/5409 | ||||
|         // eslint-disable-next-line playwright/no-wait-for-timeout | ||||
|         await page.waitForTimeout(1 * 1000); | ||||
|  | ||||
|         // open sidebar | ||||
|         await page.locator('button.c-notebook__toggle-nav-button').click(); | ||||
|     }); | ||||
|  | ||||
|     test('Locked page should now be in a locked state @addInit', async ({ page }, testInfo) => { | ||||
|         test.fixme(testInfo.project === 'chrome-beta', "Test is unreliable on chrome-beta"); | ||||
|         // main lock message on page | ||||
|         const lockMessage = page.locator('text=This page has been committed and cannot be modified or removed'); | ||||
|         expect.soft(await lockMessage.count()).toEqual(1); | ||||
|  | ||||
|         // lock icon on page in sidebar | ||||
|         const pageLockIcon = page.locator('ul.c-notebook__pages li div.icon-lock'); | ||||
|         expect.soft(await pageLockIcon.count()).toEqual(1); | ||||
|  | ||||
|         // no way to remove a restricted notebook with a locked page | ||||
|         await openContextMenuRestrictedNotebook(page); | ||||
|         const menuOptions = page.locator('.c-menu ul'); | ||||
|  | ||||
|         await expect(menuOptions).not.toContainText('Remove'); | ||||
|     }); | ||||
|  | ||||
|     test('Can still: add page, rename, add entry, delete unlocked pages @addInit', async ({ page }) => { | ||||
|         // Click text=Page Add >> button | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('text=Page Add >> button').click() | ||||
|         ]); | ||||
|         // Click text=Unnamed Page >> nth=1 | ||||
|         await page.locator('text=Unnamed Page').nth(1).click(); | ||||
|         // Press a with modifiers | ||||
|         await page.locator('text=Unnamed Page').nth(1).fill(TEST_TEXT_NAME); | ||||
|  | ||||
|         // expect to be able to rename unlocked pages | ||||
|         const newPageElement = page.locator(`text=${TEST_TEXT_NAME}`); | ||||
|         const newPageCount = await newPageElement.count(); | ||||
|         await newPageElement.press('Enter'); // exit contenteditable state | ||||
|         expect.soft(newPageCount).toEqual(1); | ||||
|  | ||||
|         // enter test text | ||||
|         await enterTextEntry(page); | ||||
|  | ||||
|         // expect new page to be lockable | ||||
|         const commitButton = page.locator('BUTTON:HAS-TEXT("COMMIT ENTRIES")'); | ||||
|         expect.soft(await commitButton.count()).toEqual(1); | ||||
|  | ||||
|         // Click text=Unnamed PageTest Page >> button | ||||
|         await page.locator('text=Unnamed PageTest Page >> button').click(); | ||||
|         // Click text=Delete Page | ||||
|         await page.locator('text=Delete Page').click(); | ||||
|         // Click text=Ok | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('text=Ok').click() | ||||
|         ]); | ||||
|  | ||||
|         // deleted page, should no longer exist | ||||
|         const deletedPageElement = page.locator(`text=${TEST_TEXT_NAME}`); | ||||
|         expect(await deletedPageElement.count()).toEqual(0); | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| test.describe('Restricted Notebook with a page locked and with an embed @addInit', () => { | ||||
|  | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         await startAndAddRestrictedNotebookObject(page); | ||||
|         await dragAndDropEmbed(page); | ||||
|     }); | ||||
|  | ||||
|     test('Allows embeds to be deleted if page unlocked @addInit', async ({ page }) => { | ||||
|         // Click .c-ne__embed__name .c-popup-menu-button | ||||
|         await page.locator('.c-ne__embed__name .c-popup-menu-button').click(); // embed popup menu | ||||
|  | ||||
|         const embedMenu = page.locator('body >> .c-menu'); | ||||
|         await expect(embedMenu).toContainText('Remove This Embed'); | ||||
|     }); | ||||
|  | ||||
|     test('Disallows embeds to be deleted if page locked @addInit', async ({ page }) => { | ||||
|         await lockPage(page); | ||||
|         // Click .c-ne__embed__name .c-popup-menu-button | ||||
|         await page.locator('.c-ne__embed__name .c-popup-menu-button').click(); // embed popup menu | ||||
|  | ||||
|         const embedMenu = page.locator('body >> .c-menu'); | ||||
|         await expect(embedMenu).not.toContainText('Remove This Embed'); | ||||
|     }); | ||||
|  | ||||
| }); | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function startAndAddNotebookObject(page) { | ||||
| async function startAndAddRestrictedNotebookObject(page) { | ||||
|     // eslint-disable-next-line no-undef | ||||
|     await page.addInitScript({ path: path.join(__dirname, 'addInitRestrictedNotebook.js') }); | ||||
|     //Go to baseURL | ||||
| @@ -48,8 +189,6 @@ async function startAndAddNotebookObject(page) { | ||||
|         page.waitForNavigation({waitUntil: 'networkidle'}), | ||||
|         page.click('text=OK') | ||||
|     ]); | ||||
|  | ||||
|     return; | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -63,8 +202,6 @@ async function enterTextEntry(page) { | ||||
|     await page.locator('div.c-ne__text').click(); | ||||
|     await page.locator('div.c-ne__text').fill(TEST_TEXT); | ||||
|     await page.locator('div.c-ne__text').press('Enter'); | ||||
|  | ||||
|     return; | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -87,178 +224,32 @@ async function dragAndDropEmbed(page) { | ||||
|         page.locator('text=Unnamed CUSTOM_NAME').click() | ||||
|     ]); | ||||
|  | ||||
|     await page.dragAndDrop(SINE_WAVE_GENERATOR, NOTEBOOK_DROP_AREA); | ||||
|  | ||||
|     return; | ||||
|     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(COMMIT_BUTTON_TEXT); | ||||
|     const commitButton = page.locator('button:has-text("Commit Entries")'); | ||||
|     await commitButton.click(); | ||||
|  | ||||
|     // confirmation dialog click | ||||
|     //Wait until Lock Banner is visible | ||||
|     await page.locator('text=Lock Page').click(); | ||||
|  | ||||
|     // waiting for mutation of locked page | ||||
|     await new Promise((resolve, reject) => { | ||||
|         setTimeout(resolve, 1000); | ||||
|     }); | ||||
|  | ||||
|     return; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function openContextMenuRestrictedNotebook(page) { | ||||
|     // Click text=Open MCT My Items (This expands the My Items folder to show it's chilren in the tree) | ||||
|     await page.locator('text=Open MCT My Items >> span').nth(3).click(); | ||||
|     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' | ||||
|     }); | ||||
|  | ||||
|     return; | ||||
| } | ||||
|  | ||||
| test.describe('Restricted Notebook', () => { | ||||
|  | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         await startAndAddNotebookObject(page); | ||||
|     }); | ||||
|  | ||||
|     test('Can be renamed', async ({ page }) => { | ||||
|         await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText(`Unnamed ${CUSTOM_NAME}`); | ||||
|     }); | ||||
|  | ||||
|     test('Can be deleted if there are no locked pages', async ({ page }) => { | ||||
|         await openContextMenuRestrictedNotebook(page); | ||||
|  | ||||
|         const menuOptions = page.locator('.c-menu ul'); | ||||
|         await expect.soft(menuOptions).toContainText('Remove'); | ||||
|  | ||||
|         const restrictedNotebookTreeObject = page.locator(`a:has-text("Unnamed ${CUSTOM_NAME}")`); | ||||
|  | ||||
|         // notbook tree object exists | ||||
|         expect.soft(await restrictedNotebookTreeObject.count()).toEqual(1); | ||||
|  | ||||
|         // Click text=Remove | ||||
|         await page.locator('text=Remove').click(); | ||||
|         // Click text=OK | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine?tc.mode=fixed&tc.startBound=1653671067340&tc.endBound=1653672867340&tc.timeSystem=utc&view=grid' }*/), | ||||
|             page.locator('text=OK').click() | ||||
|         ]); | ||||
|  | ||||
|         // has been deleted | ||||
|         expect.soft(await restrictedNotebookTreeObject.count()).toEqual(0); | ||||
|     }); | ||||
|  | ||||
|     test('Can be locked if at least one page has one entry', async ({ page }) => { | ||||
|  | ||||
|         await enterTextEntry(page); | ||||
|  | ||||
|         const commitButton = page.locator(COMMIT_BUTTON_TEXT); | ||||
|         expect.soft(await commitButton.count()).toEqual(1); | ||||
|     }); | ||||
|  | ||||
| }); | ||||
|  | ||||
| test.describe('Restricted Notebook with at least one entry and with the page locked', () => { | ||||
|  | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         await startAndAddNotebookObject(page); | ||||
|         await enterTextEntry(page); | ||||
|         await lockPage(page); | ||||
|  | ||||
|         // open sidebar | ||||
|         await page.locator('button.c-notebook__toggle-nav-button').click(); | ||||
|     }); | ||||
|  | ||||
|     test('Locked page should now be in a locked state', async ({ page }) => { | ||||
|         // main lock message on page | ||||
|         const lockMessage = page.locator('text=This page has been committed and cannot be modified or removed'); | ||||
|         expect.soft(await lockMessage.count()).toEqual(1); | ||||
|  | ||||
|         // lock icon on page in sidebar | ||||
|         const pageLockIcon = page.locator('ul.c-notebook__pages li div.icon-lock'); | ||||
|         expect.soft(await pageLockIcon.count()).toEqual(1); | ||||
|  | ||||
|         // no way to remove a restricted notebook with a locked page | ||||
|         await openContextMenuRestrictedNotebook(page); | ||||
|  | ||||
|         const menuOptions = page.locator('.c-menu ul'); | ||||
|  | ||||
|         await expect.soft(menuOptions).not.toContainText('Remove'); | ||||
|  | ||||
|     }); | ||||
|  | ||||
|     test('Can still: add page, rename, add entry, delete unlocked pages', async ({ page }) => { | ||||
|         // Click text=Page Add >> button | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('text=Page Add >> button').click() | ||||
|         ]); | ||||
|         // Click text=Unnamed Page >> nth=1 | ||||
|         await page.locator('text=Unnamed Page').nth(1).click(); | ||||
|         // Press a with modifiers | ||||
|         await page.locator('text=Unnamed Page').nth(1).fill(TEST_TEXT_NAME); | ||||
|  | ||||
|         // expect to be able to rename unlocked pages | ||||
|         const newPageElement = page.locator(`text=${TEST_TEXT_NAME}`); | ||||
|         const newPageCount = await newPageElement.count(); | ||||
|         await newPageElement.press('Enter'); // exit contenteditable state | ||||
|         expect.soft(newPageCount).toEqual(1); | ||||
|  | ||||
|         // enter test text | ||||
|         await enterTextEntry(page); | ||||
|  | ||||
|         // expect new page to be lockable | ||||
|         const commitButton = page.locator(COMMIT_BUTTON_TEXT); | ||||
|         expect.soft(await commitButton.count()).toEqual(1); | ||||
|  | ||||
|         // Click text=Unnamed PageTest Page >> button | ||||
|         await page.locator('text=Unnamed PageTest Page >> button').click(); | ||||
|         // Click text=Delete Page | ||||
|         await page.locator('text=Delete Page').click(); | ||||
|         // Click text=Ok | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('text=Ok').click() | ||||
|         ]); | ||||
|  | ||||
|         // deleted page, should no longer exist | ||||
|         const deletedPageElement = page.locator(`text=${TEST_TEXT_NAME}`); | ||||
|         expect.soft(await deletedPageElement.count()).toEqual(0); | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| test.describe('Restricted Notebook with a page locked and with an embed', () => { | ||||
|  | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         await startAndAddNotebookObject(page); | ||||
|         await dragAndDropEmbed(page); | ||||
|     }); | ||||
|  | ||||
|     test('Allows embeds to be deleted if page unlocked', async ({ page }) => { | ||||
|         // Click .c-ne__embed__name .c-popup-menu-button | ||||
|         await page.locator('.c-ne__embed__name .c-popup-menu-button').click(); // embed popup menu | ||||
|  | ||||
|         const embedMenu = page.locator('body >> .c-menu'); | ||||
|         await expect.soft(embedMenu).toContainText('Remove This Embed'); | ||||
|     }); | ||||
|  | ||||
|     test('Disallows embeds to be deleted if page locked', async ({ page }) => { | ||||
|         await lockPage(page); | ||||
|         // Click .c-ne__embed__name .c-popup-menu-button | ||||
|         await page.locator('.c-ne__embed__name .c-popup-menu-button').click(); // embed popup menu | ||||
|  | ||||
|         const embedMenu = page.locator('body >> .c-menu'); | ||||
|         await expect.soft(embedMenu).not.toContainText('Remove This Embed'); | ||||
|     }); | ||||
|  | ||||
| }); | ||||
|   | ||||
							
								
								
									
										205
									
								
								e2e/tests/plugins/notebook/tags.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										205
									
								
								e2e/tests/plugins/notebook/tags.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,205 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* | ||||
| This test suite is dedicated to tests which verify form functionality. | ||||
| */ | ||||
|  | ||||
| const { expect } = require('@playwright/test'); | ||||
| const { test } = require('../../../fixtures'); | ||||
|  | ||||
| /** | ||||
|   * Creates a notebook object and adds an entry. | ||||
|   * @param {import('@playwright/test').Page} - page to load | ||||
|   * @param {number} [iterations = 1] - the number of entries to create | ||||
|   */ | ||||
| async function createNotebookAndEntry(page, iterations = 1) { | ||||
|     //Go to baseURL | ||||
|     await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|     // Click button:has-text("Create") | ||||
|     await page.locator('button:has-text("Create")').click(); | ||||
|     await page.locator('[title="Create and save timestamped notes with embedded object snapshots."]').click(); | ||||
|     // Click button:has-text("OK") | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('[name="mctForm"] >> text=My Items').click(), | ||||
|         page.locator('button:has-text("OK")').click() | ||||
|     ]); | ||||
|  | ||||
|     for (let iteration = 0; iteration < iterations; iteration++) { | ||||
|         // Click text=To start a new entry, click here or drag and drop any object | ||||
|         await page.locator('text=To start a new entry, click here or drag and drop any object').click(); | ||||
|         const entryLocator = `[aria-label="Notebook Entry Input"] >> nth = ${iteration}`; | ||||
|         await page.locator(entryLocator).click(); | ||||
|         await page.locator(entryLocator).fill(`Entry ${iteration}`); | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| /** | ||||
|   * Creates a notebook object, adds an entry, and adds a tag. | ||||
|   * @param {import('@playwright/test').Page} page | ||||
|   * @param {number} [iterations = 1] - the number of entries (and tags) to create | ||||
|   */ | ||||
| async function createNotebookEntryAndTags(page, iterations = 1) { | ||||
|     await createNotebookAndEntry(page, iterations); | ||||
|  | ||||
|     for (let iteration = 0; iteration < iterations; iteration++) { | ||||
|         // Click text=To start a new entry, click here or drag and drop any object | ||||
|         await page.locator(`button:has-text("Add Tag") >> nth = ${iteration}`).click(); | ||||
|  | ||||
|         // Click [placeholder="Type to select tag"] | ||||
|         await page.locator('[placeholder="Type to select tag"]').click(); | ||||
|         // Click text=Driving | ||||
|         await page.locator('[aria-label="Autocomplete Options"] >> text=Driving').click(); | ||||
|  | ||||
|         // Click button:has-text("Add Tag") | ||||
|         await page.locator(`button:has-text("Add Tag") >> nth = ${iteration}`).click(); | ||||
|         // Click [placeholder="Type to select tag"] | ||||
|         await page.locator('[placeholder="Type to select tag"]').click(); | ||||
|         // Click text=Science | ||||
|         await page.locator('[aria-label="Autocomplete Options"] >> text=Science').click(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| test.describe('Tagging in Notebooks', () => { | ||||
|     test('Can load tags', async ({ page }) => { | ||||
|         await createNotebookAndEntry(page); | ||||
|         // Click text=To start a new entry, click here or drag and drop any object | ||||
|         await page.locator('button:has-text("Add Tag")').click(); | ||||
|  | ||||
|         // Click [placeholder="Type to select tag"] | ||||
|         await page.locator('[placeholder="Type to select tag"]').click(); | ||||
|  | ||||
|         await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Science"); | ||||
|         await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Drilling"); | ||||
|         await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Driving"); | ||||
|     }); | ||||
|     test('Can add tags', async ({ page }) => { | ||||
|         await createNotebookEntryAndTags(page); | ||||
|  | ||||
|         await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Science"); | ||||
|         await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Driving"); | ||||
|  | ||||
|         // Click button:has-text("Add Tag") | ||||
|         await page.locator('button:has-text("Add Tag")').click(); | ||||
|         // Click [placeholder="Type to select tag"] | ||||
|         await page.locator('[placeholder="Type to select tag"]').click(); | ||||
|  | ||||
|         await expect(page.locator('[aria-label="Autocomplete Options"]')).not.toContainText("Science"); | ||||
|         await expect(page.locator('[aria-label="Autocomplete Options"]')).not.toContainText("Driving"); | ||||
|         await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Drilling"); | ||||
|     }); | ||||
|     test('Can search for tags', async ({ page }) => { | ||||
|         await createNotebookEntryAndTags(page); | ||||
|         // Click [aria-label="OpenMCT Search"] input[type="search"] | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); | ||||
|         // Fill [aria-label="OpenMCT Search"] input[type="search"] | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc'); | ||||
|         await expect(page.locator('[aria-label="Search Result"]')).toContainText("Science"); | ||||
|         await expect(page.locator('[aria-label="Search Result"]')).toContainText("Driving"); | ||||
|  | ||||
|         // Click [aria-label="OpenMCT Search"] input[type="search"] | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); | ||||
|         // Fill [aria-label="OpenMCT Search"] input[type="search"] | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Sc'); | ||||
|         await expect(page.locator('[aria-label="Search Result"]')).toContainText("Science"); | ||||
|         await expect(page.locator('[aria-label="Search Result"]')).toContainText("Driving"); | ||||
|  | ||||
|         // Click [aria-label="OpenMCT Search"] input[type="search"] | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); | ||||
|         // Fill [aria-label="OpenMCT Search"] input[type="search"] | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Xq'); | ||||
|         await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible(); | ||||
|         await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible(); | ||||
|     }); | ||||
|  | ||||
|     test('Can delete tags', async ({ page }) => { | ||||
|         await createNotebookEntryAndTags(page); | ||||
|         await page.locator('[aria-label="Notebook Entries"]').click(); | ||||
|         // Delete Driving | ||||
|         await page.locator('text=Science Driving Add Tag >> button').nth(1).click(); | ||||
|  | ||||
|         await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Science"); | ||||
|         await expect(page.locator('[aria-label="Notebook Entry"]')).not.toContainText("Driving"); | ||||
|  | ||||
|         // Fill [aria-label="OpenMCT Search"] input[type="search"] | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc'); | ||||
|         await expect(page.locator('[aria-label="Search Result"]')).not.toContainText("Driving"); | ||||
|     }); | ||||
|     test('Tags persist across reload', async ({ page }) => { | ||||
|         //Go to baseURL | ||||
|         await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|         // Create a clock object we can navigate to | ||||
|         await page.click('button:has-text("Create")'); | ||||
|  | ||||
|         // Click Clock | ||||
|         await page.click('text=Clock'); | ||||
|         // Click button:has-text("OK") | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('[name="mctForm"] >> text=My Items').click(), | ||||
|             page.locator('button:has-text("OK")').click() | ||||
|         ]); | ||||
|  | ||||
|         await page.click('.c-disclosure-triangle'); | ||||
|  | ||||
|         const ITERATIONS = 4; | ||||
|         await createNotebookEntryAndTags(page, ITERATIONS); | ||||
|  | ||||
|         for (let iteration = 0; iteration < ITERATIONS; iteration++) { | ||||
|             const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`; | ||||
|             await expect(page.locator(entryLocator)).toContainText("Science"); | ||||
|             await expect(page.locator(entryLocator)).toContainText("Driving"); | ||||
|         } | ||||
|  | ||||
|         // Click Unnamed Clock | ||||
|         await page.click('text="Unnamed Clock"'); | ||||
|  | ||||
|         // Click Unnamed Notebook | ||||
|         await page.click('text="Unnamed Notebook"'); | ||||
|  | ||||
|         for (let iteration = 0; iteration < ITERATIONS; iteration++) { | ||||
|             const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`; | ||||
|             await expect(page.locator(entryLocator)).toContainText("Science"); | ||||
|             await expect(page.locator(entryLocator)).toContainText("Driving"); | ||||
|         } | ||||
|  | ||||
|         //Reload Page | ||||
|         await Promise.all([ | ||||
|             page.reload(), | ||||
|             page.waitForLoadState('networkidle') | ||||
|         ]); | ||||
|  | ||||
|         // Click Unnamed Notebook | ||||
|         await page.click('text="Unnamed Notebook"'); | ||||
|  | ||||
|         for (let iteration = 0; iteration < ITERATIONS; iteration++) { | ||||
|             const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`; | ||||
|             await expect(page.locator(entryLocator)).toContainText("Science"); | ||||
|             await expect(page.locator(entryLocator)).toContainText("Driving"); | ||||
|         } | ||||
|  | ||||
|     }); | ||||
| }); | ||||
| @@ -24,22 +24,9 @@ | ||||
| Testsuite for plot autoscale. | ||||
| */ | ||||
|  | ||||
| const { test: _test } = require('../../../fixtures.js'); | ||||
| const { 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, | ||||
| @@ -50,7 +37,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. | ||||
|         await test.setTimeout(120 * 1000); | ||||
|         test.slow(); | ||||
|  | ||||
|         await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
| @@ -62,16 +49,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); | ||||
|  | ||||
|         // Make sure that after turning off autoscale, the user selected range values start at the same values the plot had prior. | ||||
|         await Promise.all([ | ||||
|             testYTicks(page, ['-1.00', '-0.50', '0.00', '0.50', '1.00']), | ||||
|             new Promise(r => setTimeout(r, 100)) | ||||
|                 .then(() => canvas.screenshot()) | ||||
|                 .then(shot => expect(shot).toMatchSnapshot('autoscale-canvas-prepan.png', { maxDiffPixels: 40 })) | ||||
|         ]); | ||||
|         await canvas.hover({trial: true}); | ||||
|  | ||||
|         expect(await canvas.screenshot()).toMatchSnapshot('autoscale-canvas-prepan'); | ||||
|  | ||||
|         //Alt Drag Start | ||||
|         await page.keyboard.down('Alt'); | ||||
|  | ||||
|         await canvas.dragTo(canvas, { | ||||
| @@ -85,15 +72,15 @@ test.describe('ExportAsJSON', () => { | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         //Alt Drag End | ||||
|         await page.keyboard.up('Alt'); | ||||
|  | ||||
|         // Ensure the drag worked. | ||||
|         await Promise.all([ | ||||
|             testYTicks(page, ['0.00', '0.50', '1.00', '1.50', '2.00']), | ||||
|             new Promise(r => setTimeout(r, 100)) | ||||
|                 .then(() => canvas.screenshot()) | ||||
|                 .then(shot => expect(shot).toMatchSnapshot('autoscale-canvas-panned.png', { maxDiffPixels: 40 })) | ||||
|         ]); | ||||
|         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'); | ||||
|     }); | ||||
| }); | ||||
|  | ||||
|   | ||||
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 18 KiB | 
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 18 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 18 KiB | 
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 21 KiB | 
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 21 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 19 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 }) => { | ||||
|         //This is necessary due to the size of the test suite. | ||||
|         await test.setTimeout(120 * 1000); | ||||
|         //Test.slow decorator is currently broken. Needs to be fixed in https://github.com/nasa/openmct/issues/5374 | ||||
|         test.slow(); | ||||
|  | ||||
|         await makeOverlayPlot(page); | ||||
|         await testRegularTicks(page); | ||||
| @@ -44,20 +44,6 @@ test.describe('Log plot tests', () => { | ||||
|         await testLogTicks(page); | ||||
|         await saveOverlayPlot(page); | ||||
|         await testLogTicks(page); | ||||
|         //await testLogPlotPixels(page); | ||||
|  | ||||
|         // FIXME: Get rid of the waitForTimeout() and lint warning exception. | ||||
|         // eslint-disable-next-line playwright/no-wait-for-timeout | ||||
|         await page.waitForTimeout(1 * 1000); | ||||
|  | ||||
|         // refresh page and wait for charts and ticks to load | ||||
|         await page.reload({ waitUntil: 'networkidle'}); | ||||
|         await page.waitForSelector('.gl-plot-chart-area'); | ||||
|         await page.waitForSelector('.gl-plot-y-tick-label'); | ||||
|  | ||||
|         // test log ticks hold up after refresh | ||||
|         await testLogTicks(page); | ||||
|         //await testLogPlotPixels(page); | ||||
|     }); | ||||
|  | ||||
|     // Leaving test as 'TODO' for now. | ||||
| @@ -121,14 +107,14 @@ async function makeOverlayPlot(page) { | ||||
|  | ||||
|     // set amplitude to 6, offset 4, period 2 | ||||
|  | ||||
|     await page.locator('div:nth-child(5) .form-row .c-form-row__controls .form-control .field input').click(); | ||||
|     await page.locator('div:nth-child(5) .form-row .c-form-row__controls .form-control .field input').fill('6'); | ||||
|     await page.locator('div:nth-child(5) .c-form-row__controls .form-control .field input').click(); | ||||
|     await page.locator('div:nth-child(5) .c-form-row__controls .form-control .field input').fill('6'); | ||||
|  | ||||
|     await page.locator('div:nth-child(6) .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(6) .c-form-row__controls .form-control .field input').click(); | ||||
|     await page.locator('div:nth-child(6) .c-form-row__controls .form-control .field input').fill('4'); | ||||
|  | ||||
|     await page.locator('div:nth-child(7) .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'); | ||||
|     await page.locator('div:nth-child(7) .c-form-row__controls .form-control .field input').click(); | ||||
|     await page.locator('div:nth-child(7) .c-form-row__controls .form-control .field input').fill('2'); | ||||
|  | ||||
|     // Click OK to make generator | ||||
|  | ||||
|   | ||||
							
								
								
									
										155
									
								
								e2e/tests/plugins/plot/missingPlotObj.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								e2e/tests/plugins/plot/missingPlotObj.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,155 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* | ||||
| Tests to verify log plot functionality when objects are missing | ||||
| */ | ||||
|  | ||||
| const { test } = require('../../../fixtures.js'); | ||||
| const { expect } = require('@playwright/test'); | ||||
|  | ||||
| test.describe('Handle missing object for plots', () => { | ||||
|     test('Displays empty div for missing stacked plot item', async ({ page, browserName }) => { | ||||
|         test.fixme(browserName === 'firefox', 'Firefox failing due to console events being missed'); | ||||
|         const errorLogs = []; | ||||
|  | ||||
|         page.on("console", (message) => { | ||||
|             if (message.type() === 'warning' && message.text().includes('Missing domain object')) { | ||||
|                 errorLogs.push(message.text()); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         //Make stacked plot | ||||
|         await makeStackedPlot(page); | ||||
|  | ||||
|         //Gets local storage and deletes the last sine wave generator in the stacked plot | ||||
|         const localStorage = await page.evaluate(() => window.localStorage); | ||||
|         const parsedData = JSON.parse(localStorage.mct); | ||||
|         const keys = Object.keys(parsedData); | ||||
|         const lastKey = keys[keys.length - 1]; | ||||
|  | ||||
|         delete parsedData[lastKey]; | ||||
|  | ||||
|         //Sets local storage with missing object | ||||
|         await page.evaluate( | ||||
|             `window.localStorage.setItem('mct', '${JSON.stringify(parsedData)}')` | ||||
|         ); | ||||
|  | ||||
|         //Reloads page and clicks on stacked plot | ||||
|         await Promise.all([ | ||||
|             page.reload(), | ||||
|             page.waitForLoadState('networkidle') | ||||
|         ]); | ||||
|  | ||||
|         //Verify Main section is there on load | ||||
|         await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Stacked Plot'); | ||||
|  | ||||
|         await page.locator('text=Open MCT My Items >> span').nth(3).click(); | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('text=Unnamed Stacked Plot').first().click() | ||||
|         ]); | ||||
|  | ||||
|         //Check that there is only one stacked item plot with a plot, the missing one will be empty | ||||
|         await expect(page.locator(".c-plot--stacked-container:has(.gl-plot)")).toHaveCount(1); | ||||
|         //Verify that console.warn is thrown | ||||
|         expect(errorLogs).toHaveLength(1); | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| /** | ||||
|  * This is used the create a stacked plot object | ||||
|  * @private | ||||
|  */ | ||||
| async function makeStackedPlot(page) { | ||||
|     // fresh page with time range from 2022-03-29 22:00:00.000Z to 2022-03-29 22:00:30.000Z | ||||
|     await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|     // create stacked plot | ||||
|     await page.locator('button.c-create-button').click(); | ||||
|     await page.locator('li:has-text("Stacked Plot")').click(); | ||||
|  | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation({ waitUntil: 'networkidle'}), | ||||
|         page.locator('text=OK').click(), | ||||
|         //Wait for Save Banner to appear | ||||
|         page.waitForSelector('.c-message-banner__message') | ||||
|     ]); | ||||
|  | ||||
|     // save the stacked plot | ||||
|     await saveStackedPlot(page); | ||||
|  | ||||
|     // create a sinewave generator | ||||
|     await createSineWaveGenerator(page); | ||||
|  | ||||
|     // click on stacked plot | ||||
|     await page.locator('text=Open MCT My Items >> span').nth(3).click(); | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('text=Unnamed Stacked Plot').first().click() | ||||
|     ]); | ||||
|  | ||||
|     // create a second sinewave generator | ||||
|     await createSineWaveGenerator(page); | ||||
|  | ||||
|     // click on stacked plot | ||||
|     await page.locator('text=Open MCT My Items >> span').nth(3).click(); | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('text=Unnamed Stacked Plot').first().click() | ||||
|     ]); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * This is used to save a stacked plot object | ||||
|  * @private | ||||
|  */ | ||||
| async function saveStackedPlot(page) { | ||||
|     // save stacked plot | ||||
|     await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click(); | ||||
|  | ||||
|     await Promise.all([ | ||||
|         page.locator('text=Save and Finish Editing').click(), | ||||
|         //Wait for Save Banner to appear | ||||
|         page.waitForSelector('.c-message-banner__message') | ||||
|     ]); | ||||
|     //Wait until Save Banner is gone | ||||
|     await page.locator('.c-message-banner__close-button').click(); | ||||
|     await page.waitForSelector('.c-message-banner__message', { state: 'detached' }); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * This is used to create a sine wave generator object | ||||
|  * @private | ||||
|  */ | ||||
| async function createSineWaveGenerator(page) { | ||||
|     //Create sine wave generator | ||||
|     await page.locator('button.c-create-button').click(); | ||||
|     await page.locator('li:has-text("Sine Wave Generator")').click(); | ||||
|  | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation({ waitUntil: 'networkidle'}), | ||||
|         page.locator('text=OK').click(), | ||||
|         //Wait for Save Banner to appear | ||||
|         page.waitForSelector('.c-message-banner__message') | ||||
|     ]); | ||||
| } | ||||
							
								
								
									
										41
									
								
								e2e/tests/plugins/remoteClock/remoteClock.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								e2e/tests/plugins/remoteClock/remoteClock.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| const { test } = require('../../../fixtures.js'); | ||||
| // eslint-disable-next-line no-unused-vars | ||||
| const { expect } = require('@playwright/test'); | ||||
|  | ||||
| test.describe('Remote Clock', () => { | ||||
|     // eslint-disable-next-line require-await | ||||
|     test.fixme('blocks historical requests until first tick is received', async ({ page }) => { | ||||
|         test.info().annotations.push({ | ||||
|             type: 'issue', | ||||
|             description: 'https://github.com/nasa/openmct/issues/5221' | ||||
|         }); | ||||
|         // addInitScript to with remote clock | ||||
|         // Switch time conductor mode to 'remote clock' | ||||
|         // Navigate to telemetry | ||||
|         // Verify that the plot renders historical data within the correct bounds | ||||
|         // Refresh the page | ||||
|         // Verify again that the plot renders historical data within the correct bounds | ||||
|     }); | ||||
| }); | ||||
| @@ -24,7 +24,7 @@ const { test } = require('../../../fixtures'); | ||||
| const { expect } = require('@playwright/test'); | ||||
|  | ||||
| test.describe('Telemetry Table', () => { | ||||
|     test('unpauses when paused by button and user changes bounds', async ({ page }) => { | ||||
|     test('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' | ||||
| @@ -39,9 +39,6 @@ test.describe('Telemetry Table', () => { | ||||
|         await page.locator(createButton).click(); | ||||
|         await page.locator('li:has-text("Telemetry Table")').click(); | ||||
|  | ||||
|         // Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184 | ||||
|         await page.click('form[name="mctForm"] a:has-text("My Items")'); | ||||
|  | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('text=OK').click(), | ||||
| @@ -59,9 +56,6 @@ test.describe('Telemetry Table', () => { | ||||
|         // add Sine Wave Generator with defaults | ||||
|         await page.locator('li:has-text("Sine Wave Generator")').click(); | ||||
|  | ||||
|         // Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184 | ||||
|         await page.click('form[name="mctForm"] a:has-text("My Items")'); | ||||
|  | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('text=OK').click(), | ||||
| @@ -77,25 +71,34 @@ test.describe('Telemetry Table', () => { | ||||
|         ]); | ||||
|  | ||||
|         // Click pause button | ||||
|         const pauseButton = await page.locator('button.c-button.icon-pause'); | ||||
|         const pauseButton = page.locator('button.c-button.icon-pause'); | ||||
|         await pauseButton.click(); | ||||
|  | ||||
|         const tableWrapper = await page.locator('div.c-table-wrapper'); | ||||
|         const tableWrapper = page.locator('div.c-table-wrapper'); | ||||
|         await expect(tableWrapper).toHaveClass(/is-paused/); | ||||
|  | ||||
|         // Arbitrarily change end date to some time in the future | ||||
|         // 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.setUTCDate(endDate.getUTCDate() + 1); | ||||
|         endDate = endDate.toISOString().replace(/T.*/, ''); | ||||
|  | ||||
|         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); | ||||
|     }); | ||||
| }); | ||||
|   | ||||
							
								
								
									
										185
									
								
								e2e/tests/plugins/timer/timer.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										185
									
								
								e2e/tests/plugins/timer/timer.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,185 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| const { test } = require('../../../fixtures.js'); | ||||
| const { expect } = require('@playwright/test'); | ||||
|  | ||||
| test.describe('Timer', () => { | ||||
|  | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         //Go to baseURL | ||||
|         await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|         //Click the Create button | ||||
|         await page.click('button:has-text("Create")'); | ||||
|  | ||||
|         // Click 'Timer' | ||||
|         await page.click('text=Timer'); | ||||
|  | ||||
|         // Click text=OK | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation({waitUntil: 'networkidle'}), | ||||
|             page.click('text=OK') | ||||
|         ]); | ||||
|  | ||||
|         await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Timer'); | ||||
|     }); | ||||
|  | ||||
|     test('Can perform actions on the Timer', async ({ page }) => { | ||||
|         test.info().annotations.push({ | ||||
|             type: 'issue', | ||||
|             description: 'https://github.com/nasa/openmct/issues/4313' | ||||
|         }); | ||||
|  | ||||
|         await test.step("From the tree context menu", async () => { | ||||
|             await triggerTimerContextMenuAction(page, 'Start'); | ||||
|             await triggerTimerContextMenuAction(page, 'Pause'); | ||||
|             await triggerTimerContextMenuAction(page, 'Restart at 0'); | ||||
|             await triggerTimerContextMenuAction(page, 'Stop'); | ||||
|         }); | ||||
|  | ||||
|         await test.step("From the 3dot menu", async () => { | ||||
|             await triggerTimer3dotMenuAction(page, 'Start'); | ||||
|             await triggerTimer3dotMenuAction(page, 'Pause'); | ||||
|             await triggerTimer3dotMenuAction(page, 'Restart at 0'); | ||||
|             await triggerTimer3dotMenuAction(page, 'Stop'); | ||||
|         }); | ||||
|  | ||||
|         await test.step("From the object view", async () => { | ||||
|             await triggerTimerViewAction(page, 'Start'); | ||||
|             await triggerTimerViewAction(page, 'Pause'); | ||||
|             await triggerTimerViewAction(page, 'Restart at 0'); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| /** | ||||
|  * Actions that can be performed on a timer from context menus. | ||||
|  * @typedef {'Start' | 'Stop' | 'Pause' | 'Restart at 0'} TimerAction | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * Actions that can be performed on a timer from the object view. | ||||
|  * @typedef {'Start' | 'Pause' | 'Restart at 0'} TimerViewAction | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * Open the timer context menu from the object tree. | ||||
|  * Expands the 'My Items' folder if it is not already expanded. | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function openTimerContextMenu(page) { | ||||
|     const myItemsFolder = page.locator('text=Open MCT My Items >> span').nth(3); | ||||
|     const className = await myItemsFolder.getAttribute('class'); | ||||
|     if (!className.includes('c-disclosure-triangle--expanded')) { | ||||
|         await myItemsFolder.click(); | ||||
|     } | ||||
|  | ||||
|     await page.locator(`a:has-text("Unnamed Timer")`).click({ | ||||
|         button: 'right' | ||||
|     }); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Trigger a timer action from the tree context menu | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  * @param {TimerAction} action | ||||
|  */ | ||||
| async function triggerTimerContextMenuAction(page, action) { | ||||
|     const menuAction = `.c-menu ul li >> text="${action}"`; | ||||
|     await openTimerContextMenu(page); | ||||
|     await page.locator(menuAction).click(); | ||||
|     assertTimerStateAfterAction(page, action); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Trigger a timer action from the 3dot menu | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  * @param {TimerAction} action | ||||
|  */ | ||||
| async function triggerTimer3dotMenuAction(page, action) { | ||||
|     const menuAction = `.c-menu ul li >> text="${action}"`; | ||||
|     const threeDotMenuButton = 'button[title="More options"]'; | ||||
|     let isActionAvailable = false; | ||||
|     let iterations = 0; | ||||
|     // Dismiss/open the 3dot menu until the action is available | ||||
|     // or a maxiumum number of iterations is reached | ||||
|     while (!isActionAvailable && iterations <= 20) { | ||||
|         await page.click('.c-object-view'); | ||||
|         await page.click(threeDotMenuButton); | ||||
|         isActionAvailable = await page.locator(menuAction).isVisible(); | ||||
|         iterations++; | ||||
|     } | ||||
|  | ||||
|     await page.locator(menuAction).click(); | ||||
|     assertTimerStateAfterAction(page, action); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Trigger a timer action from the object view | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  * @param {TimerViewAction} action | ||||
|  */ | ||||
| async function triggerTimerViewAction(page, action) { | ||||
|     await page.locator('.c-timer').hover({trial: true}); | ||||
|     const buttonTitle = buttonTitleFromAction(action); | ||||
|     await page.click(`button[title="${buttonTitle}"]`); | ||||
|     assertTimerStateAfterAction(page, action); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Takes in a TimerViewAction and returns the button title | ||||
|  * @param {TimerViewAction} action | ||||
|  */ | ||||
| function buttonTitleFromAction(action) { | ||||
|     switch (action) { | ||||
|     case 'Start': | ||||
|         return 'Start'; | ||||
|     case 'Pause': | ||||
|         return 'Pause'; | ||||
|     case 'Restart at 0': | ||||
|         return 'Reset'; | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Verify the timer state after a timer action has been performed. | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  * @param {TimerAction} action | ||||
|  */ | ||||
| async function assertTimerStateAfterAction(page, action) { | ||||
|     let timerStateClass; | ||||
|     switch (action) { | ||||
|     case 'Start': | ||||
|     case 'Restart at 0': | ||||
|         timerStateClass = "is-started"; | ||||
|         break; | ||||
|     case 'Stop': | ||||
|         timerStateClass = 'is-stopped'; | ||||
|         break; | ||||
|     case 'Pause': | ||||
|         timerStateClass = 'is-paused'; | ||||
|         break; | ||||
|     } | ||||
|  | ||||
|     await expect.soft(page.locator('.c-timer')).toHaveClass(new RegExp(timerStateClass)); | ||||
| } | ||||
| @@ -1,22 +0,0 @@ | ||||
| { | ||||
|   "cookies": [], | ||||
|   "origins": [ | ||||
|     { | ||||
|       "origin": "http://localhost:8080", | ||||
|       "localStorage": [ | ||||
|         { | ||||
|           "name": "tcHistory", | ||||
|           "value": "{\"utc\":[{\"start\":1652301954635,\"end\":1652303754635}]}" | ||||
|         }, | ||||
|         { | ||||
|           "name": "mct", | ||||
|           "value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1652303756008,\"modified\":1652303756007},\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"73f2d9ae-d1f3-4561-b7fc-ecd5df557249\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1652303755999,\"location\":\"mine\",\"persisted\":1652303756002}}" | ||||
|         }, | ||||
|         { | ||||
|           "name": "mct-tree-expanded", | ||||
|           "value": "[]" | ||||
|         } | ||||
|       ] | ||||
|     } | ||||
|   ] | ||||
| } | ||||
| @@ -45,6 +45,15 @@ test('Verify that the create button appears and that the Folder Domain Object is | ||||
|     await page.click('button:has-text("Create")'); | ||||
|  | ||||
|     // Verify that Create Folder appears in the dropdown | ||||
|     const locator = page.locator(':nth-match(:text("Folder"), 2)'); | ||||
|     await expect(locator).toBeEnabled(); | ||||
|     await expect(page.locator(':nth-match(:text("Folder"), 2)')).toBeEnabled(); | ||||
| }); | ||||
|  | ||||
| test('Verify that My Items Tree appears @ipad', async ({ page }) => { | ||||
|     //Test.slow annotation is currently broken. Needs to be fixed in https://github.com/nasa/openmct/issues/5374 | ||||
|     test.slow(); | ||||
|     //Go to baseURL | ||||
|     await page.goto('/'); | ||||
|  | ||||
|     //My Items to be visible | ||||
|     await expect(page.locator('a:has-text("My Items")')).toBeEnabled(); | ||||
| }); | ||||
|   | ||||
							
								
								
									
										111
									
								
								e2e/tests/ui/layout/search/grandsearch.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								e2e/tests/ui/layout/search/grandsearch.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,111 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* | ||||
| This test suite is dedicated to tests which verify search functionality. | ||||
| */ | ||||
|  | ||||
| const { expect } = require('@playwright/test'); | ||||
| const { test } = require('../../../../fixtures'); | ||||
|  | ||||
| /** | ||||
|   * Creates a notebook object and adds an entry. | ||||
|   * @param {import('@playwright/test').Page} page | ||||
|   */ | ||||
| async function createClockAndDisplayLayout(page) { | ||||
|     //Go to baseURL | ||||
|     await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|     // Click button:has-text("Create") | ||||
|     await page.locator('button:has-text("Create")').click(); | ||||
|     // Click li:has-text("Notebook") | ||||
|     await page.locator('li:has-text("Clock")').click(); | ||||
|     // Click button:has-text("OK") | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('button:has-text("OK")').click() | ||||
|     ]); | ||||
|  | ||||
|     // Click a:has-text("My Items") | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('a:has-text("My Items") >> nth=0').click() | ||||
|     ]); | ||||
|     // Click button:has-text("Create") | ||||
|     await page.locator('button:has-text("Create")').click(); | ||||
|     // Click li:has-text("Notebook") | ||||
|     await page.locator('li:has-text("Display Layout")').click(); | ||||
|     // Click button:has-text("OK") | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('button:has-text("OK")').click() | ||||
|     ]); | ||||
| } | ||||
|  | ||||
| test.describe('Grand Search', () => { | ||||
|     test('Can search for objects, and subsequent search dropdown behaves properly', async ({ page }) => { | ||||
|         await createClockAndDisplayLayout(page); | ||||
|  | ||||
|         // Click [aria-label="OpenMCT Search"] input[type="search"] | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); | ||||
|         // Fill [aria-label="OpenMCT Search"] input[type="search"] | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Cl'); | ||||
|         await expect(page.locator('[aria-label="Search Result"]')).toContainText('Clock'); | ||||
|         // Click text=Elements >> nth=0 | ||||
|         await page.locator('text=Elements').first().click(); | ||||
|         await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible(); | ||||
|  | ||||
|         // Click [aria-label="OpenMCT Search"] [aria-label="Search Input"] | ||||
|         await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click(); | ||||
|         // Click [aria-label="Unnamed Clock clock result"] >> text=Unnamed Clock | ||||
|         await page.locator('[aria-label="Unnamed Clock clock result"] >> text=Unnamed Clock').click(); | ||||
|         await expect(page.locator('.js-preview-window')).toBeVisible(); | ||||
|  | ||||
|         // Click [aria-label="Close"] | ||||
|         await page.locator('[aria-label="Close"]').click(); | ||||
|         await expect(page.locator('[aria-label="Search Result"]')).toBeVisible(); | ||||
|         await expect(page.locator('[aria-label="Search Result"]')).toContainText('Cloc'); | ||||
|  | ||||
|         // Click [aria-label="OpenMCT Search"] a >> nth=0 | ||||
|         await page.locator('[aria-label="OpenMCT Search"] a').first().click(); | ||||
|         await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible(); | ||||
|  | ||||
|         // Fill [aria-label="OpenMCT Search"] input[type="search"] | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('foo'); | ||||
|         await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible(); | ||||
|  | ||||
|         // Click text=Snapshot Save and Finish Editing Save and Continue Editing >> button >> nth=1 | ||||
|         await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click(); | ||||
|         // Click text=Save and Finish Editing | ||||
|         await page.locator('text=Save and Finish Editing').click(); | ||||
|         // Click [aria-label="OpenMCT Search"] [aria-label="Search Input"] | ||||
|         await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click(); | ||||
|         // Fill [aria-label="OpenMCT Search"] [aria-label="Search Input"] | ||||
|         await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').fill('Cl'); | ||||
|         // Click text=Unnamed Clock | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('text=Unnamed Clock').click() | ||||
|         ]); | ||||
|         await expect(page.locator('.is-object-type-clock')).toBeVisible(); | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										76
									
								
								e2e/tests/visual/addInit.visual.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								e2e/tests/visual/addInit.visual.spec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | ||||
| /* eslint-disable no-undef */ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* | ||||
| Collection of Visual Tests set to run with modified init scripts to inject plugins not otherwise available in the default contexts. | ||||
|  | ||||
| These should only use functional expect statements to verify assumptions about the state | ||||
| in a test and not for functional verification of correctness. Visual tests are not supposed | ||||
| to "fail" on assertions. Instead, they should be used to detect changes between builds or branches. | ||||
|  | ||||
| Note: Larger testsuite sizes are OK due to the setup time associated with these tests. | ||||
| */ | ||||
|  | ||||
| const { test } = require('../../fixtures.js'); | ||||
| const percySnapshot = require('@percy/playwright'); | ||||
| const path = require('path'); | ||||
| const sinon = require('sinon'); | ||||
|  | ||||
| const VISUAL_GRACE_PERIOD = 5 * 1000; //Lets the application "simmer" before the snapshot is taken | ||||
|  | ||||
| const CUSTOM_NAME = 'CUSTOM_NAME'; | ||||
|  | ||||
| // Snippet from https://github.com/microsoft/playwright/issues/6347#issuecomment-965887758 | ||||
| // Will replace with cy.clock() equivalent | ||||
| test.beforeEach(async ({ context }) => { | ||||
|     await context.addInitScript({ | ||||
|         path: path.join(__dirname, '../../..', './node_modules/sinon/pkg/sinon.js') | ||||
|     }); | ||||
|     await context.addInitScript(() => { | ||||
|         window.__clock = sinon.useFakeTimers({ | ||||
|             now: 0, | ||||
|             shouldAdvanceTime: true | ||||
|         }); //Set browser clock to UNIX Epoch | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| test('Visual - Restricted Notebook is visually correct @addInit', async ({ page }) => { | ||||
|     // eslint-disable-next-line no-undef | ||||
|     await page.addInitScript({ path: path.join(__dirname, '../plugins/notebook', './addInitRestrictedNotebook.js') }); | ||||
|     //Go to baseURL | ||||
|     await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|     //Click the Create button | ||||
|     await page.click('button:has-text("Create")'); | ||||
|     // Click text=CUSTOM_NAME | ||||
|     await page.click(`text=${CUSTOM_NAME}`); | ||||
|     // Click text=OK | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation({waitUntil: 'networkidle'}), | ||||
|         page.click('text=OK') | ||||
|     ]); | ||||
|  | ||||
|     // Take a snapshot of the newly created CUSTOM_NAME notebook | ||||
|     await page.waitForTimeout(VISUAL_GRACE_PERIOD); | ||||
|     await percySnapshot(page, 'Restricted Notebook with CUSTOM_NAME'); | ||||
|  | ||||
| }); | ||||
							
								
								
									
										71
									
								
								e2e/tests/visual/controlledClock.visual.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								e2e/tests/visual/controlledClock.visual.spec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,71 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* | ||||
| Collection of Visual Tests set to run in a default context. The tests within this suite | ||||
| are only meant to run against openmct's app.js started by `npm run start` within the | ||||
| `./e2e/playwright-visual.config.js` file. | ||||
|  | ||||
| These should only use functional expect statements to verify assumptions about the state | ||||
| in a test and not for functional verification of correctness. Visual tests are not supposed | ||||
| to "fail" on assertions. Instead, they should be used to detect changes between builds or branches. | ||||
|  | ||||
| Note: Larger testsuite sizes are OK due to the setup time associated with these tests. | ||||
| */ | ||||
|  | ||||
| const { test } = require('../../fixtures.js'); | ||||
| const { expect } = require('@playwright/test'); | ||||
| const percySnapshot = require('@percy/playwright'); | ||||
| const path = require('path'); | ||||
| const sinon = require('sinon'); | ||||
|  | ||||
| // Snippet from https://github.com/microsoft/playwright/issues/6347#issuecomment-965887758 | ||||
| // Will replace with cy.clock() equivalent | ||||
| test.beforeEach(async ({ context }) => { | ||||
|     await context.addInitScript({ | ||||
|         // eslint-disable-next-line no-undef | ||||
|         path: path.join(__dirname, '../../..', './node_modules/sinon/pkg/sinon.js') | ||||
|     }); | ||||
|     await context.addInitScript(() => { | ||||
|         window.__clock = sinon.useFakeTimers({ | ||||
|             now: 0, //Set browser clock to UNIX Epoch | ||||
|             shouldAdvanceTime: false, //Don't advance the clock | ||||
|             toFake: ["setTimeout", "nextTick"] | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
| test.use({ storageState: './e2e/test-data/VisualTestData_storage.json' }); | ||||
|  | ||||
| test('Visual - Overlay Plot Loading Indicator @localstorage', async ({ page }) => { | ||||
|     // Go to baseURL | ||||
|     await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|     await page.locator('a:has-text("Unnamed Overlay Plot Overlay Plot")').click(); | ||||
|     //Ensure that we're on the Unnamed Overlay Plot object | ||||
|     await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Overlay Plot'); | ||||
|  | ||||
|     //Wait for canvas to be rendered and stop animating | ||||
|     await page.locator('canvas >> nth=1').hover({trial: true}); | ||||
|  | ||||
|     //Take snapshot of Sine Wave Generator within Overlay Plot | ||||
|     await percySnapshot(page, 'SineWaveInOverlayPlot'); | ||||
| }); | ||||
| @@ -32,7 +32,8 @@ to "fail" on assertions. Instead, they should be used to detect changes between | ||||
| Note: Larger testsuite sizes are OK due to the setup time associated with these tests. | ||||
| */ | ||||
|  | ||||
| const { test, expect } = require('@playwright/test'); | ||||
| const { test } = require('../../fixtures.js'); | ||||
| const { expect } = require('@playwright/test'); | ||||
| const percySnapshot = require('@percy/playwright'); | ||||
| const path = require('path'); | ||||
| const sinon = require('sinon'); | ||||
| @@ -96,7 +97,11 @@ test('Visual - Default Condition Set', async ({ page }) => { | ||||
|     await percySnapshot(page, 'Default Condition Set'); | ||||
| }); | ||||
|  | ||||
| test('Visual - Default Condition Widget', async ({ page }) => { | ||||
| test.fixme('Visual - Default Condition Widget', async ({ page }) => { | ||||
|     test.info().annotations.push({ | ||||
|         type: 'issue', | ||||
|         description: 'https://github.com/nasa/openmct/issues/5349' | ||||
|     }); | ||||
|     //Go to baseURL | ||||
|     await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
| @@ -206,3 +211,22 @@ test('Visual - Display Layout Icon is correct', async ({ page }) => { | ||||
|     await percySnapshot(page, 'Display Layout Create Menu'); | ||||
|  | ||||
| }); | ||||
|  | ||||
| test('Visual - Default Gauge is correct', async ({ page }) => { | ||||
|  | ||||
|     //Go to baseURL | ||||
|     await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|     //Click the Create button | ||||
|     await page.click('button:has-text("Create")'); | ||||
|  | ||||
|     await page.click('text=Gauge'); | ||||
|  | ||||
|     await page.click('text=OK'); | ||||
|  | ||||
|     // Take a snapshot of the newly created Gauge object | ||||
|     await page.waitForTimeout(VISUAL_GRACE_PERIOD); | ||||
|     await percySnapshot(page, 'Default Gauge'); | ||||
|  | ||||
| }); | ||||
|  | ||||
|   | ||||
							
								
								
									
										86
									
								
								e2e/tests/visual/generateVisualTestData.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								e2e/tests/visual/generateVisualTestData.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,86 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* | ||||
| This test suite is dedicated to generating LocalStorage via Session Storage to be used | ||||
| in some visual test suites like controlledClock.visual.spec.js. This suite should run to completion | ||||
| and generate an artifact named ./e2e/test-data/VisualTestData_storage.json . This will run | ||||
| on every Commit to ensure that this object still loads into tests correctly and will retain the | ||||
| .e2e.spec.js suffix. | ||||
|  | ||||
| TODO: Provide additional validation of object properties as it grows. | ||||
|  | ||||
| */ | ||||
|  | ||||
| const { test } = require('../../fixtures.js'); | ||||
| const { expect } = require('@playwright/test'); | ||||
|  | ||||
| test('Generate Visual Test Data @localStorage', async ({ page, context }) => { | ||||
|     //Go to baseURL | ||||
|     await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|     await page.locator('button:has-text("Create")').click(); | ||||
|  | ||||
|     // add overlay plot with defaults | ||||
|     await page.locator('li:has-text("Overlay Plot")').click(); | ||||
|  | ||||
|     // Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184 | ||||
|     await page.click('form[name="mctForm"] a:has-text("My Items")'); | ||||
|  | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('text=OK').click(), | ||||
|         //Wait for Save Banner to appear1 | ||||
|         page.waitForSelector('.c-message-banner__message') | ||||
|     ]); | ||||
|  | ||||
|     // save (exit edit mode) | ||||
|     await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click(); | ||||
|     await page.locator('text=Save and Finish Editing').click(); | ||||
|  | ||||
|     // click create button | ||||
|     await page.locator('button:has-text("Create")').click(); | ||||
|  | ||||
|     // add sine wave generator with defaults | ||||
|     await page.locator('li:has-text("Sine Wave Generator")').click(); | ||||
|  | ||||
|     //Add a 5000 ms Delay | ||||
|     await page.locator('[aria-label="Loading Delay \\(ms\\)"]').fill('5000'); | ||||
|  | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('text=OK').click(), | ||||
|         //Wait for Save Banner to appear1 | ||||
|         page.waitForSelector('.c-message-banner__message') | ||||
|     ]); | ||||
|  | ||||
|     // focus the overlay plot | ||||
|     await page.locator('text=Open MCT My Items >> span').nth(3).click(); | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('text=Unnamed Overlay Plot').first().click() | ||||
|     ]); | ||||
|  | ||||
|     await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Overlay Plot'); | ||||
|     //Save localStorage for future test execution | ||||
|     await context.storageState({ path: './e2e/test-data/VisualTestData_storage.json' }); | ||||
| }); | ||||
							
								
								
									
										105
									
								
								e2e/tests/visual/search.visual.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								e2e/tests/visual/search.visual.spec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,105 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* | ||||
| This test suite is dedicated to tests which verify search functionality. | ||||
| */ | ||||
|  | ||||
| const { test } = require('../../fixtures.js'); | ||||
| const { expect } = require('@playwright/test'); | ||||
| const percySnapshot = require('@percy/playwright'); | ||||
|  | ||||
| /** | ||||
|   * Creates a notebook object and adds an entry. | ||||
|   * @param {import('@playwright/test').Page} page | ||||
|   */ | ||||
| async function createClockAndDisplayLayout(page) { | ||||
|     //Go to baseURL | ||||
|     await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|     // Click button:has-text("Create") | ||||
|     await page.locator('button:has-text("Create")').click(); | ||||
|     // Click li:has-text("Notebook") | ||||
|     await page.locator('li:has-text("Clock")').click(); | ||||
|     // Click button:has-text("OK") | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('button:has-text("OK")').click() | ||||
|     ]); | ||||
|  | ||||
|     // Click a:has-text("My Items") | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('a:has-text("My Items") >> nth=0').click() | ||||
|     ]); | ||||
|     // Click button:has-text("Create") | ||||
|     await page.locator('button:has-text("Create")').click(); | ||||
|     // Click li:has-text("Notebook") | ||||
|     await page.locator('li:has-text("Display Layout")').click(); | ||||
|     // Click button:has-text("OK") | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('button:has-text("OK")').click() | ||||
|     ]); | ||||
| } | ||||
|  | ||||
| test.describe('Grand Search', () => { | ||||
|     test('Can search for objects, and subsequent search dropdown behaves properly', async ({ page }) => { | ||||
|         await createClockAndDisplayLayout(page); | ||||
|  | ||||
|         // Click [aria-label="OpenMCT Search"] input[type="search"] | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); | ||||
|         // Fill [aria-label="OpenMCT Search"] input[type="search"] | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Cl'); | ||||
|         await expect(page.locator('[aria-label="Search Result"]')).toContainText('Clock'); | ||||
|         await percySnapshot(page, 'Searching for Clocks'); | ||||
|         // Click text=Elements >> nth=0 | ||||
|         await page.locator('text=Elements').first().click(); | ||||
|         await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible(); | ||||
|  | ||||
|         // Click [aria-label="OpenMCT Search"] [aria-label="Search Input"] | ||||
|         await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click(); | ||||
|         // Click [aria-label="Unnamed Clock clock result"] >> text=Unnamed Clock | ||||
|         await page.locator('[aria-label="Unnamed Clock clock result"] >> text=Unnamed Clock').click(); | ||||
|         await percySnapshot(page, 'Preview for clock should display when editing enabled and search item clicked'); | ||||
|  | ||||
|         // Click [aria-label="Close"] | ||||
|         await page.locator('[aria-label="Close"]').click(); | ||||
|         await percySnapshot(page, 'Search should still be showing after preview closed'); | ||||
|  | ||||
|         // Click text=Snapshot Save and Finish Editing Save and Continue Editing >> button >> nth=1 | ||||
|         await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click(); | ||||
|         // Click text=Save and Finish Editing | ||||
|         await page.locator('text=Save and Finish Editing').click(); | ||||
|         // Click [aria-label="OpenMCT Search"] [aria-label="Search Input"] | ||||
|         await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click(); | ||||
|         // Fill [aria-label="OpenMCT Search"] [aria-label="Search Input"] | ||||
|         await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').fill('Cl'); | ||||
|         // Click text=Unnamed Clock | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('text=Unnamed Clock').click() | ||||
|         ]); | ||||
|         await percySnapshot(page, 'Clicking on search results should navigate to them if not editing'); | ||||
|  | ||||
|     }); | ||||
| }); | ||||
| @@ -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", | ||||
|   | ||||
							
								
								
									
										83
									
								
								example/faultManagment/exampleFaultSource.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								example/faultManagment/exampleFaultSource.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,83 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| export default function () { | ||||
|     return function install(openmct) { | ||||
|         openmct.install(openmct.plugins.FaultManagement()); | ||||
|  | ||||
|         openmct.faults.addProvider({ | ||||
|             request(domainObject, options) { | ||||
|                 const faults = JSON.parse(localStorage.getItem('faults')); | ||||
|  | ||||
|                 return Promise.resolve(faults.alarms); | ||||
|             }, | ||||
|             subscribe(domainObject, callback) { | ||||
|                 const faultsData = JSON.parse(localStorage.getItem('faults')).alarms; | ||||
|  | ||||
|                 function getRandomIndex(start, end) { | ||||
|                     return Math.floor(start + (Math.random() * (end - start + 1))); | ||||
|                 } | ||||
|  | ||||
|                 let id = setInterval(() => { | ||||
|                     const index = getRandomIndex(0, faultsData.length - 1); | ||||
|                     const randomFaultData = faultsData[index]; | ||||
|                     const randomFault = randomFaultData.fault; | ||||
|                     randomFault.currentValueInfo.value = Math.random(); | ||||
|                     callback({ | ||||
|                         fault: randomFault, | ||||
|                         type: 'alarms' | ||||
|                     }); | ||||
|                 }, 300); | ||||
|  | ||||
|                 return () => { | ||||
|                     clearInterval(id); | ||||
|                 }; | ||||
|             }, | ||||
|             supportsRequest(domainObject) { | ||||
|                 const faults = localStorage.getItem('faults'); | ||||
|  | ||||
|                 return faults && domainObject.type === 'faultManagement'; | ||||
|             }, | ||||
|             supportsSubscribe(domainObject) { | ||||
|                 const faults = localStorage.getItem('faults'); | ||||
|  | ||||
|                 return faults && domainObject.type === 'faultManagement'; | ||||
|             }, | ||||
|             acknowledgeFault(fault, { comment = '' }) { | ||||
|                 console.log('acknowledgeFault', fault); | ||||
|                 console.log('comment', comment); | ||||
|  | ||||
|                 return Promise.resolve({ | ||||
|                     success: true | ||||
|                 }); | ||||
|             }, | ||||
|             shelveFault(fault, shelveData) { | ||||
|                 console.log('shelveFault', fault); | ||||
|                 console.log('shelveData', shelveData); | ||||
|  | ||||
|                 return Promise.resolve({ | ||||
|                     success: true | ||||
|                 }); | ||||
|             } | ||||
|         }); | ||||
|     }; | ||||
| } | ||||
							
								
								
									
										47
									
								
								example/faultManagment/pluginSpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								example/faultManagment/pluginSpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import { | ||||
|     createOpenMct, | ||||
|     resetApplicationState | ||||
| } from '../../src/utils/testing'; | ||||
|  | ||||
| describe("The Example Fault Source Plugin", () => { | ||||
|     let openmct; | ||||
|  | ||||
|     beforeEach(() => { | ||||
|         openmct = createOpenMct(); | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     it('is not installed by default', () => { | ||||
|         expect(openmct.faults.provider).toBeUndefined(); | ||||
|     }); | ||||
|  | ||||
|     it('can be installed', () => { | ||||
|         openmct.install(openmct.plugins.example.ExampleFaultSource()); | ||||
|         expect(openmct.faults.provider).not.toBeUndefined(); | ||||
|     }); | ||||
| }); | ||||
| @@ -32,7 +32,8 @@ define([ | ||||
|         offset: 0, | ||||
|         dataRateInHz: 1, | ||||
|         randomness: 0, | ||||
|         phase: 0 | ||||
|         phase: 0, | ||||
|         loadDelay: 0 | ||||
|     }; | ||||
|  | ||||
|     function GeneratorProvider(openmct) { | ||||
| @@ -53,8 +54,9 @@ define([ | ||||
|             'period', | ||||
|             'offset', | ||||
|             'dataRateInHz', | ||||
|             'randomness', | ||||
|             'phase', | ||||
|             'randomness' | ||||
|             'loadDelay' | ||||
|         ]; | ||||
|  | ||||
|         request = request || {}; | ||||
|   | ||||
| @@ -116,6 +116,7 @@ | ||||
|         var dataRateInHz = request.dataRateInHz; | ||||
|         var phase = request.phase; | ||||
|         var randomness = request.randomness; | ||||
|         var loadDelay = Math.max(request.loadDelay, 0); | ||||
|  | ||||
|         var step = 1000 / dataRateInHz; | ||||
|         var nextStep = start - (start % step) + step; | ||||
| @@ -133,6 +134,14 @@ | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         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 ? { | ||||
|   | ||||
| @@ -81,7 +81,7 @@ define([ | ||||
|                 { | ||||
|                     name: "Amplitude", | ||||
|                     control: "numberfield", | ||||
|                     cssClass: "l-input-sm l-numeric", | ||||
|                     cssClass: "l-numeric", | ||||
|                     key: "amplitude", | ||||
|                     required: true, | ||||
|                     property: [ | ||||
| @@ -92,7 +92,7 @@ define([ | ||||
|                 { | ||||
|                     name: "Offset", | ||||
|                     control: "numberfield", | ||||
|                     cssClass: "l-input-sm l-numeric", | ||||
|                     cssClass: "l-numeric", | ||||
|                     key: "offset", | ||||
|                     required: true, | ||||
|                     property: [ | ||||
| @@ -132,6 +132,17 @@ define([ | ||||
|                         "telemetry", | ||||
|                         "randomness" | ||||
|                     ] | ||||
|                 }, | ||||
|                 { | ||||
|                     name: "Loading Delay (ms)", | ||||
|                     control: "numberfield", | ||||
|                     cssClass: "l-input-sm l-numeric", | ||||
|                     key: "loadDelay", | ||||
|                     required: true, | ||||
|                     property: [ | ||||
|                         "telemetry", | ||||
|                         "loadDelay" | ||||
|                     ] | ||||
|                 } | ||||
|             ], | ||||
|             initialize: function (object) { | ||||
| @@ -141,7 +152,8 @@ define([ | ||||
|                     offset: 0, | ||||
|                     dataRateInHz: 1, | ||||
|                     phase: 0, | ||||
|                     randomness: 0 | ||||
|                     randomness: 0, | ||||
|                     loadDelay: 0 | ||||
|                 }; | ||||
|             } | ||||
|         }); | ||||
|   | ||||
| @@ -168,7 +168,7 @@ function getImageUrlListFromConfig(configuration) { | ||||
| } | ||||
|  | ||||
| function getImageLoadDelay(domainObject) { | ||||
|     const imageLoadDelay = domainObject.configuration.imageLoadDelayInMilliSeconds; | ||||
|     const imageLoadDelay = Math.trunc(Number(domainObject.configuration.imageLoadDelayInMilliSeconds)); | ||||
|     if (!imageLoadDelay) { | ||||
|         openmctInstance.objects.mutate(domainObject, 'configuration.imageLoadDelayInMilliSeconds', DEFAULT_IMAGE_LOAD_DELAY_IN_MILISECONDS); | ||||
|  | ||||
| @@ -190,7 +190,9 @@ function getRealtimeProvider() { | ||||
|         subscribe: (domainObject, callback) => { | ||||
|             const delay = getImageLoadDelay(domainObject); | ||||
|             const interval = setInterval(() => { | ||||
|                 callback(pointForTimestamp(Date.now(), domainObject.name, getImageSamples(domainObject.configuration), delay)); | ||||
|                 const imageSamples = getImageSamples(domainObject.configuration); | ||||
|                 const datum = pointForTimestamp(Date.now(), domainObject.name, imageSamples, delay); | ||||
|                 callback(datum); | ||||
|             }, delay); | ||||
|  | ||||
|             return () => { | ||||
| @@ -229,8 +231,9 @@ function getLadProvider() { | ||||
|         }, | ||||
|         request: (domainObject, options) => { | ||||
|             const delay = getImageLoadDelay(domainObject); | ||||
|             const datum = pointForTimestamp(Date.now(), domainObject.name, getImageSamples(domainObject.configuration), delay); | ||||
|  | ||||
|             return Promise.resolve([pointForTimestamp(Date.now(), domainObject.name, delay)]); | ||||
|             return Promise.resolve([datum]); | ||||
|         } | ||||
|     }; | ||||
| } | ||||
|   | ||||
| @@ -75,7 +75,6 @@ | ||||
|         const TWO_HOURS = ONE_HOUR * 2; | ||||
|         const ONE_DAY = ONE_HOUR * 24; | ||||
|  | ||||
|  | ||||
|         openmct.install(openmct.plugins.LocalStorage()); | ||||
|  | ||||
|         openmct.install(openmct.plugins.example.Generator()); | ||||
| @@ -192,7 +191,7 @@ | ||||
|         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()); | ||||
|   | ||||
| @@ -74,13 +74,8 @@ module.exports = (config) => { | ||||
|         }, | ||||
|         coverageIstanbulReporter: { | ||||
|             fixWebpackSourcePaths: true, | ||||
|             dir: "dist/reports/coverage", | ||||
|             reports: ['lcovonly', 'text-summary'], | ||||
|             thresholds: { | ||||
|                 global: { | ||||
|                     lines: 52 | ||||
|                 } | ||||
|             } | ||||
|             dir: "coverage/unit", | ||||
|             reports: ['lcovonly'] | ||||
|         }, | ||||
|         specReporter: { | ||||
|             maxLogLines: 5, | ||||
|   | ||||
							
								
								
									
										41
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										41
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,31 +1,32 @@ | ||||
| { | ||||
|   "name": "openmct", | ||||
|   "version": "2.0.5-SNAPSHOT", | ||||
|   "version": "2.1.0-SNAPSHOT", | ||||
|   "description": "The Open MCT core platform", | ||||
|   "devDependencies": { | ||||
|     "@babel/eslint-parser": "7.18.2", | ||||
|     "@braintree/sanitize-url": "6.0.0", | ||||
|     "@percy/cli": "1.2.1", | ||||
|     "@percy/playwright": "1.0.4", | ||||
|     "@playwright/test": "1.21.1", | ||||
|     "@playwright/test": "1.23.0", | ||||
|     "@types/eventemitter3": "^1.0.0", | ||||
|     "@types/jasmine": "^4.0.1", | ||||
|     "@types/karma": "^6.3.2", | ||||
|     "@types/lodash": "^4.14.178", | ||||
|     "@types/mocha": "^9.1.0", | ||||
|     "babel-loader": "8.2.3", | ||||
|     "babel-loader": "8.2.5", | ||||
|     "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.13.0", | ||||
|     "eslint": "8.18.0", | ||||
|     "eslint-plugin-compat": "4.0.2", | ||||
|     "eslint-plugin-playwright": "0.9.0", | ||||
|     "eslint-plugin-vue": "9.1.0", | ||||
|     "eslint-plugin-vue": "9.1.1", | ||||
|     "eslint-plugin-you-dont-need-lodash-underscore": "6.12.0", | ||||
|     "eventemitter3": "1.2.0", | ||||
|     "express": "4.13.1", | ||||
| @@ -33,7 +34,7 @@ | ||||
|     "git-rev-sync": "3.0.2", | ||||
|     "html2canvas": "1.4.1", | ||||
|     "imports-loader": "0.8.0", | ||||
|     "jasmine-core": "4.1.1", | ||||
|     "jasmine-core": "4.2.0", | ||||
|     "jsdoc": "3.5.5", | ||||
|     "karma": "6.3.20", | ||||
|     "karma-chrome-launcher": "3.1.1", | ||||
| @@ -41,7 +42,7 @@ | ||||
|     "karma-coverage": "2.2.0", | ||||
|     "karma-coverage-istanbul-reporter": "3.0.3", | ||||
|     "karma-firefox-launcher": "2.1.2", | ||||
|     "karma-jasmine": "4.0.1", | ||||
|     "karma-jasmine": "5.1.0", | ||||
|     "karma-junit-reporter": "2.0.1", | ||||
|     "karma-sourcemap-loader": "0.3.8", | ||||
|     "karma-spec-reporter": "0.0.34", | ||||
| @@ -49,19 +50,20 @@ | ||||
|     "lighthouse": "9.6.1", | ||||
|     "location-bar": "3.0.1", | ||||
|     "lodash": "4.17.21", | ||||
|     "mini-css-extract-plugin": "2.6.0", | ||||
|     "moment": "2.29.3", | ||||
|     "mini-css-extract-plugin": "2.6.1", | ||||
|     "moment": "2.29.4", | ||||
|     "moment-duration-format": "2.3.2", | ||||
|     "moment-timezone": "0.5.34", | ||||
|     "node-bourbon": "4.2.3", | ||||
|     "painterro": "1.2.56", | ||||
|     "nyc":"15.1.0", | ||||
|     "painterro": "1.2.78", | ||||
|     "plotly.js-basic-dist": "2.12.0", | ||||
|     "plotly.js-gl2d-dist": "2.12.0", | ||||
|     "printj": "1.3.1", | ||||
|     "request": "2.88.2", | ||||
|     "resolve-url-loader": "5.0.0", | ||||
|     "sass": "1.52.2", | ||||
|     "sass-loader": "12.6.0", | ||||
|     "sass-loader": "13.0.2", | ||||
|     "sinon": "14.0.0", | ||||
|     "style-loader": "^1.0.1", | ||||
|     "uuid": "8.3.2", | ||||
| @@ -70,7 +72,7 @@ | ||||
|     "vue-loader": "15.9.8", | ||||
|     "vue-template-compiler": "2.6.14", | ||||
|     "webpack": "5.68.0", | ||||
|     "webpack-cli": "4.9.2", | ||||
|     "webpack-cli": "4.10.0", | ||||
|     "webpack-dev-middleware": "5.3.3", | ||||
|     "webpack-hot-middleware": "2.25.1", | ||||
|     "webpack-merge": "5.8.0" | ||||
| @@ -86,21 +88,26 @@ | ||||
|     "build:coverage": "webpack --config webpack.coverage.js", | ||||
|     "build:watch": "webpack --config webpack.dev.js --watch", | ||||
|     "info": "npx envinfo --system --browsers --npmPackages --binaries --languages --markdown", | ||||
|     "test": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run", | ||||
|     "test:firefox": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run --browsers=FirefoxHeadless", | ||||
|     "test": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run", | ||||
|     "test:firefox": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run --browsers=FirefoxHeadless", | ||||
|     "test:debug": "cross-env NODE_ENV=debug karma start --no-single-run", | ||||
|     "test:e2e:ci": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome smoke branding default condition timeConductor clock exampleImagery notebook persistence performance", | ||||
|     "test:e2e": "npx playwright test", | ||||
|     "test:e2e:ci": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome visual smoke branding default condition timeConductor clock persistence performance grandsearch tags", | ||||
|     "test:e2e:local": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome", | ||||
|     "test:e2e:updatesnapshots": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome --grep @snapshot --update-snapshots", | ||||
|     "test:e2e:updatesnapshots": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @snapshot --update-snapshots", | ||||
|     "test:e2e:visual": "percy exec --config ./e2e/.percy.yml -- npx playwright test --config=e2e/playwright-visual.config.js", | ||||
|     "test:e2e:full": "npx playwright test --config=e2e/playwright-ci.config.js", | ||||
|     "test:perf": "npx playwright test --config=e2e/playwright-performance.config.js", | ||||
|     "test:watch": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --no-single-run", | ||||
|     "test:watch": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --no-single-run", | ||||
|     "jsdoc": "jsdoc -c jsdoc.json -R API.md -r -d dist/docs/api", | ||||
|     "update-about-dialog-copyright": "perl -pi -e 's/20\\d\\d\\-202\\d/2014\\-2022/gm' ./src/ui/layout/AboutDialog.vue", | ||||
|     "update-copyright-date": "npm run update-about-dialog-copyright && grep -lr --null --include=*.{js,scss,vue,ts,sh,html,md,frag} 'Copyright (c) 20' . | xargs -r0 perl -pi -e 's/Copyright\\s\\(c\\)\\s20\\d\\d\\-20\\d\\d/Copyright \\(c\\)\\ 2014\\-2022/gm'", | ||||
|     "otherdoc": "node docs/gendocs.js --in docs/src --out dist/docs --suppress-toc 'docs/src/index.md|docs/src/process/index.md'", | ||||
|     "docs": "npm run jsdoc ; npm run otherdoc", | ||||
|     "cov:e2e:report":"nyc report --reporter=lcovonly --report-dir=./coverage/e2e", | ||||
|     "cov:e2e:full:publish":"codecov --disable=gcov -f ./coverage/e2e/lcov.info -F e2e-full", | ||||
|     "cov:e2e:ci:publish":"codecov --disable=gcov -f ./coverage/e2e/lcov.info -F e2e-ci", | ||||
|     "cov:unit:publish":"codecov --disable=gcov -f ./coverage/unit/lcov.info -F unit", | ||||
|     "prepare": "npm run build:prod" | ||||
|   }, | ||||
|   "repository": { | ||||
|   | ||||
							
								
								
									
										272
									
								
								src/MCT.js
									
									
									
									
									
								
							
							
						
						
									
										272
									
								
								src/MCT.js
									
									
									
									
									
								
							| @@ -96,160 +96,167 @@ define([ | ||||
|         }; | ||||
|  | ||||
|         this.destroy = this.destroy.bind(this); | ||||
|         /** | ||||
|          * Tracks current selection state of the application. | ||||
|          * @private | ||||
|          */ | ||||
|         this.selection = new Selection(this); | ||||
|         [ | ||||
|             /** | ||||
|             * Tracks current selection state of the application. | ||||
|             * @private | ||||
|             */ | ||||
|             ['selection', () => new Selection(this)], | ||||
|  | ||||
|         /** | ||||
|          * MCT's time conductor, which may be used to synchronize view contents | ||||
|          * for telemetry- or time-based views. | ||||
|          * @type {module:openmct.TimeConductor} | ||||
|          * @memberof module:openmct.MCT# | ||||
|          * @name conductor | ||||
|          */ | ||||
|         this.time = new api.TimeAPI(this); | ||||
|             /** | ||||
|              * MCT's time conductor, which may be used to synchronize view contents | ||||
|              * for telemetry- or time-based views. | ||||
|              * @type {module:openmct.TimeConductor} | ||||
|              * @memberof module:openmct.MCT# | ||||
|              * @name conductor | ||||
|              */ | ||||
|             ['time', () => new api.TimeAPI(this)], | ||||
|  | ||||
|         /** | ||||
|          * An interface for interacting with the composition of domain objects. | ||||
|          * The composition of a domain object is the list of other domain | ||||
|          * objects it "contains" (for instance, that should be displayed | ||||
|          * beneath it in the tree.) | ||||
|          * | ||||
|          * `composition` may be called as a function, in which case it acts | ||||
|          * as [`composition.get`]{@link module:openmct.CompositionAPI#get}. | ||||
|          * | ||||
|          * @type {module:openmct.CompositionAPI} | ||||
|          * @memberof module:openmct.MCT# | ||||
|          * @name composition | ||||
|          */ | ||||
|         this.composition = new api.CompositionAPI(this); | ||||
|             /** | ||||
|              * An interface for interacting with the composition of domain objects. | ||||
|              * The composition of a domain object is the list of other domain | ||||
|              * objects it "contains" (for instance, that should be displayed | ||||
|              * beneath it in the tree.) | ||||
|              * | ||||
|              * `composition` may be called as a function, in which case it acts | ||||
|              * as [`composition.get`]{@link module:openmct.CompositionAPI#get}. | ||||
|              * | ||||
|              * @type {module:openmct.CompositionAPI} | ||||
|              * @memberof module:openmct.MCT# | ||||
|              * @name composition | ||||
|              */ | ||||
|             ['composition', () => new api.CompositionAPI(this)], | ||||
|  | ||||
|         /** | ||||
|          * Registry for views of domain objects which should appear in the | ||||
|          * main viewing area. | ||||
|          * | ||||
|          * @type {module:openmct.ViewRegistry} | ||||
|          * @memberof module:openmct.MCT# | ||||
|          * @name objectViews | ||||
|          */ | ||||
|         this.objectViews = new ViewRegistry(); | ||||
|             /** | ||||
|              * Registry for views of domain objects which should appear in the | ||||
|              * main viewing area. | ||||
|              * | ||||
|              * @type {module:openmct.ViewRegistry} | ||||
|              * @memberof module:openmct.MCT# | ||||
|              * @name objectViews | ||||
|              */ | ||||
|             ['objectViews', () => new ViewRegistry()], | ||||
|  | ||||
|         /** | ||||
|          * Registry for views which should appear in the Inspector area. | ||||
|          * These views will be chosen based on the selection state. | ||||
|          * | ||||
|          * @type {module:openmct.InspectorViewRegistry} | ||||
|          * @memberof module:openmct.MCT# | ||||
|          * @name inspectorViews | ||||
|          */ | ||||
|         this.inspectorViews = new InspectorViewRegistry(); | ||||
|             /** | ||||
|              * Registry for views which should appear in the Inspector area. | ||||
|              * These views will be chosen based on the selection state. | ||||
|              * | ||||
|              * @type {module:openmct.InspectorViewRegistry} | ||||
|              * @memberof module:openmct.MCT# | ||||
|              * @name inspectorViews | ||||
|              */ | ||||
|             ['inspectorViews', () => new InspectorViewRegistry()], | ||||
|  | ||||
|         /** | ||||
|          * Registry for views which should appear in Edit Properties | ||||
|          * dialogs, and similar user interface elements used for | ||||
|          * modifying domain objects external to its regular views. | ||||
|          * | ||||
|          * @type {module:openmct.ViewRegistry} | ||||
|          * @memberof module:openmct.MCT# | ||||
|          * @name propertyEditors | ||||
|          */ | ||||
|         this.propertyEditors = new ViewRegistry(); | ||||
|             /** | ||||
|              * Registry for views which should appear in Edit Properties | ||||
|              * dialogs, and similar user interface elements used for | ||||
|              * modifying domain objects external to its regular views. | ||||
|              * | ||||
|              * @type {module:openmct.ViewRegistry} | ||||
|              * @memberof module:openmct.MCT# | ||||
|              * @name propertyEditors | ||||
|              */ | ||||
|             ['propertyEditors', () => new ViewRegistry()], | ||||
|  | ||||
|         /** | ||||
|          * Registry for views which should appear in the status indicator area. | ||||
|          * @type {module:openmct.ViewRegistry} | ||||
|          * @memberof module:openmct.MCT# | ||||
|          * @name indicators | ||||
|          */ | ||||
|         this.indicators = new ViewRegistry(); | ||||
|             /** | ||||
|              * Registry for views which should appear in the toolbar area while | ||||
|              * editing. These views will be chosen based on the selection state. | ||||
|              * | ||||
|              * @type {module:openmct.ToolbarRegistry} | ||||
|              * @memberof module:openmct.MCT# | ||||
|              * @name toolbars | ||||
|              */ | ||||
|             ['toolbars', () => new ToolbarRegistry()], | ||||
|  | ||||
|         /** | ||||
|          * Registry for views which should appear in the toolbar area while | ||||
|          * editing. These views will be chosen based on the selection state. | ||||
|          * | ||||
|          * @type {module:openmct.ToolbarRegistry} | ||||
|          * @memberof module:openmct.MCT# | ||||
|          * @name toolbars | ||||
|          */ | ||||
|         this.toolbars = new ToolbarRegistry(); | ||||
|             /** | ||||
|              * Registry for domain object types which may exist within this | ||||
|              * instance of Open MCT. | ||||
|              * | ||||
|              * @type {module:openmct.TypeRegistry} | ||||
|              * @memberof module:openmct.MCT# | ||||
|              * @name types | ||||
|              */ | ||||
|             ['types', () => new api.TypeRegistry()], | ||||
|  | ||||
|         /** | ||||
|          * Registry for domain object types which may exist within this | ||||
|          * instance of Open MCT. | ||||
|          * | ||||
|          * @type {module:openmct.TypeRegistry} | ||||
|          * @memberof module:openmct.MCT# | ||||
|          * @name types | ||||
|          */ | ||||
|         this.types = new api.TypeRegistry(); | ||||
|             /** | ||||
|              * An interface for interacting with domain objects and the domain | ||||
|              * object hierarchy. | ||||
|              * | ||||
|              * @type {module:openmct.ObjectAPI} | ||||
|              * @memberof module:openmct.MCT# | ||||
|              * @name objects | ||||
|              */ | ||||
|             ['objects', () => new api.ObjectAPI.default(this.types, this)], | ||||
|  | ||||
|         /** | ||||
|          * An interface for interacting with domain objects and the domain | ||||
|          * object hierarchy. | ||||
|          * | ||||
|          * @type {module:openmct.ObjectAPI} | ||||
|          * @memberof module:openmct.MCT# | ||||
|          * @name objects | ||||
|          */ | ||||
|         this.objects = new api.ObjectAPI.default(this.types, this); | ||||
|             /** | ||||
|              * An interface for retrieving and interpreting telemetry data associated | ||||
|              * with a domain object. | ||||
|              * | ||||
|              * @type {module:openmct.TelemetryAPI} | ||||
|              * @memberof module:openmct.MCT# | ||||
|              * @name telemetry | ||||
|              */ | ||||
|             ['telemetry', () => new api.TelemetryAPI.default(this)], | ||||
|  | ||||
|         /** | ||||
|          * An interface for retrieving and interpreting telemetry data associated | ||||
|          * with a domain object. | ||||
|          * | ||||
|          * @type {module:openmct.TelemetryAPI} | ||||
|          * @memberof module:openmct.MCT# | ||||
|          * @name telemetry | ||||
|          */ | ||||
|         this.telemetry = new api.TelemetryAPI(this); | ||||
|             /** | ||||
|              * An interface for creating new indicators and changing them dynamically. | ||||
|              * | ||||
|              * @type {module:openmct.IndicatorAPI} | ||||
|              * @memberof module:openmct.MCT# | ||||
|              * @name indicators | ||||
|              */ | ||||
|             ['indicators', () => new api.IndicatorAPI(this)], | ||||
|  | ||||
|         /** | ||||
|          * An interface for creating new indicators and changing them dynamically. | ||||
|          * | ||||
|          * @type {module:openmct.IndicatorAPI} | ||||
|          * @memberof module:openmct.MCT# | ||||
|          * @name indicators | ||||
|          */ | ||||
|         this.indicators = new api.IndicatorAPI(this); | ||||
|             /** | ||||
|              * MCT's user awareness management, to enable user and | ||||
|              * role specific functionality. | ||||
|              * @type {module:openmct.UserAPI} | ||||
|              * @memberof module:openmct.MCT# | ||||
|              * @name user | ||||
|              */ | ||||
|             ['user', () => new api.UserAPI(this)], | ||||
|  | ||||
|         /** | ||||
|          * MCT's user awareness management, to enable user and | ||||
|          * role specific functionality. | ||||
|          * @type {module:openmct.UserAPI} | ||||
|          * @memberof module:openmct.MCT# | ||||
|          * @name user | ||||
|          */ | ||||
|         this.user = new api.UserAPI(this); | ||||
|             ['notifications', () => new api.NotificationAPI()], | ||||
|  | ||||
|         this.notifications = new api.NotificationAPI(); | ||||
|             ['editor', () => new api.EditorAPI.default(this)], | ||||
|  | ||||
|         this.editor = new api.EditorAPI.default(this); | ||||
|             ['overlays', () => new OverlayAPI.default()], | ||||
|  | ||||
|         this.overlays = new OverlayAPI.default(); | ||||
|             ['menus', () => new api.MenuAPI(this)], | ||||
|  | ||||
|         this.menus = new api.MenuAPI(this); | ||||
|             ['actions', () => new api.ActionsAPI(this)], | ||||
|  | ||||
|         this.actions = new api.ActionsAPI(this); | ||||
|             ['status', () => new api.StatusAPI(this)], | ||||
|  | ||||
|         this.status = new api.StatusAPI(this); | ||||
|             ['priority', () => api.PriorityAPI], | ||||
|  | ||||
|         this.priority = api.PriorityAPI; | ||||
|             ['router', () => new ApplicationRouter(this)], | ||||
|  | ||||
|         this.router = new ApplicationRouter(this); | ||||
|         this.forms = new api.FormsAPI.default(this); | ||||
|             ['faults', () => new api.FaultManagementAPI.default(this)], | ||||
|  | ||||
|         this.branding = BrandingAPI.default; | ||||
|             ['forms', () => new api.FormsAPI.default(this)], | ||||
|  | ||||
|         /** | ||||
|          * MCT's annotation API that enables | ||||
|          * human-created comments and categorization linked to data products | ||||
|          * @type {module:openmct.AnnotationAPI} | ||||
|          * @memberof module:openmct.MCT# | ||||
|          * @name annotation | ||||
|          */ | ||||
|         this.annotation = new api.AnnotationAPI(this); | ||||
|             ['branding', () => BrandingAPI.default], | ||||
|  | ||||
|             /** | ||||
|              * MCT's annotation API that enables | ||||
|              * human-created comments and categorization linked to data products | ||||
|              * @type {module:openmct.AnnotationAPI} | ||||
|              * @memberof module:openmct.MCT# | ||||
|              * @name annotation | ||||
|              */ | ||||
|             ['annotation', () => new api.AnnotationAPI(this)] | ||||
|         ].forEach(apiEntry => { | ||||
|             const apiName = apiEntry[0]; | ||||
|             const apiObject = apiEntry[1](); | ||||
|  | ||||
|             Object.defineProperty(this, apiName, { | ||||
|                 value: apiObject, | ||||
|                 enumerable: false, | ||||
|                 configurable: false, | ||||
|                 writable: true | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         // Plugins that are installed by default | ||||
|         this.install(this.plugins.Plot()); | ||||
| @@ -280,6 +287,7 @@ define([ | ||||
|         this.install(this.plugins.ObjectInterceptors()); | ||||
|         this.install(this.plugins.DeviceClassifier()); | ||||
|         this.install(this.plugins.UserIndicator()); | ||||
|         this.install(this.plugins.Gauge()); | ||||
|     } | ||||
|  | ||||
|     MCT.prototype = Object.create(EventEmitter.prototype); | ||||
|   | ||||
| @@ -85,8 +85,6 @@ class ActionCollection extends EventEmitter { | ||||
|     } | ||||
|  | ||||
|     destroy() { | ||||
|         super.removeAllListeners(); | ||||
|  | ||||
|         if (!this.skipEnvironmentObservers) { | ||||
|             this.objectUnsubscribes.forEach(unsubscribe => { | ||||
|                 unsubscribe(); | ||||
| @@ -96,6 +94,7 @@ class ActionCollection extends EventEmitter { | ||||
|         } | ||||
|  | ||||
|         this.emit('destroy', this.view); | ||||
|         this.removeAllListeners(); | ||||
|     } | ||||
|  | ||||
|     getVisibleActions() { | ||||
|   | ||||
| @@ -172,17 +172,19 @@ export default class AnnotationAPI extends EventEmitter { | ||||
|                 name: contentText, | ||||
|                 domainObject: targetDomainObject, | ||||
|                 annotationType, | ||||
|                 tags: [], | ||||
|                 tags: [tag], | ||||
|                 contentText, | ||||
|                 targets | ||||
|             }; | ||||
|             existingAnnotation = await this.create(annotationCreationArguments); | ||||
|             const newAnnotation = await this.create(annotationCreationArguments); | ||||
|  | ||||
|             return newAnnotation; | ||||
|         } else { | ||||
|             const tagArray = [tag, ...existingAnnotation.tags]; | ||||
|             this.openmct.objects.mutate(existingAnnotation, 'tags', tagArray); | ||||
|  | ||||
|             return existingAnnotation; | ||||
|         } | ||||
|  | ||||
|         const tagArray = [tag, ...existingAnnotation.tags]; | ||||
|         this.openmct.objects.mutate(existingAnnotation, 'tags', tagArray); | ||||
|  | ||||
|         return existingAnnotation; | ||||
|     } | ||||
|  | ||||
|     removeAnnotationTag(existingAnnotation, tagToRemove) { | ||||
|   | ||||
| @@ -24,6 +24,7 @@ define([ | ||||
|     './actions/ActionsAPI', | ||||
|     './composition/CompositionAPI', | ||||
|     './Editor', | ||||
|     './faultmanagement/FaultManagementAPI', | ||||
|     './forms/FormsAPI', | ||||
|     './indicators/IndicatorAPI', | ||||
|     './menu/MenuAPI', | ||||
| @@ -40,6 +41,7 @@ define([ | ||||
|     ActionsAPI, | ||||
|     CompositionAPI, | ||||
|     EditorAPI, | ||||
|     FaultManagementAPI, | ||||
|     FormsAPI, | ||||
|     IndicatorAPI, | ||||
|     MenuAPI, | ||||
| @@ -57,6 +59,7 @@ define([ | ||||
|         ActionsAPI: ActionsAPI.default, | ||||
|         CompositionAPI: CompositionAPI, | ||||
|         EditorAPI: EditorAPI, | ||||
|         FaultManagementAPI: FaultManagementAPI, | ||||
|         FormsAPI: FormsAPI, | ||||
|         IndicatorAPI: IndicatorAPI.default, | ||||
|         MenuAPI: MenuAPI.default, | ||||
|   | ||||
							
								
								
									
										106
									
								
								src/api/faultmanagement/FaultManagementAPI.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								src/api/faultmanagement/FaultManagementAPI.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,106 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| export default class FaultManagementAPI { | ||||
|     constructor(openmct) { | ||||
|         this.openmct = openmct; | ||||
|     } | ||||
|  | ||||
|     addProvider(provider) { | ||||
|         this.provider = provider; | ||||
|     } | ||||
|  | ||||
|     supportsActions() { | ||||
|         return this.provider?.acknowledgeFault !== undefined && this.provider?.shelveFault !== undefined; | ||||
|     } | ||||
|  | ||||
|     request(domainObject) { | ||||
|         if (!this.provider?.supportsRequest(domainObject)) { | ||||
|             return Promise.reject(); | ||||
|         } | ||||
|  | ||||
|         return this.provider.request(domainObject); | ||||
|     } | ||||
|  | ||||
|     subscribe(domainObject, callback) { | ||||
|         if (!this.provider?.supportsSubscribe(domainObject)) { | ||||
|             return Promise.reject(); | ||||
|         } | ||||
|  | ||||
|         return this.provider.subscribe(domainObject, callback); | ||||
|     } | ||||
|  | ||||
|     acknowledgeFault(fault, ackData) { | ||||
|         return this.provider.acknowledgeFault(fault, ackData); | ||||
|     } | ||||
|  | ||||
|     shelveFault(fault, shelveData) { | ||||
|         return this.provider.shelveFault(fault, shelveData); | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** @typedef {object} Fault | ||||
|  * @property {string} type | ||||
|  * @property {object} fault | ||||
|  * @property {boolean} fault.acknowledged | ||||
|  * @property {object} fault.currentValueInfo | ||||
|  * @property {number} fault.currentValueInfo.value | ||||
|  * @property {string} fault.currentValueInfo.rangeCondition | ||||
|  * @property {string} fault.currentValueInfo.monitoringResult | ||||
|  * @property {string} fault.id | ||||
|  * @property {string} fault.name | ||||
|  * @property {string} fault.namespace | ||||
|  * @property {number} fault.seqNum | ||||
|  * @property {string} fault.severity | ||||
|  * @property {boolean} fault.shelved | ||||
|  * @property {string} fault.shortDescription | ||||
|  * @property {string} fault.triggerTime | ||||
|  * @property {object} fault.triggerValueInfo | ||||
|  * @property {number} fault.triggerValueInfo.value | ||||
|  * @property {string} fault.triggerValueInfo.rangeCondition | ||||
|  * @property {string} fault.triggerValueInfo.monitoringResult | ||||
|  * @example | ||||
|  *  { | ||||
|  *     "type": "", | ||||
|  *     "fault": { | ||||
|  *         "acknowledged": true, | ||||
|  *         "currentValueInfo": { | ||||
|  *             "value": 0, | ||||
|  *             "rangeCondition": "", | ||||
|  *             "monitoringResult": "" | ||||
|  *         }, | ||||
|  *         "id": "", | ||||
|  *         "name": "", | ||||
|  *         "namespace": "", | ||||
|  *         "seqNum": 0, | ||||
|  *         "severity": "", | ||||
|  *         "shelved": true, | ||||
|  *         "shortDescription": "", | ||||
|  *         "triggerTime": "", | ||||
|  *         "triggerValueInfo": { | ||||
|  *             "value": 0, | ||||
|  *             "rangeCondition": "", | ||||
|  *             "monitoringResult": "" | ||||
|  *         } | ||||
|  *     } | ||||
|  * } | ||||
|  */ | ||||
							
								
								
									
										144
									
								
								src/api/faultmanagement/FaultManagementAPISpec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								src/api/faultmanagement/FaultManagementAPISpec.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,144 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * License); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an AS IS BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import { | ||||
|     createOpenMct, | ||||
|     resetApplicationState | ||||
| } from '../../utils/testing'; | ||||
|  | ||||
| const faultName = 'super duper fault'; | ||||
| const aFault = { | ||||
|     type: '', | ||||
|     fault: { | ||||
|         acknowledged: true, | ||||
|         currentValueInfo: { | ||||
|             value: 0, | ||||
|             rangeCondition: '', | ||||
|             monitoringResult: '' | ||||
|         }, | ||||
|         id: '', | ||||
|         name: faultName, | ||||
|         namespace: '', | ||||
|         seqNum: 0, | ||||
|         severity: '', | ||||
|         shelved: true, | ||||
|         shortDescription: '', | ||||
|         triggerTime: '', | ||||
|         triggerValueInfo: { | ||||
|             value: 0, | ||||
|             rangeCondition: '', | ||||
|             monitoringResult: '' | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| const faultDomainObject = { | ||||
|     name: 'it is not your fault', | ||||
|     type: 'faultManagement', | ||||
|     identifier: { | ||||
|         key: 'nobodies', | ||||
|         namespace: 'fault' | ||||
|     } | ||||
| }; | ||||
| const aComment = 'THIS is my fault.'; | ||||
| const faultManagementProvider = { | ||||
|     request() { | ||||
|         return Promise.resolve([aFault]); | ||||
|     }, | ||||
|     subscribe(domainObject, callback) { | ||||
|         return () => {}; | ||||
|     }, | ||||
|     supportsRequest(domainObject) { | ||||
|         return domainObject.type === 'faultManagement'; | ||||
|     }, | ||||
|     supportsSubscribe(domainObject) { | ||||
|         return domainObject.type === 'faultManagement'; | ||||
|     }, | ||||
|     acknowledgeFault(fault, { comment = '' }) { | ||||
|         return Promise.resolve({ | ||||
|             success: true | ||||
|         }); | ||||
|     }, | ||||
|     shelveFault(fault, shelveData) { | ||||
|         return Promise.resolve({ | ||||
|             success: true | ||||
|         }); | ||||
|     } | ||||
| }; | ||||
|  | ||||
| describe('The Fault Management API', () => { | ||||
|     let openmct; | ||||
|  | ||||
|     beforeEach(() => { | ||||
|         openmct = createOpenMct(); | ||||
|         openmct.install(openmct.plugins.FaultManagement()); | ||||
|         // openmct.install(openmct.plugins.example.ExampleFaultSource()); | ||||
|         openmct.faults.addProvider(faultManagementProvider); | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|         return resetApplicationState(openmct); | ||||
|     }); | ||||
|  | ||||
|     it('allows you to request a fault', async () => { | ||||
|         spyOn(faultManagementProvider, 'supportsRequest').and.callThrough(); | ||||
|  | ||||
|         let faultResponse = await openmct.faults.request(faultDomainObject); | ||||
|  | ||||
|         expect(faultManagementProvider.supportsRequest).toHaveBeenCalledWith(faultDomainObject); | ||||
|         expect(faultResponse[0].fault.name).toEqual(faultName); | ||||
|     }); | ||||
|  | ||||
|     it('allows you to subscribe to a fault', () => { | ||||
|         spyOn(faultManagementProvider, 'subscribe').and.callThrough(); | ||||
|         spyOn(faultManagementProvider, 'supportsSubscribe').and.callThrough(); | ||||
|  | ||||
|         let unsubscribe = openmct.faults.subscribe(faultDomainObject, () => {}); | ||||
|  | ||||
|         expect(unsubscribe).toEqual(jasmine.any(Function)); | ||||
|         expect(faultManagementProvider.supportsSubscribe).toHaveBeenCalledWith(faultDomainObject); | ||||
|         expect(faultManagementProvider.subscribe).toHaveBeenCalledOnceWith(faultDomainObject, jasmine.any(Function)); | ||||
|  | ||||
|     }); | ||||
|  | ||||
|     it('will tell you if the fault management provider supports actions', () => { | ||||
|         expect(openmct.faults.supportsActions()).toBeTrue(); | ||||
|     }); | ||||
|  | ||||
|     it('will allow you to acknowledge a fault', async () => { | ||||
|         spyOn(faultManagementProvider, 'acknowledgeFault').and.callThrough(); | ||||
|  | ||||
|         let ackResponse = await openmct.faults.acknowledgeFault(aFault, aComment); | ||||
|  | ||||
|         expect(faultManagementProvider.acknowledgeFault).toHaveBeenCalledWith(aFault, aComment); | ||||
|         expect(ackResponse.success).toBeTrue(); | ||||
|     }); | ||||
|  | ||||
|     it('will allow you to shelve a fault', async () => { | ||||
|         spyOn(faultManagementProvider, 'shelveFault').and.callThrough(); | ||||
|  | ||||
|         let shelveResponse = await openmct.faults.shelveFault(aFault, aComment); | ||||
|  | ||||
|         expect(faultManagementProvider.shelveFault).toHaveBeenCalledWith(aFault, aComment); | ||||
|         expect(shelveResponse.success).toBeTrue(); | ||||
|     }); | ||||
|  | ||||
| }); | ||||
| @@ -60,6 +60,7 @@ | ||||
|             tabindex="0" | ||||
|             :disabled="isInvalid" | ||||
|             class="c-button c-button--major" | ||||
|             aria-label="Save" | ||||
|             @click="onSave" | ||||
|         > | ||||
|             {{ submitLabel }} | ||||
| @@ -67,6 +68,7 @@ | ||||
|         <button | ||||
|             tabindex="0" | ||||
|             class="c-button js-cancel-button" | ||||
|             aria-label="Cancel" | ||||
|             @click="onDismiss" | ||||
|         > | ||||
|             {{ cancelLabel }} | ||||
|   | ||||
| @@ -44,6 +44,7 @@ | ||||
|     <div | ||||
|         v-if="!hideOptions" | ||||
|         class="c-menu c-input--autocomplete__options" | ||||
|         aria-label="Autocomplete Options" | ||||
|         @blur="hideOptions = true" | ||||
|     > | ||||
|         <ul> | ||||
|   | ||||
| @@ -28,6 +28,7 @@ | ||||
|     > | ||||
|         <input | ||||
|             v-model="field" | ||||
|             :aria-label="model.name" | ||||
|             type="number" | ||||
|             :min="model.min" | ||||
|             :max="model.max" | ||||
|   | ||||
| @@ -36,7 +36,7 @@ | ||||
|         <li | ||||
|             v-for="action in options.actions" | ||||
|             :key="action.name" | ||||
|             :class="action.cssClass" | ||||
|             :class="[action.cssClass, action.isDisabled ? 'disabled' : '']" | ||||
|             :title="action.description" | ||||
|             :data-testid="action.testId || false" | ||||
|             @click="action.onItemClicked" | ||||
|   | ||||
| @@ -224,7 +224,8 @@ class InMemorySearchProvider { | ||||
|  | ||||
|     /** | ||||
|      * Schedule an id to be indexed at a later date.  If there are less | ||||
|      * pending requests then allowed, will kick off an indexing request. | ||||
|      * 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. | ||||
|      * | ||||
|      * @private | ||||
|      * @param {identifier} id to be indexed. | ||||
| @@ -258,8 +259,12 @@ class InMemorySearchProvider { | ||||
|     } | ||||
|  | ||||
|     onAnnotationCreation(annotationObject) { | ||||
|         const provider = this; | ||||
|         provider.index(annotationObject); | ||||
|  | ||||
|         const objectProvider = this.openmct.objects.getProvider(annotationObject.identifier); | ||||
|         if (objectProvider === undefined || objectProvider.search === undefined) { | ||||
|             const provider = this; | ||||
|             provider.index(annotationObject); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     onNameMutation(domainObject, name) { | ||||
| @@ -270,7 +275,6 @@ class InMemorySearchProvider { | ||||
|     } | ||||
|  | ||||
|     onTagMutation(domainObject, newTags) { | ||||
|         domainObject.oldTags = domainObject.tags; | ||||
|         domainObject.tags = newTags; | ||||
|         const provider = this; | ||||
|  | ||||
| @@ -404,20 +408,16 @@ class InMemorySearchProvider { | ||||
|             } | ||||
|  | ||||
|         }); | ||||
|         // remove old tags | ||||
|         if (model.oldTags) { | ||||
|             model.oldTags.forEach(tagIDToRemove => { | ||||
|                 const existsInNewModel = model.tags.includes(tagIDToRemove); | ||||
|                 if (!existsInNewModel && this.localIndexedAnnotationsByTag[tagIDToRemove]) { | ||||
|                     this.localIndexedAnnotationsByTag[tagIDToRemove] = this.localIndexedAnnotationsByTag[tagIDToRemove]. | ||||
|                         filter(annotationToRemove => { | ||||
|                             const shouldKeep = annotationToRemove.keyString !== keyString; | ||||
|         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; | ||||
|                         }); | ||||
|                 } | ||||
|                 return shouldKeep; | ||||
|             }); | ||||
|         } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     localIndexAnnotation(objectToIndex, model) { | ||||
| @@ -449,7 +449,7 @@ class InMemorySearchProvider { | ||||
|             keyString | ||||
|         }; | ||||
|         if (model && (model.type === 'annotation')) { | ||||
|             if (model.targets && model.targets) { | ||||
|             if (model.targets) { | ||||
|                 this.localIndexAnnotation(objectToIndex, model); | ||||
|             } | ||||
|  | ||||
|   | ||||
| @@ -94,19 +94,16 @@ | ||||
|  | ||||
|         }); | ||||
|         // remove old tags | ||||
|         if (model.oldTags) { | ||||
|             model.oldTags.forEach(tagIDToRemove => { | ||||
|                 const existsInNewModel = model.tags.includes(tagIDToRemove); | ||||
|                 if (!existsInNewModel && indexedAnnotationsByTag[tagIDToRemove]) { | ||||
|                     indexedAnnotationsByTag[tagIDToRemove] = indexedAnnotationsByTag[tagIDToRemove]. | ||||
|                         filter(annotationToRemove => { | ||||
|                             const shouldKeep = annotationToRemove.keyString !== keyString; | ||||
|         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; | ||||
|                         }); | ||||
|                 } | ||||
|                 return shouldKeep; | ||||
|             }); | ||||
|         } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     function indexItem(keyString, model) { | ||||
| @@ -116,7 +113,7 @@ | ||||
|             keyString | ||||
|         }; | ||||
|         if (model && (model.type === 'annotation')) { | ||||
|             if (model.targets && model.targets) { | ||||
|             if (model.targets) { | ||||
|                 indexAnnotation(objectToIndex, model); | ||||
|             } | ||||
|  | ||||
|   | ||||
| @@ -52,8 +52,8 @@ class MutableDomainObject { | ||||
|                 // Property should not be serialized | ||||
|                 enumerable: false | ||||
|             }, | ||||
|             _observers: { | ||||
|                 value: [], | ||||
|             _callbacksForPaths: { | ||||
|                 value: {}, | ||||
|                 // Property should not be serialized | ||||
|                 enumerable: false | ||||
|             }, | ||||
| @@ -64,15 +64,31 @@ class MutableDomainObject { | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|     /** | ||||
|      * BRAND new approach | ||||
|      * - Register a listener on $_synchronize_model | ||||
|      * - The $_synchronize_model event provides the path. Figure out whether the mutated path is equal to, or a parent of the observed path. | ||||
|      * - If so, trigger callback with new value | ||||
|      * - As an optimization, ONLY trigger if value has actually changed. Could be deferred until later? | ||||
|      */ | ||||
|  | ||||
|     $observe(path, callback) { | ||||
|         let fullPath = qualifiedEventName(this, path); | ||||
|         let eventOff = | ||||
|             this._globalEventEmitter.off.bind(this._globalEventEmitter, fullPath, callback); | ||||
|         let callbacksForPath = this._callbacksForPaths[path]; | ||||
|         if (callbacksForPath === undefined) { | ||||
|             callbacksForPath = []; | ||||
|             this._callbacksForPaths[path] = callbacksForPath; | ||||
|         } | ||||
|  | ||||
|         this._globalEventEmitter.on(fullPath, callback); | ||||
|         this._observers.push(eventOff); | ||||
|         callbacksForPath.push(callback); | ||||
|  | ||||
|         return function unlisten() { | ||||
|             let index = callbacksForPath.indexOf(callback); | ||||
|             callbacksForPath.splice(index, 1); | ||||
|             if (callbacksForPath.length === 0) { | ||||
|                 delete this._callbacksForPaths[path]; | ||||
|             } | ||||
|         }.bind(this); | ||||
|  | ||||
|         return eventOff; | ||||
|     } | ||||
|     $set(path, value) { | ||||
|         _.set(this, path, value); | ||||
| @@ -88,25 +104,14 @@ class MutableDomainObject { | ||||
|         this._globalEventEmitter.emit(ANY_OBJECT_EVENT, this); | ||||
|         //Emit wildcard event, with path so that callback knows what changed | ||||
|         this._globalEventEmitter.emit(qualifiedEventName(this, '*'), this, path, value); | ||||
|  | ||||
|         //Emit events specific to properties affected | ||||
|         let parentPropertiesList = path.split('.'); | ||||
|         for (let index = parentPropertiesList.length; index > 0; index--) { | ||||
|             let parentPropertyPath = parentPropertiesList.slice(0, index).join('.'); | ||||
|             this._globalEventEmitter.emit(qualifiedEventName(this, parentPropertyPath), _.get(this, parentPropertyPath)); | ||||
|         } | ||||
|  | ||||
|         //TODO: Emit events for listeners of child properties when parent changes. | ||||
|         // Do it at observer time - also register observers for parent attribute path. | ||||
|     } | ||||
|  | ||||
|     $refresh(model) { | ||||
|         //TODO: Currently we are updating the entire object. | ||||
|         // In the future we could update a specific property of the object using the 'path' parameter. | ||||
|         const clone = JSON.parse(JSON.stringify(this)); | ||||
|         this._globalEventEmitter.emit(qualifiedEventName(this, '$_synchronize_model'), model); | ||||
|  | ||||
|         //Emit wildcard event, with path so that callback knows what changed | ||||
|         this._globalEventEmitter.emit(qualifiedEventName(this, '*'), this); | ||||
|         //Emit wildcard event | ||||
|         this._globalEventEmitter.emit(qualifiedEventName(this, '*'), this, '*', this, clone); | ||||
|     } | ||||
|  | ||||
|     $on(event, callback) { | ||||
| @@ -114,23 +119,53 @@ class MutableDomainObject { | ||||
|  | ||||
|         return () => this._instanceEventEmitter.off(event, callback); | ||||
|     } | ||||
|     $destroy() { | ||||
|         while (this._observers.length > 0) { | ||||
|             const observer = this._observers.pop(); | ||||
|             observer(); | ||||
|         } | ||||
|     $updateListenersOnPath(updatedModel, mutatedPath, newValue, oldModel) { | ||||
|         const isRefresh = mutatedPath === '*'; | ||||
|  | ||||
|         Object.entries(this._callbacksForPaths).forEach(([observedPath, callbacks]) => { | ||||
|             if (isChildOf(observedPath, mutatedPath) | ||||
|                 || isParentOf(observedPath, mutatedPath)) { | ||||
|                 let newValueOfObservedPath; | ||||
|  | ||||
|                 if (observedPath === '*') { | ||||
|                     newValueOfObservedPath = updatedModel; | ||||
|  | ||||
|                 } else { | ||||
|                     newValueOfObservedPath = _.get(updatedModel, observedPath); | ||||
|                 } | ||||
|  | ||||
|                 if (isRefresh && observedPath !== '*') { | ||||
|                     const oldValueOfObservedPath = _.get(oldModel, observedPath); | ||||
|                     if (!_.isEqual(newValueOfObservedPath, oldValueOfObservedPath)) { | ||||
|                         callbacks.forEach(callback => callback(newValueOfObservedPath)); | ||||
|                     } | ||||
|                 } else { | ||||
|                     //Assumed to be different if result of mutation. | ||||
|                     callbacks.forEach(callback => callback(newValueOfObservedPath)); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|     $synchronizeModel(updatedObject) { | ||||
|         let clone = JSON.parse(JSON.stringify(updatedObject)); | ||||
|         utils.refresh(this, clone); | ||||
|     } | ||||
|     $destroy() { | ||||
|         Object.keys(this._callbacksForPaths).forEach(key => delete this._callbacksForPaths[key]); | ||||
|         this._instanceEventEmitter.emit('$_destroy'); | ||||
|         this._globalEventEmitter.off(qualifiedEventName(this, '$_synchronize_model'), this.$synchronizeModel); | ||||
|         this._globalEventEmitter.off(qualifiedEventName(this, '*'), this.$updateListenersOnPath); | ||||
|     } | ||||
|  | ||||
|     static createMutable(object, mutationTopic) { | ||||
|         let mutable = Object.create(new MutableDomainObject(mutationTopic)); | ||||
|         Object.assign(mutable, object); | ||||
|  | ||||
|         mutable.$observe('$_synchronize_model', (updatedObject) => { | ||||
|             let clone = JSON.parse(JSON.stringify(updatedObject)); | ||||
|             utils.refresh(mutable, clone); | ||||
|         }); | ||||
|         mutable.$updateListenersOnPath = mutable.$updateListenersOnPath.bind(mutable); | ||||
|         mutable.$synchronizeModel = mutable.$synchronizeModel.bind(mutable); | ||||
|  | ||||
|         mutable._globalEventEmitter.on(qualifiedEventName(mutable, '$_synchronize_model'), mutable.$synchronizeModel); | ||||
|         mutable._globalEventEmitter.on(qualifiedEventName(mutable, '*'), mutable.$updateListenersOnPath); | ||||
|  | ||||
|         return mutable; | ||||
|     } | ||||
| @@ -147,4 +182,12 @@ function qualifiedEventName(object, eventName) { | ||||
|     return [keystring, eventName].join(':'); | ||||
| } | ||||
|  | ||||
| function isChildOf(observedPath, mutatedPath) { | ||||
|     return Boolean(mutatedPath === '*' || observedPath?.startsWith(mutatedPath)); | ||||
| } | ||||
|  | ||||
| function isParentOf(observedPath, mutatedPath) { | ||||
|     return Boolean(observedPath === '*' || mutatedPath?.startsWith(observedPath)); | ||||
| } | ||||
|  | ||||
| export default MutableDomainObject; | ||||
|   | ||||
| @@ -233,7 +233,11 @@ export default class ObjectAPI { | ||||
|  | ||||
|             delete this.cache[keystring]; | ||||
|  | ||||
|             result = this.applyGetInterceptors(identifier); | ||||
|             if (!result) { | ||||
|                 //no result means resource either doesn't exist or is missing | ||||
|                 //otherwise it's an error, and we shouldn't apply interceptors | ||||
|                 result = this.applyGetInterceptors(identifier); | ||||
|             } | ||||
|  | ||||
|             return result; | ||||
|         }); | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import ObjectAPI from './ObjectAPI.js'; | ||||
| import { createOpenMct, resetApplicationState } from '../../utils/testing'; | ||||
|  | ||||
| describe("The Object API", () => { | ||||
| fdescribe("The Object API", () => { | ||||
|     let objectAPI; | ||||
|     let typeRegistry; | ||||
|     let openmct = {}; | ||||
| @@ -287,53 +287,167 @@ describe("The Object API", () => { | ||||
|                 mutableSecondInstance.$destroy(); | ||||
|             }); | ||||
|  | ||||
|             it('to stay synchronized when mutated', function () { | ||||
|                 objectAPI.mutate(mutable, 'otherAttribute', 'new-attribute-value'); | ||||
|                 expect(mutableSecondInstance.otherAttribute).toBe('new-attribute-value'); | ||||
|             }); | ||||
|  | ||||
|             it('to indicate when a property changes', function () { | ||||
|                 let mutationCallback = jasmine.createSpy('mutation-callback'); | ||||
|                 let unlisten; | ||||
|  | ||||
|                 return new Promise(function (resolve) { | ||||
|                     mutationCallback.and.callFake(resolve); | ||||
|                     unlisten = objectAPI.observe(mutableSecondInstance, 'otherAttribute', mutationCallback); | ||||
|                     objectAPI.mutate(mutable, 'otherAttribute', 'some-new-value'); | ||||
|                 }).then(function () { | ||||
|                     expect(mutationCallback).toHaveBeenCalledWith('some-new-value'); | ||||
|                     unlisten(); | ||||
|             describe('on mutation', () => { | ||||
|                 it('to stay synchronized', function () { | ||||
|                     objectAPI.mutate(mutable, 'otherAttribute', 'new-attribute-value'); | ||||
|                     expect(mutableSecondInstance.otherAttribute).toBe('new-attribute-value'); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             it('to indicate when a child property has changed', function () { | ||||
|                 let embeddedKeyCallback = jasmine.createSpy('embeddedKeyCallback'); | ||||
|                 let embeddedObjectCallback = jasmine.createSpy('embeddedObjectCallback'); | ||||
|                 let objectAttributeCallback = jasmine.createSpy('objectAttribute'); | ||||
|                 let listeners = []; | ||||
|                 it('to indicate when a property changes', function () { | ||||
|                     let mutationCallback = jasmine.createSpy('mutation-callback'); | ||||
|                     let unlisten; | ||||
|  | ||||
|                 return new Promise(function (resolve) { | ||||
|                     objectAttributeCallback.and.callFake(resolve); | ||||
|  | ||||
|                     listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject.embeddedKey', embeddedKeyCallback)); | ||||
|                     listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject', embeddedObjectCallback)); | ||||
|                     listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute', objectAttributeCallback)); | ||||
|  | ||||
|                     objectAPI.mutate(mutable, 'objectAttribute.embeddedObject.embeddedKey', 'updated-embedded-value'); | ||||
|                 }).then(function () { | ||||
|                     expect(embeddedKeyCallback).toHaveBeenCalledWith('updated-embedded-value'); | ||||
|                     expect(embeddedObjectCallback).toHaveBeenCalledWith({ | ||||
|                         embeddedKey: 'updated-embedded-value' | ||||
|                     return new Promise(function (resolve) { | ||||
|                         mutationCallback.and.callFake(resolve); | ||||
|                         unlisten = objectAPI.observe(mutableSecondInstance, 'otherAttribute', mutationCallback); | ||||
|                         objectAPI.mutate(mutable, 'otherAttribute', 'some-new-value'); | ||||
|                     }).then(function () { | ||||
|                         expect(mutationCallback).toHaveBeenCalledWith('some-new-value'); | ||||
|                         unlisten(); | ||||
|                     }); | ||||
|                     expect(objectAttributeCallback).toHaveBeenCalledWith({ | ||||
|                         embeddedObject: { | ||||
|                 }); | ||||
|  | ||||
|                 it('to indicate when a child property has changed', function () { | ||||
|                     let embeddedKeyCallback = jasmine.createSpy('embeddedKeyCallback'); | ||||
|                     let embeddedObjectCallback = jasmine.createSpy('embeddedObjectCallback'); | ||||
|                     let objectAttributeCallback = jasmine.createSpy('objectAttribute'); | ||||
|                     let listeners = []; | ||||
|  | ||||
|                     return new Promise(function (resolve) { | ||||
|                         objectAttributeCallback.and.callFake(resolve); | ||||
|  | ||||
|                         listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject.embeddedKey', embeddedKeyCallback)); | ||||
|                         listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject', embeddedObjectCallback)); | ||||
|                         listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute', objectAttributeCallback)); | ||||
|  | ||||
|                         objectAPI.mutate(mutable, 'objectAttribute.embeddedObject.embeddedKey', 'updated-embedded-value'); | ||||
|                     }).then(function () { | ||||
|                         expect(embeddedKeyCallback).toHaveBeenCalledWith('updated-embedded-value'); | ||||
|                         expect(embeddedObjectCallback).toHaveBeenCalledWith({ | ||||
|                             embeddedKey: 'updated-embedded-value' | ||||
|                         } | ||||
|                     }); | ||||
|                         }); | ||||
|                         expect(objectAttributeCallback).toHaveBeenCalledWith({ | ||||
|                             embeddedObject: { | ||||
|                                 embeddedKey: 'updated-embedded-value' | ||||
|                             } | ||||
|                         }); | ||||
|  | ||||
|                     listeners.forEach(listener => listener()); | ||||
|                         listeners.forEach(listener => listener()); | ||||
|                     }); | ||||
|                 }); | ||||
|  | ||||
|                 it('to indicate when a parent property has changed', function () { | ||||
|                     let embeddedKeyCallback = jasmine.createSpy('embeddedKeyCallback'); | ||||
|                     let embeddedObjectCallback = jasmine.createSpy('embeddedObjectCallback'); | ||||
|                     let objectAttributeCallback = jasmine.createSpy('objectAttribute'); | ||||
|                     let listeners = []; | ||||
|  | ||||
|                     return new Promise(function (resolve) { | ||||
|                         objectAttributeCallback.and.callFake(resolve); | ||||
|  | ||||
|                         listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject.embeddedKey', embeddedKeyCallback)); | ||||
|                         listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject', embeddedObjectCallback)); | ||||
|                         listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute', objectAttributeCallback)); | ||||
|  | ||||
|                         objectAPI.mutate(mutable, 'objectAttribute.embeddedObject', 'updated-embedded-value'); | ||||
|                     }).then(function () { | ||||
|                         expect(embeddedKeyCallback).toHaveBeenCalledWith(undefined); | ||||
|                         expect(embeddedObjectCallback).toHaveBeenCalledWith('updated-embedded-value'); | ||||
|                         expect(objectAttributeCallback).toHaveBeenCalledWith({ | ||||
|                             embeddedObject: 'updated-embedded-value' | ||||
|                         }); | ||||
|  | ||||
|                         listeners.forEach(listener => listener()); | ||||
|                     }); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             describe('on refresh', () => { | ||||
|                 let refreshModel; | ||||
|  | ||||
|                 beforeEach(() => { | ||||
|                     refreshModel = JSON.parse(JSON.stringify(mutable)); | ||||
|                 }); | ||||
|  | ||||
|                 it('to stay synchronized', function () { | ||||
|                     refreshModel.otherAttribute = 'new-attribute-value'; | ||||
|                     mutable.$refresh(refreshModel); | ||||
|                     expect(mutableSecondInstance.otherAttribute).toBe('new-attribute-value'); | ||||
|                 }); | ||||
|  | ||||
|                 it('to indicate when a property changes', function () { | ||||
|                     let mutationCallback = jasmine.createSpy('mutation-callback'); | ||||
|                     let unlisten; | ||||
|  | ||||
|                     return new Promise(function (resolve) { | ||||
|                         mutationCallback.and.callFake(resolve); | ||||
|                         unlisten = objectAPI.observe(mutableSecondInstance, 'otherAttribute', mutationCallback); | ||||
|                         refreshModel.otherAttribute = 'some-new-value'; | ||||
|                         mutable.$refresh(refreshModel); | ||||
|                     }).then(function () { | ||||
|                         expect(mutationCallback).toHaveBeenCalledWith('some-new-value'); | ||||
|                         unlisten(); | ||||
|                     }); | ||||
|                 }); | ||||
|  | ||||
|                 it('to indicate when a child property has changed', function () { | ||||
|                     let embeddedKeyCallback = jasmine.createSpy('embeddedKeyCallback'); | ||||
|                     let embeddedObjectCallback = jasmine.createSpy('embeddedObjectCallback'); | ||||
|                     let objectAttributeCallback = jasmine.createSpy('objectAttribute'); | ||||
|                     let listeners = []; | ||||
|  | ||||
|                     return new Promise(function (resolve) { | ||||
|                         objectAttributeCallback.and.callFake(resolve); | ||||
|  | ||||
|                         listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject.embeddedKey', embeddedKeyCallback)); | ||||
|                         listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject', embeddedObjectCallback)); | ||||
|                         listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute', objectAttributeCallback)); | ||||
|  | ||||
|                         refreshModel.objectAttribute.embeddedObject.embeddedKey = 'updated-embedded-value'; | ||||
|                         mutable.$refresh(refreshModel); | ||||
|                     }).then(function () { | ||||
|                         expect(embeddedKeyCallback).toHaveBeenCalledWith('updated-embedded-value'); | ||||
|                         expect(embeddedObjectCallback).toHaveBeenCalledWith({ | ||||
|                             embeddedKey: 'updated-embedded-value' | ||||
|                         }); | ||||
|                         expect(objectAttributeCallback).toHaveBeenCalledWith({ | ||||
|                             embeddedObject: { | ||||
|                                 embeddedKey: 'updated-embedded-value' | ||||
|                             } | ||||
|                         }); | ||||
|  | ||||
|                         listeners.forEach(listener => listener()); | ||||
|                     }); | ||||
|                 }); | ||||
|  | ||||
|                 it('to indicate when a parent property has changed', function () { | ||||
|                     let embeddedKeyCallback = jasmine.createSpy('embeddedKeyCallback'); | ||||
|                     let embeddedObjectCallback = jasmine.createSpy('embeddedObjectCallback'); | ||||
|                     let objectAttributeCallback = jasmine.createSpy('objectAttribute'); | ||||
|                     let listeners = []; | ||||
|  | ||||
|                     return new Promise(function (resolve) { | ||||
|                         objectAttributeCallback.and.callFake(resolve); | ||||
|  | ||||
|                         listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject.embeddedKey', embeddedKeyCallback)); | ||||
|                         listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject', embeddedObjectCallback)); | ||||
|                         listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute', objectAttributeCallback)); | ||||
|  | ||||
|                         refreshModel.objectAttribute.embeddedObject = 'updated-embedded-value'; | ||||
|  | ||||
|                         mutable.$refresh(refreshModel); | ||||
|                     }).then(function () { | ||||
|                         expect(embeddedKeyCallback).toHaveBeenCalledWith(undefined); | ||||
|                         expect(embeddedObjectCallback).toHaveBeenCalledWith('updated-embedded-value'); | ||||
|                         expect(objectAttributeCallback).toHaveBeenCalledWith({ | ||||
|                             embeddedObject: 'updated-embedded-value' | ||||
|                         }); | ||||
|  | ||||
|                         listeners.forEach(listener => listener()); | ||||
|                     }); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|   | ||||
| @@ -20,122 +20,18 @@ | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| const { TelemetryCollection } = require("./TelemetryCollection"); | ||||
| 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'; | ||||
|  | ||||
| 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~ | ||||
|      */ | ||||
| export default class 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 | ||||
|      */ | ||||
|     function TelemetryAPI(openmct) { | ||||
|     constructor(openmct) { | ||||
|         this.openmct = openmct; | ||||
|  | ||||
|         this.formatMapCache = new WeakMap(); | ||||
| @@ -148,12 +44,14 @@ define([ | ||||
|         this.requestProviders = []; | ||||
|         this.subscriptionProviders = []; | ||||
|         this.valueFormatterCache = new WeakMap(); | ||||
|  | ||||
|         this.requestInterceptorRegistry = new TelemetryRequestInterceptorRegistry(); | ||||
|     } | ||||
|  | ||||
|     TelemetryAPI.prototype.abortAllRequests = function () { | ||||
|     abortAllRequests() { | ||||
|         this.requestAbortControllers.forEach((controller) => controller.abort()); | ||||
|         this.requestAbortControllers.clear(); | ||||
|     }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Return Custom String Formatter | ||||
| @@ -162,9 +60,9 @@ define([ | ||||
|      * @param {string} format custom formatter string (eg: %.4f, <s etc.) | ||||
|      * @returns {CustomStringFormatter} | ||||
|      */ | ||||
|     TelemetryAPI.prototype.customStringFormatter = function (valueMetadata, format) { | ||||
|         return new CustomStringFormatter.default(this.openmct, valueMetadata, format); | ||||
|     }; | ||||
|     customStringFormatter(valueMetadata, format) { | ||||
|         return new CustomStringFormatter(this.openmct, valueMetadata, format); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Return true if the given domainObject is a telemetry object.  A telemetry | ||||
| @@ -174,9 +72,9 @@ define([ | ||||
|      * @param {module:openmct.DomainObject} domainObject | ||||
|      * @returns {boolean} true if the object is a telemetry object. | ||||
|      */ | ||||
|     TelemetryAPI.prototype.isTelemetryObject = function (domainObject) { | ||||
|     isTelemetryObject(domainObject) { | ||||
|         return Boolean(this.findMetadataProvider(domainObject)); | ||||
|     }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Check if this provider can supply telemetry data associated with | ||||
| @@ -188,10 +86,10 @@ define([ | ||||
|      * @returns {boolean} true if telemetry can be provided | ||||
|      * @memberof module:openmct.TelemetryAPI~TelemetryProvider# | ||||
|      */ | ||||
|     TelemetryAPI.prototype.canProvideTelemetry = function (domainObject) { | ||||
|     canProvideTelemetry(domainObject) { | ||||
|         return Boolean(this.findSubscriptionProvider(domainObject)) | ||||
|                || Boolean(this.findRequestProvider(domainObject)); | ||||
|     }; | ||||
|                 || Boolean(this.findRequestProvider(domainObject)); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Register a telemetry provider with the telemetry service. This | ||||
| @@ -201,7 +99,7 @@ define([ | ||||
|      * @param {module:openmct.TelemetryAPI~TelemetryProvider} provider the new | ||||
|      *        telemetry provider | ||||
|      */ | ||||
|     TelemetryAPI.prototype.addProvider = function (provider) { | ||||
|     addProvider(provider) { | ||||
|         if (provider.supportsRequest) { | ||||
|             this.requestProviders.unshift(provider); | ||||
|         } | ||||
| @@ -217,54 +115,54 @@ define([ | ||||
|         if (provider.supportsLimits) { | ||||
|             this.limitProviders.unshift(provider); | ||||
|         } | ||||
|     }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @private | ||||
|      */ | ||||
|     TelemetryAPI.prototype.findSubscriptionProvider = function () { | ||||
|     findSubscriptionProvider() { | ||||
|         const args = Array.prototype.slice.apply(arguments); | ||||
|         function supportsDomainObject(provider) { | ||||
|             return provider.supportsSubscribe.apply(provider, args); | ||||
|         } | ||||
|  | ||||
|         return this.subscriptionProviders.filter(supportsDomainObject)[0]; | ||||
|     }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @private | ||||
|      */ | ||||
|     TelemetryAPI.prototype.findRequestProvider = function (domainObject) { | ||||
|     findRequestProvider(domainObject) { | ||||
|         const args = Array.prototype.slice.apply(arguments); | ||||
|         function supportsDomainObject(provider) { | ||||
|             return provider.supportsRequest.apply(provider, args); | ||||
|         } | ||||
|  | ||||
|         return this.requestProviders.filter(supportsDomainObject)[0]; | ||||
|     }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @private | ||||
|      */ | ||||
|     TelemetryAPI.prototype.findMetadataProvider = function (domainObject) { | ||||
|     findMetadataProvider(domainObject) { | ||||
|         return this.metadataProviders.filter(function (p) { | ||||
|             return p.supportsMetadata(domainObject); | ||||
|         })[0]; | ||||
|     }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @private | ||||
|      */ | ||||
|     TelemetryAPI.prototype.findLimitEvaluator = function (domainObject) { | ||||
|     findLimitEvaluator(domainObject) { | ||||
|         return this.limitProviders.filter(function (p) { | ||||
|             return p.supportsLimits(domainObject); | ||||
|         })[0]; | ||||
|     }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @private | ||||
|      */ | ||||
|     TelemetryAPI.prototype.standardizeRequestOptions = function (options) { | ||||
|     standardizeRequestOptions(options) { | ||||
|         if (!Object.prototype.hasOwnProperty.call(options, 'start')) { | ||||
|             options.start = this.openmct.time.bounds().start; | ||||
|         } | ||||
| @@ -276,7 +174,47 @@ define([ | ||||
|         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. | ||||
| @@ -292,13 +230,13 @@ define([ | ||||
|      *        options for this telemetry collection request | ||||
|      * @returns {TelemetryCollection} a TelemetryCollection instance | ||||
|      */ | ||||
|     TelemetryAPI.prototype.requestCollection = function (domainObject, options = {}) { | ||||
|     requestCollection(domainObject, options = {}) { | ||||
|         return new TelemetryCollection( | ||||
|             this.openmct, | ||||
|             domainObject, | ||||
|             options | ||||
|         ); | ||||
|     }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Request historical telemetry for a domain object. | ||||
| @@ -315,7 +253,7 @@ define([ | ||||
|      * @returns {Promise.<object[]>} a promise for an array of | ||||
|      *          telemetry data | ||||
|      */ | ||||
|     TelemetryAPI.prototype.request = function (domainObject) { | ||||
|     async request(domainObject) { | ||||
|         if (this.noRequestProviderForAllObjects) { | ||||
|             return Promise.resolve([]); | ||||
|         } | ||||
| @@ -330,6 +268,7 @@ define([ | ||||
|         this.requestAbortControllers.add(abortController); | ||||
|  | ||||
|         this.standardizeRequestOptions(arguments[1]); | ||||
|  | ||||
|         const provider = this.findRequestProvider.apply(this, arguments); | ||||
|         if (!provider) { | ||||
|             this.requestAbortControllers.delete(abortController); | ||||
| @@ -337,6 +276,8 @@ define([ | ||||
|             return this.handleMissingRequestProvider(domainObject); | ||||
|         } | ||||
|  | ||||
|         arguments[1] = await this.applyRequestInterceptors(domainObject, arguments[1]); | ||||
|  | ||||
|         return provider.request.apply(provider, arguments) | ||||
|             .catch((rejected) => { | ||||
|                 if (rejected.name !== 'AbortError') { | ||||
| @@ -348,7 +289,7 @@ define([ | ||||
|             }).finally(() => { | ||||
|                 this.requestAbortControllers.delete(abortController); | ||||
|             }); | ||||
|     }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Subscribe to realtime telemetry for a specific domain object. | ||||
| @@ -364,7 +305,7 @@ define([ | ||||
|      * @returns {Function} a function which may be called to terminate | ||||
|      *          the subscription | ||||
|      */ | ||||
|     TelemetryAPI.prototype.subscribe = function (domainObject, callback, options) { | ||||
|     subscribe(domainObject, callback, options) { | ||||
|         const provider = this.findSubscriptionProvider(domainObject); | ||||
|  | ||||
|         if (!this.subscribeCache) { | ||||
| @@ -401,7 +342,7 @@ define([ | ||||
|                 delete this.subscribeCache[keyString]; | ||||
|             } | ||||
|         }.bind(this); | ||||
|     }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get telemetry metadata for a given domain object.  Returns a telemetry | ||||
| @@ -410,7 +351,7 @@ define([ | ||||
|      * | ||||
|      * @returns {TelemetryMetadataManager} | ||||
|      */ | ||||
|     TelemetryAPI.prototype.getMetadata = function (domainObject) { | ||||
|     getMetadata(domainObject) { | ||||
|         if (!this.metadataCache.has(domainObject)) { | ||||
|             const metadataProvider = this.findMetadataProvider(domainObject); | ||||
|             if (!metadataProvider) { | ||||
| @@ -426,14 +367,14 @@ define([ | ||||
|         } | ||||
|  | ||||
|         return this.metadataCache.get(domainObject); | ||||
|     }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Return an array of valueMetadatas that are common to all supplied | ||||
|      * telemetry objects and match the requested hints. | ||||
|      * | ||||
|      */ | ||||
|     TelemetryAPI.prototype.commonValuesForHints = function (metadatas, hints) { | ||||
|     commonValuesForHints(metadatas, hints) { | ||||
|         const options = metadatas.map(function (metadata) { | ||||
|             const values = metadata.valuesForHints(hints); | ||||
|  | ||||
| @@ -453,14 +394,14 @@ define([ | ||||
|         }); | ||||
|  | ||||
|         return _.sortBy(options, sortKeys); | ||||
|     }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get a value formatter for a given valueMetadata. | ||||
|      * | ||||
|      * @returns {TelemetryValueFormatter} | ||||
|      */ | ||||
|     TelemetryAPI.prototype.getValueFormatter = function (valueMetadata) { | ||||
|     getValueFormatter(valueMetadata) { | ||||
|         if (!this.valueFormatterCache.has(valueMetadata)) { | ||||
|             this.valueFormatterCache.set( | ||||
|                 valueMetadata, | ||||
| @@ -469,7 +410,7 @@ define([ | ||||
|         } | ||||
|  | ||||
|         return this.valueFormatterCache.get(valueMetadata); | ||||
|     }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get a value formatter for a given key. | ||||
| @@ -477,9 +418,9 @@ define([ | ||||
|      * | ||||
|      * @returns {Format} | ||||
|      */ | ||||
|     TelemetryAPI.prototype.getFormatter = function (key) { | ||||
|     getFormatter(key) { | ||||
|         return this.formatters.get(key); | ||||
|     }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get a format map of all value formatters for a given piece of telemetry | ||||
| @@ -487,7 +428,7 @@ define([ | ||||
|      * | ||||
|      * @returns {Object<String, {TelemetryValueFormatter}>} | ||||
|      */ | ||||
|     TelemetryAPI.prototype.getFormatMap = function (metadata) { | ||||
|     getFormatMap(metadata) { | ||||
|         if (!metadata) { | ||||
|             return {}; | ||||
|         } | ||||
| @@ -502,14 +443,14 @@ define([ | ||||
|         } | ||||
|  | ||||
|         return this.formatMapCache.get(metadata); | ||||
|     }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Error Handling: Missing Request provider | ||||
|      * | ||||
|      * @returns Promise | ||||
|      */ | ||||
|     TelemetryAPI.prototype.handleMissingRequestProvider = function (domainObject) { | ||||
|     handleMissingRequestProvider(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'; | ||||
| @@ -529,18 +470,18 @@ define([ | ||||
|         } | ||||
|  | ||||
|         this.openmct.notifications.error(message); | ||||
|         console.error(detailMessage); | ||||
|         console.warn(detailMessage); | ||||
|  | ||||
|         return Promise.resolve([]); | ||||
|     }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Register a new telemetry data formatter. | ||||
|      * @param {Format} format the | ||||
|      */ | ||||
|     TelemetryAPI.prototype.addFormat = function (format) { | ||||
|     addFormat(format) { | ||||
|         this.formatters.set(format.key, format); | ||||
|     }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get a limit evaluator for this domain object. | ||||
| @@ -558,9 +499,9 @@ define([ | ||||
|      * @method limitEvaluator | ||||
|      * @memberof module:openmct.TelemetryAPI~TelemetryProvider# | ||||
|      */ | ||||
|     TelemetryAPI.prototype.limitEvaluator = function (domainObject) { | ||||
|     limitEvaluator(domainObject) { | ||||
|         return this.getLimitEvaluator(domainObject); | ||||
|     }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get a limits for this domain object. | ||||
| @@ -578,9 +519,9 @@ define([ | ||||
|      * @method limits | ||||
|      * @memberof module:openmct.TelemetryAPI~TelemetryProvider# | ||||
|      */ | ||||
|     TelemetryAPI.prototype.limitDefinition = function (domainObject) { | ||||
|     limitDefinition(domainObject) { | ||||
|         return this.getLimits(domainObject); | ||||
|     }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get a limit evaluator for this domain object. | ||||
| @@ -598,7 +539,7 @@ define([ | ||||
|      * @method limitEvaluator | ||||
|      * @memberof module:openmct.TelemetryAPI~TelemetryProvider# | ||||
|      */ | ||||
|     TelemetryAPI.prototype.getLimitEvaluator = function (domainObject) { | ||||
|     getLimitEvaluator(domainObject) { | ||||
|         const provider = this.findLimitEvaluator(domainObject); | ||||
|         if (!provider) { | ||||
|             return { | ||||
| @@ -607,7 +548,7 @@ define([ | ||||
|         } | ||||
|  | ||||
|         return provider.getLimitEvaluator(domainObject); | ||||
|     }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get a limit definitions for this domain object. | ||||
| @@ -636,7 +577,7 @@ define([ | ||||
|      *  supported colors are purple, red, orange, yellow and cyan | ||||
|      * @memberof module:openmct.TelemetryAPI~TelemetryProvider# | ||||
|      */ | ||||
|     TelemetryAPI.prototype.getLimits = function (domainObject) { | ||||
|     getLimits(domainObject) { | ||||
|         const provider = this.findLimitEvaluator(domainObject); | ||||
|         if (!provider || !provider.getLimits) { | ||||
|             return { | ||||
| @@ -647,7 +588,104 @@ define([ | ||||
|         } | ||||
|  | ||||
|         return provider.getLimits(domainObject); | ||||
|     }; | ||||
|     } | ||||
| } | ||||
|  | ||||
|     return TelemetryAPI; | ||||
| }); | ||||
| /** | ||||
|  * 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 | ||||
|  */ | ||||
|   | ||||
| @@ -21,7 +21,7 @@ | ||||
|  *****************************************************************************/ | ||||
| import { createOpenMct, resetApplicationState } from 'utils/testing'; | ||||
| import TelemetryAPI from './TelemetryAPI'; | ||||
| const { TelemetryCollection } = require("./TelemetryCollection"); | ||||
| import TelemetryCollection from './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 class TelemetryCollection extends EventEmitter { | ||||
| export default class TelemetryCollection extends EventEmitter { | ||||
|     /** | ||||
|      * Creates a Telemetry Collection | ||||
|      * | ||||
| @@ -49,6 +49,7 @@ export class TelemetryCollection extends EventEmitter { | ||||
|         this.pageState = undefined; | ||||
|         this.lastBounds = undefined; | ||||
|         this.requestAbort = undefined; | ||||
|         this.isStrategyLatest = this.options.strategy === 'latest'; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -126,7 +127,8 @@ export class TelemetryCollection extends EventEmitter { | ||||
|             this.requestAbort = new AbortController(); | ||||
|             options.signal = this.requestAbort.signal; | ||||
|             this.emit('requestStarted'); | ||||
|             historicalData = await historicalProvider.request(this.domainObject, options); | ||||
|             const modifiedOptions = await this.openmct.telemetry.applyRequestInterceptors(this.domainObject, options); | ||||
|             historicalData = await historicalProvider.request(this.domainObject, modifiedOptions); | ||||
|         } catch (error) { | ||||
|             if (error.name !== 'AbortError') { | ||||
|                 console.error('Error requesting telemetry data...'); | ||||
| @@ -168,17 +170,18 @@ export 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; | ||||
| @@ -218,7 +221,17 @@ export class TelemetryCollection extends EventEmitter { | ||||
|         } | ||||
|  | ||||
|         if (added.length) { | ||||
|             this.emit('add', added); | ||||
|             // 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); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -278,13 +291,20 @@ export class TelemetryCollection extends EventEmitter { | ||||
|  | ||||
|             if (startChanged) { | ||||
|                 testDatum[this.timeKey] = bounds.start; | ||||
|                 // 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); | ||||
|  | ||||
|                 // 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 = []; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             if (endChanged) { | ||||
| @@ -296,7 +316,6 @@ export class TelemetryCollection extends EventEmitter { | ||||
|                     datum => this.parseTime(datum) | ||||
|                 ); | ||||
|                 added = this.futureBuffer.splice(0, endIndex); | ||||
|                 this.boundedTelemetry = [...this.boundedTelemetry, ...added]; | ||||
|             } | ||||
|  | ||||
|             if (discarded.length > 0) { | ||||
| @@ -304,6 +323,13 @@ export 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 { | ||||
| @@ -322,7 +348,14 @@ export class TelemetryCollection extends EventEmitter { | ||||
|      * @private | ||||
|      */ | ||||
|     _setTimeSystem(timeSystem) { | ||||
|         let domains = this.metadata.valuesForHints(['domain']); | ||||
|         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 domain = domains.find((d) => d.key === timeSystem.key); | ||||
|  | ||||
|         if (domain !== undefined) { | ||||
| @@ -335,7 +368,6 @@ export 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) => { | ||||
| @@ -356,7 +388,6 @@ export class TelemetryCollection extends EventEmitter { | ||||
|      * @todo handle subscriptions more granually | ||||
|      */ | ||||
|     _reset() { | ||||
|         performance.mark('tlm:reset'); | ||||
|         this.boundedTelemetry = []; | ||||
|         this.futureBuffer = []; | ||||
|  | ||||
|   | ||||
							
								
								
									
										68
									
								
								src/api/telemetry/TelemetryRequestInterceptor.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								src/api/telemetry/TelemetryRequestInterceptor.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,68 @@ | ||||
| /***************************************************************************** | ||||
|  * 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); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
| @@ -168,7 +168,7 @@ export default class StatusAPI extends EventEmitter { | ||||
|      */ | ||||
|     async resetStatusForRole(role) { | ||||
|         const provider = this.#userAPI.getProvider(); | ||||
|         const defaultStatus = await this.getDefaultStatus(); | ||||
|         const defaultStatus = await this.getDefaultStatusForRole(role); | ||||
|  | ||||
|         if (provider.setStatusForRole) { | ||||
|             return provider.setStatusForRole(role, defaultStatus); | ||||
|   | ||||
| @@ -197,7 +197,7 @@ export default { | ||||
|             } | ||||
|         }, | ||||
|         setUnit() { | ||||
|             this.unit = this.valueMetadata.unit || ''; | ||||
|             this.unit = this.valueMetadata ? this.valueMetadata.unit : ''; | ||||
|         }, | ||||
|         firstNonDomainAttribute(metadata) { | ||||
|             return metadata | ||||
|   | ||||
| @@ -83,9 +83,12 @@ export default { | ||||
|             for (let ladTable of ladTables) { | ||||
|                 for (let telemetryObject of ladTable) { | ||||
|                     let metadata = this.openmct.telemetry.getMetadata(telemetryObject.domainObject); | ||||
|                     for (let metadatum of metadata.valueMetadatas) { | ||||
|                         if (metadatum.unit) { | ||||
|                             return true; | ||||
|  | ||||
|                     if (metadata) { | ||||
|                         for (let metadatum of metadata.valueMetadatas) { | ||||
|                             if (metadatum.unit) { | ||||
|                                 return true; | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|   | ||||
| @@ -155,7 +155,7 @@ describe("The LAD Table", () => { | ||||
|         // add another telemetry object as composition in lad table to test multi rows | ||||
|         mockObj.ladTable.composition.push(anotherTelemetryObj.identifier); | ||||
|  | ||||
|         beforeEach(async (done) => { | ||||
|         beforeEach(async () => { | ||||
|             let telemetryRequestResolve; | ||||
|             let telemetryObjectResolve; | ||||
|             let anotherTelemetryObjectResolve; | ||||
| @@ -204,8 +204,6 @@ describe("The LAD Table", () => { | ||||
|  | ||||
|             await Promise.all([telemetryRequestPromise, telemetryObjectPromise, anotherTelemetryObjectPromise]); | ||||
|             await Vue.nextTick(); | ||||
|  | ||||
|             done(); | ||||
|         }); | ||||
|  | ||||
|         it("should show one row per object in the composition", () => { | ||||
|   | ||||
| @@ -178,6 +178,26 @@ export default { | ||||
|             this.requestDataFor(telemetryObject); | ||||
|             this.subscribeToObject(telemetryObject); | ||||
|         }, | ||||
|         setTrace(key, name, axisMetadata, xValues, yValues) { | ||||
|             let trace = { | ||||
|                 key, | ||||
|                 name: name, | ||||
|                 x: xValues, | ||||
|                 y: yValues, | ||||
|                 xAxisMetadata: {}, | ||||
|                 yAxisMetadata: axisMetadata.yAxisMetadata, | ||||
|                 type: this.domainObject.configuration.useBar ? 'bar' : 'scatter', | ||||
|                 mode: 'lines', | ||||
|                 line: { | ||||
|                     shape: this.domainObject.configuration.useInterpolation | ||||
|                 }, | ||||
|                 marker: { | ||||
|                     color: this.domainObject.configuration.barStyles.series[key].color | ||||
|                 }, | ||||
|                 hoverinfo: this.domainObject.configuration.useBar ? 'skip' : 'x+y' | ||||
|             }; | ||||
|             this.addTrace(trace, key); | ||||
|         }, | ||||
|         addTrace(trace, key) { | ||||
|             if (!this.trace.length) { | ||||
|                 this.trace = this.trace.concat([trace]); | ||||
| @@ -236,7 +256,15 @@ export default { | ||||
|         refreshData(bounds, isTick) { | ||||
|             if (!isTick) { | ||||
|                 const telemetryObjects = Object.values(this.telemetryObjects); | ||||
|                 telemetryObjects.forEach(this.requestDataFor); | ||||
|                 telemetryObjects.forEach((telemetryObject) => { | ||||
|                     //clear existing data | ||||
|                     const key = this.openmct.objects.makeKeyString(telemetryObject.identifier); | ||||
|                     const axisMetadata = this.getAxisMetadata(telemetryObject); | ||||
|                     this.setTrace(key, telemetryObject.name, axisMetadata, [], []); | ||||
|                     //request new data | ||||
|                     this.requestDataFor(telemetryObject); | ||||
|                     this.subscribeToObject(telemetryObject); | ||||
|                 }); | ||||
|             } | ||||
|         }, | ||||
|         removeAllSubscriptions() { | ||||
| @@ -320,25 +348,7 @@ export default { | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|             let trace = { | ||||
|                 key, | ||||
|                 name: telemetryObject.name, | ||||
|                 x: xValues, | ||||
|                 y: yValues, | ||||
|                 xAxisMetadata: xAxisMetadata, | ||||
|                 yAxisMetadata: axisMetadata.yAxisMetadata, | ||||
|                 type: this.domainObject.configuration.useBar ? 'bar' : 'scatter', | ||||
|                 mode: 'lines', | ||||
|                 line: { | ||||
|                     shape: this.domainObject.configuration.useInterpolation | ||||
|                 }, | ||||
|                 marker: { | ||||
|                     color: this.domainObject.configuration.barStyles.series[key].color | ||||
|                 }, | ||||
|                 hoverinfo: this.domainObject.configuration.useBar ? 'skip' : 'x+y' | ||||
|             }; | ||||
|  | ||||
|             this.addTrace(trace, key); | ||||
|             this.setTrace(key, telemetryObject.name, axisMetadata, xValues, yValues); | ||||
|         }, | ||||
|         isDataInTimeRange(datum, key, telemetryObject) { | ||||
|             const timeSystemKey = this.timeContext.timeSystem().key; | ||||
|   | ||||
| @@ -66,12 +66,15 @@ export default function BarGraphViewProvider(openmct) { | ||||
|                                 } | ||||
|                             }; | ||||
|                         }, | ||||
|                         template: '<bar-graph-view :options="options"></bar-graph-view>' | ||||
|                         template: '<bar-graph-view ref="graphComponent" :options="options"></bar-graph-view>' | ||||
|                     }); | ||||
|                 }, | ||||
|                 destroy: function () { | ||||
|                     component.$destroy(); | ||||
|                     component = undefined; | ||||
|                 }, | ||||
|                 onClearData() { | ||||
|                     component.$refs.graphComponent.refreshData(); | ||||
|                 } | ||||
|             }; | ||||
|         } | ||||
|   | ||||
| @@ -281,11 +281,11 @@ export default { | ||||
|                 this.xKeyOptions.push( | ||||
|                     metadataValues.reduce((previousValue, currentValue) => { | ||||
|                         return { | ||||
|                             name: `${previousValue.name}, ${currentValue.name}`, | ||||
|                             name: previousValue?.name ? `${previousValue.name}, ${currentValue.name}` : `${currentValue.name}`, | ||||
|                             value: currentValue.key, | ||||
|                             isArrayValue: currentValue.isArrayValue | ||||
|                         }; | ||||
|                     }) | ||||
|                     }, {name: ''}) | ||||
|                 ); | ||||
|             } | ||||
|  | ||||
| @@ -316,11 +316,16 @@ export default { | ||||
|                     } | ||||
|                 } else { | ||||
|                     if (this.yKey === undefined) { | ||||
|                         yKeyOptionIndex = this.yKeyOptions.findIndex((option, index) => index !== xKeyOptionIndex); | ||||
|                         if (yKeyOptionIndex > -1) { | ||||
|                         if (metadataValues.length && metadataArrayValues.length === 0) { | ||||
|                             update = true; | ||||
|                             this.yKey = this.yKeyOptions[yKeyOptionIndex].value; | ||||
|                             this.yKeyLabel = this.yKeyOptions[yKeyOptionIndex].name; | ||||
|                             this.yKey = 'none'; | ||||
|                         } else { | ||||
|                             yKeyOptionIndex = this.yKeyOptions.findIndex((option, index) => index !== xKeyOptionIndex); | ||||
|                             if (yKeyOptionIndex > -1) { | ||||
|                                 update = true; | ||||
|                                 this.yKey = this.yKeyOptions[yKeyOptionIndex].value; | ||||
|                                 this.yKeyLabel = this.yKeyOptions[yKeyOptionIndex].name; | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
| @@ -336,6 +341,8 @@ export default { | ||||
|  | ||||
|                     return option; | ||||
|                 }); | ||||
|             } else if (this.xKey !== undefined && this.domainObject.configuration.axes.yKey === undefined) { | ||||
|                 this.domainObject.configuration.axes.yKey = 'none'; | ||||
|             } | ||||
|  | ||||
|             this.xKeyOptions = this.xKeyOptions.map((option, index) => { | ||||
|   | ||||
| @@ -28,9 +28,9 @@ export default function () { | ||||
|     return function install(openmct) { | ||||
|         openmct.types.addType(BAR_GRAPH_KEY, { | ||||
|             key: BAR_GRAPH_KEY, | ||||
|             name: "Graph (Bar or Line)", | ||||
|             name: "Graph", | ||||
|             cssClass: "icon-bar-chart", | ||||
|             description: "View data as a bar graph. Can be added to Display Layouts.", | ||||
|             description: "Visualize data as a bar or line graph.", | ||||
|             creatable: true, | ||||
|             initialize: function (domainObject) { | ||||
|                 domainObject.composition = []; | ||||
|   | ||||
| @@ -367,19 +367,26 @@ describe("the plugin", function () { | ||||
|                 type: "test-object", | ||||
|                 name: "Test Object", | ||||
|                 telemetry: { | ||||
|                     values: [{ | ||||
|                         key: "some-key", | ||||
|                         name: "Some attribute", | ||||
|                         hints: { | ||||
|                             domain: 1 | ||||
|                         } | ||||
|                     }, { | ||||
|                         key: "some-other-key", | ||||
|                         name: "Another attribute", | ||||
|                         hints: { | ||||
|                             range: 1 | ||||
|                         } | ||||
|                     }] | ||||
|                     values: [ | ||||
|                         { | ||||
|                             key: "some-key", | ||||
|                             source: "some-key", | ||||
|                             name: "Some attribute", | ||||
|                             format: "enum", | ||||
|                             enumerations: [ | ||||
|                                 { | ||||
|                                     value: 0, | ||||
|                                     string: "OFF" | ||||
|                                 }, | ||||
|                                 { | ||||
|                                     value: 1, | ||||
|                                     string: "ON" | ||||
|                                 } | ||||
|                             ], | ||||
|                             hints: { | ||||
|                                 range: 1 | ||||
|                             } | ||||
|                         }] | ||||
|                 } | ||||
|             }; | ||||
|             const composition = openmct.composition.get(parent); | ||||
|   | ||||
| @@ -300,8 +300,11 @@ export default class ConditionManager extends EventEmitter { | ||||
|         return this.compositionLoad.then(() => { | ||||
|             let latestTimestamp; | ||||
|             let conditionResults = {}; | ||||
|             let nextLegOptions = {...options}; | ||||
|             delete nextLegOptions.onPartialResponse; | ||||
|  | ||||
|             const conditionRequests = this.conditions | ||||
|                 .map(condition => condition.requestLADConditionResult(options)); | ||||
|                 .map(condition => condition.requestLADConditionResult(nextLegOptions)); | ||||
|  | ||||
|             return Promise.all(conditionRequests) | ||||
|                 .then((results) => { | ||||
|   | ||||
| @@ -136,8 +136,8 @@ export default { | ||||
|                 this.url = url; | ||||
|             } | ||||
|  | ||||
|             const conditionSetIdentifier = domainObject.configuration.objectStyles.conditionSetIdentifier; | ||||
|             if (this.conditionSetIdentifier !== conditionSetIdentifier) { | ||||
|             const conditionSetIdentifier = domainObject.configuration?.objectStyles?.conditionSetIdentifier; | ||||
|             if (conditionSetIdentifier && this.conditionSetIdentifier !== conditionSetIdentifier) { | ||||
|                 this.conditionSetIdentifier = conditionSetIdentifier; | ||||
|             } | ||||
|  | ||||
|   | ||||
| @@ -152,7 +152,7 @@ export default { | ||||
|         }, | ||||
|         unit() { | ||||
|             let value = this.item.value; | ||||
|             let unit = this.metadata.value(value).unit; | ||||
|             let unit = this.metadata ? this.metadata.value(value).unit : ''; | ||||
|  | ||||
|             return unit; | ||||
|         }, | ||||
| @@ -280,7 +280,7 @@ export default { | ||||
|             this.limitEvaluator = this.openmct.telemetry.limitEvaluator(this.domainObject); | ||||
|             this.formats = this.openmct.telemetry.getFormatMap(this.metadata); | ||||
|  | ||||
|             const valueMetadata = this.metadata.value(this.item.value); | ||||
|             const valueMetadata = this.metadata ? this.metadata.value(this.item.value) : {}; | ||||
|             this.customStringformatter = this.openmct.telemetry.customStringFormatter(valueMetadata, this.item.format); | ||||
|  | ||||
|             this.telemetryCollection = this.openmct.telemetry.requestCollection(this.domainObject, { | ||||
|   | ||||
| @@ -28,7 +28,6 @@ | ||||
|  | ||||
|         &[s-selected] { | ||||
|             // All frames selected while editing | ||||
|             border: $editFrameSelectedBorder; | ||||
|             box-shadow: $editFrameSelectedShdw; | ||||
|  | ||||
|             .c-frame__move-bar { | ||||
|   | ||||
| @@ -41,7 +41,7 @@ describe('the plugin', function () { | ||||
|         element.appendChild(child); | ||||
|  | ||||
|         openmct.on('start', done); | ||||
|         openmct.startHeadless(); | ||||
|         openmct.start(child); | ||||
|     }); | ||||
|  | ||||
|     afterEach(() => { | ||||
|   | ||||
| @@ -128,6 +128,30 @@ export default class ExportAsJSONAction { | ||||
|  | ||||
|         return copyOfChild; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @private | ||||
|      * @param {object} child | ||||
|      * @param {object} parent | ||||
|      * @returns {object} | ||||
|      */ | ||||
|     _rewriteLinkForReference(child, parent) { | ||||
|         const childId = this._getId(child); | ||||
|         this.externalIdentifiers.push(childId); | ||||
|         const copyOfChild = JSON.parse(JSON.stringify(child)); | ||||
|  | ||||
|         copyOfChild.identifier.key = uuid(); | ||||
|         const newIdString = this._getId(copyOfChild); | ||||
|         const parentId = this._getId(parent); | ||||
|  | ||||
|         this.idMap[childId] = newIdString; | ||||
|         copyOfChild.location = null; | ||||
|         parent.configuration.objectStyles.conditionSetIdentifier = copyOfChild.identifier; | ||||
|         this.tree[newIdString] = copyOfChild; | ||||
|         this.tree[parentId].configuration.objectStyles.conditionSetIdentifier = copyOfChild.identifier; | ||||
|  | ||||
|         return copyOfChild; | ||||
|     } | ||||
|     /** | ||||
|      * @private | ||||
|      */ | ||||
| @@ -159,23 +183,27 @@ export default class ExportAsJSONAction { | ||||
|             "rootId": this._getId(this.root) | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * @private | ||||
|      * @param {object} parent | ||||
|      */ | ||||
|     _write(parent) { | ||||
|         this.calls++; | ||||
|         //conditional object styles are not saved on the composition, so we need to check for them | ||||
|         let childObjectReferenceId = parent.configuration?.objectStyles?.conditionSetIdentifier; | ||||
|  | ||||
|         const composition = this.openmct.composition.get(parent); | ||||
|         if (composition !== undefined) { | ||||
|             composition.load() | ||||
|                 .then((children) => { | ||||
|                     children.forEach((child, index) => { | ||||
|                         // Only export if object is creatable | ||||
|                     // Only export if object is creatable | ||||
|                         if (this._isCreatableAndPersistable(child)) { | ||||
|                             // Prevents infinite export of self-contained objs | ||||
|                         // Prevents infinite export of self-contained objs | ||||
|                             if (!Object.prototype.hasOwnProperty.call(this.tree, this._getId(child))) { | ||||
|                                 // If object is a link to something absent from | ||||
|                                 // tree, generate new id and treat as new object | ||||
|                             // If object is a link to something absent from | ||||
|                             // tree, generate new id and treat as new object | ||||
|                                 if (this._isLinkedObject(child, parent)) { | ||||
|                                     child = this._rewriteLink(child, parent); | ||||
|                                 } else { | ||||
| @@ -186,18 +214,41 @@ export default class ExportAsJSONAction { | ||||
|                             } | ||||
|                         } | ||||
|                     }); | ||||
|                     this.calls--; | ||||
|                     if (this.calls === 0) { | ||||
|                         this._rewriteReferences(); | ||||
|                         this._saveAs(this._wrapTree()); | ||||
|                     } | ||||
|                     this._decrementCallsAndSave(); | ||||
|                 }); | ||||
|         } else { | ||||
|             this.calls--; | ||||
|             if (this.calls === 0) { | ||||
|                 this._rewriteReferences(); | ||||
|                 this._saveAs(this._wrapTree()); | ||||
|             } | ||||
|         } else if (!childObjectReferenceId) { | ||||
|             this._decrementCallsAndSave(); | ||||
|         } | ||||
|  | ||||
|         if (childObjectReferenceId) { | ||||
|             this.openmct.objects.get(childObjectReferenceId) | ||||
|                 .then((child) => { | ||||
|                     // Only export if object is creatable | ||||
|                     if (this._isCreatableAndPersistable(child)) { | ||||
|                         // Prevents infinite export of self-contained objs | ||||
|                         if (!Object.prototype.hasOwnProperty.call(this.tree, this._getId(child))) { | ||||
|                             // If object is a link to something absent from | ||||
|                             // tree, generate new id and treat as new object | ||||
|                             if (this._isLinkedObject(child, parent)) { | ||||
|                                 child = this._rewriteLinkForReference(child, parent); | ||||
|                             } else { | ||||
|                                 this.tree[this._getId(child)] = child; | ||||
|                             } | ||||
|  | ||||
|                             this._write(child); | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     this._decrementCallsAndSave(); | ||||
|                 }); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     _decrementCallsAndSave() { | ||||
|         this.calls--; | ||||
|         if (this.calls === 0) { | ||||
|             this._rewriteReferences(); | ||||
|             this._saveAs(this._wrapTree()); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -322,4 +322,57 @@ describe('Export as JSON plugin', () => { | ||||
|  | ||||
|         exportAsJSONAction.invoke([parent]); | ||||
|     }); | ||||
|  | ||||
|     it('ExportAsJSONAction exports object references from tree', (done) => { | ||||
|         const parent = { | ||||
|             composition: [], | ||||
|             configuration: { | ||||
|                 objectStyles: { | ||||
|                     conditionSetIdentifier: { | ||||
|                         key: 'child', | ||||
|                         namespace: '' | ||||
|                     } | ||||
|                 } | ||||
|             }, | ||||
|             identifier: { | ||||
|                 key: 'parent', | ||||
|                 namespace: '' | ||||
|             }, | ||||
|             name: 'Parent', | ||||
|             type: 'folder', | ||||
|             modified: 1503598129176, | ||||
|             location: 'mine', | ||||
|             persisted: 1503598129176 | ||||
|         }; | ||||
|  | ||||
|         const child = { | ||||
|             composition: [], | ||||
|             identifier: { | ||||
|                 key: 'child', | ||||
|                 namespace: '' | ||||
|             }, | ||||
|             name: 'Child', | ||||
|             type: 'folder', | ||||
|             modified: 1503598132428, | ||||
|             location: null, | ||||
|             persisted: 1503598132428 | ||||
|         }; | ||||
|  | ||||
|         spyOn(openmct.objects, 'get').and.callFake(object => { | ||||
|             return Promise.resolve(child); | ||||
|         }); | ||||
|  | ||||
|         spyOn(exportAsJSONAction, '_saveAs').and.callFake(completedTree => { | ||||
|             expect(Object.keys(completedTree).length).toBe(2); | ||||
|             const conditionSetId = Object.keys(completedTree.openmct)[1]; | ||||
|             expect(Object.prototype.hasOwnProperty.call(completedTree, 'openmct')).toBeTruthy(); | ||||
|             expect(Object.prototype.hasOwnProperty.call(completedTree, 'rootId')).toBeTruthy(); | ||||
|             expect(Object.prototype.hasOwnProperty.call(completedTree.openmct, 'parent')).toBeTruthy(); | ||||
|             expect(completedTree.openmct[conditionSetId].name).toBe('Child'); | ||||
|  | ||||
|             done(); | ||||
|         }); | ||||
|  | ||||
|         exportAsJSONAction.invoke([parent]); | ||||
|     }); | ||||
| }); | ||||
|   | ||||
							
								
								
									
										129
									
								
								src/plugins/faultManagement/FaultManagementInspector.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								src/plugins/faultManagement/FaultManagementInspector.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,129 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| <template> | ||||
| <div | ||||
|     v-if="isShowDetails" | ||||
|     class="c-inspector__properties c-inspect-properties" | ||||
| > | ||||
|     <div class="c-inspect-properties__header">Fault Details</div> | ||||
|     <ul | ||||
|         class="c-inspect-properties__section" | ||||
|     > | ||||
|         <DetailText :detail="sourceDetails" /> | ||||
|         <DetailText :detail="occuredDetails" /> | ||||
|         <DetailText :detail="criticalityDetails" /> | ||||
|         <DetailText :detail="descriptionDetails" /> | ||||
|     </ul> | ||||
|  | ||||
|     <div class="c-inspect-properties__header">Telemetry</div> | ||||
|     <ul | ||||
|         class="c-inspect-properties__section" | ||||
|     > | ||||
|         <DetailText :detail="systemDetails" /> | ||||
|         <DetailText :detail="tripValueDetails" /> | ||||
|         <DetailText :detail="currentValueDetails" /> | ||||
|     </ul> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import DetailText from '@/ui/inspector/details/DetailText.vue'; | ||||
|  | ||||
| export default { | ||||
|     name: 'FaultManagementInspector', | ||||
|     components: { | ||||
|         DetailText | ||||
|     }, | ||||
|     inject: ['openmct'], | ||||
|     data() { | ||||
|         return { | ||||
|             isShowDetails: false | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|         criticalityDetails() { | ||||
|             return { | ||||
|                 name: 'Criticality', | ||||
|                 value: this.selectedFault?.severity | ||||
|             }; | ||||
|         }, | ||||
|         currentValueDetails() { | ||||
|             return { | ||||
|                 name: 'Live value', | ||||
|                 value: this.selectedFault?.currentValueInfo?.value | ||||
|             }; | ||||
|         }, | ||||
|         descriptionDetails() { | ||||
|             return { | ||||
|                 name: 'Description', | ||||
|                 value: this.selectedFault?.shortDescription | ||||
|             }; | ||||
|         }, | ||||
|         occuredDetails() { | ||||
|             return { | ||||
|                 name: 'Occured', | ||||
|                 value: this.selectedFault?.triggerTime | ||||
|             }; | ||||
|         }, | ||||
|         sourceDetails() { | ||||
|             return { | ||||
|                 name: 'Source', | ||||
|                 value: this.selectedFault?.name | ||||
|             }; | ||||
|         }, | ||||
|         systemDetails() { | ||||
|             return { | ||||
|                 name: 'System', | ||||
|                 value: this.selectedFault?.namespace | ||||
|             }; | ||||
|         }, | ||||
|         tripValueDetails() { | ||||
|             return { | ||||
|                 name: 'Trip Value', | ||||
|                 value: this.selectedFault?.triggerValueInfo?.value | ||||
|             }; | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.updateSelectedFaults(); | ||||
|     }, | ||||
|     methods: { | ||||
|         updateSelectedFaults() { | ||||
|             const selection = this.openmct.selection.get(); | ||||
|             this.isShowDetails = false; | ||||
|  | ||||
|             if (selection.length === 0 || selection[0].length < 2) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             const selectedFaults = selection[0][1].context.selectedFaults; | ||||
|             if (selectedFaults.length !== 1) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             this.isShowDetails = true; | ||||
|             this.selectedFault = selectedFaults[0]; | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
| @@ -0,0 +1,71 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| import FaultManagementInspector from './FaultManagementInspector.vue'; | ||||
|  | ||||
| import Vue from 'vue'; | ||||
|  | ||||
| import { FAULT_MANAGEMENT_INSPECTOR, FAULT_MANAGEMENT_TYPE } from './constants'; | ||||
|  | ||||
| export default function FaultManagementInspectorViewProvider(openmct) { | ||||
|     return { | ||||
|         openmct: openmct, | ||||
|         key: FAULT_MANAGEMENT_INSPECTOR, | ||||
|         name: 'FAULT_MANAGEMENT_TYPE', | ||||
|         canView: (selection) => { | ||||
|             if (selection.length !== 1 || selection[0].length === 0) { | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             let object = selection[0][0].context.item; | ||||
|  | ||||
|             return object && object.type === FAULT_MANAGEMENT_TYPE; | ||||
|         }, | ||||
|         view: (selection) => { | ||||
|             let component; | ||||
|  | ||||
|             return { | ||||
|                 show: function (element) { | ||||
|                     component = new Vue({ | ||||
|                         el: element, | ||||
|                         components: { | ||||
|                             FaultManagementInspector | ||||
|                         }, | ||||
|                         provide: { | ||||
|                             openmct | ||||
|                         }, | ||||
|                         template: '<FaultManagementInspector></FaultManagementInspector>' | ||||
|                     }); | ||||
|                 }, | ||||
|                 destroy: function () { | ||||
|                     if (component) { | ||||
|                         component.$destroy(); | ||||
|                         component = undefined; | ||||
|                     } | ||||
|                 } | ||||
|             }; | ||||
|         }, | ||||
|         priority: () => { | ||||
|             return 1; | ||||
|         } | ||||
|     }; | ||||
| } | ||||
							
								
								
									
										105
									
								
								src/plugins/faultManagement/FaultManagementListHeader.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								src/plugins/faultManagement/FaultManagementListHeader.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,105 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| <template> | ||||
| <div class="c-fault-mgmt-item-header c-fault-mgmt__list-header c-fault-mgmt__list"> | ||||
|     <div class="c-fault-mgmt-item-header c-fault-mgmt__checkbox"> | ||||
|         <input | ||||
|             type="checkbox" | ||||
|             :checked="isSelectAll" | ||||
|             @input="selectAll" | ||||
|         > | ||||
|     </div> | ||||
|     <div class="c-fault-mgmt-item-header c-fault-mgmt__list-header-results c-fault-mgmt__list-severity"> | ||||
|         {{ totalFaultsCount }} Results | ||||
|     </div> | ||||
|     <div class="c-fault-mgmt__list-header-content"> | ||||
|         <div class="c-fault-mgmt__list-content-right"> | ||||
|             <div class="c-fault-mgmt-item-header c-fault-mgmt__list-header-tripVal">Trip Value</div> | ||||
|             <div class="c-fault-mgmt-item-header c-fault-mgmt__list-header-liveVal">Live Value</div> | ||||
|             <div class="c-fault-mgmt-item-header c-fault-mgmt__list-header-trigTime">Trigger Time</div> | ||||
|         </div> | ||||
|     </div> | ||||
|     <div class=" c-fault-mgmt-item-header c-fault-mgmt__list-header-action-wrapper"> | ||||
|         <div class="c-fault-mgmt__list-header-sortButton c-fault-mgmt__list-action-button"> | ||||
|             <SelectField | ||||
|                 class="c-fault-mgmt-viewButton" | ||||
|                 title="Sort By" | ||||
|                 :model="model" | ||||
|                 @onChange="onChange" | ||||
|             /> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import SelectField from '@/api/forms/components/controls/SelectField.vue'; | ||||
|  | ||||
| import { SORT_ITEMS } from './constants'; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
|         SelectField | ||||
|     }, | ||||
|     inject: ['openmct', 'domainObject'], | ||||
|     props: { | ||||
|         selectedFaults: { | ||||
|             type: Array, | ||||
|             default() { | ||||
|                 return []; | ||||
|             } | ||||
|         }, | ||||
|         totalFaultsCount: { | ||||
|             type: Number, | ||||
|             default() { | ||||
|                 return 0; | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             model: {} | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|         isSelectAll() { | ||||
|             return this.totalFaultsCount > 0 && this.selectedFaults.length === this.totalFaultsCount; | ||||
|         } | ||||
|     }, | ||||
|     beforeMount() { | ||||
|         const options = Object.values(SORT_ITEMS); | ||||
|         this.model = { | ||||
|             options, | ||||
|             value: options[0].value | ||||
|         }; | ||||
|     }, | ||||
|     methods: { | ||||
|         onChange(data) { | ||||
|             this.$emit('sortChanged', data); | ||||
|         }, | ||||
|         selectAll(e) { | ||||
|             this.$emit('selectAll', e.target.checked); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
							
								
								
									
										223
									
								
								src/plugins/faultManagement/FaultManagementListItem.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										223
									
								
								src/plugins/faultManagement/FaultManagementListItem.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,223 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| <template> | ||||
| <div | ||||
|     class="c-fault-mgmt__list data-selectable" | ||||
|     :class="classesFromState" | ||||
| > | ||||
|     <div class="c-fault-mgmt-item c-fault-mgmt__list-checkbox"> | ||||
|         <input | ||||
|             type="checkbox" | ||||
|             :checked="isSelected" | ||||
|             @input="toggleSelected" | ||||
|         > | ||||
|     </div> | ||||
|     <div class="c-fault-mgmt-item"> | ||||
|         <div | ||||
|             class="c-fault-mgmt__list-severity" | ||||
|             :title="fault.severity" | ||||
|             :class="[ | ||||
|                 'is-severity-' + severity | ||||
|             ]" | ||||
|         > | ||||
|         </div> | ||||
|     </div> | ||||
|     <div class="c-fault-mgmt-item c-fault-mgmt__list-content"> | ||||
|         <div class="c-fault-mgmt-item c-fault-mgmt__list-pathname"> | ||||
|             <div class="c-fault-mgmt__list-path">{{ fault.namespace }}</div> | ||||
|             <div class="c-fault-mgmt__list-faultname">{{ fault.name }}</div> | ||||
|         </div> | ||||
|         <div class="c-fault-mgmt__list-content-right"> | ||||
|             <div class="c-fault-mgmt-item c-fault-mgmt__list-trigVal"> | ||||
|                 <div | ||||
|                     class="c-fault-mgmt-item__value" | ||||
|                     :class="tripValueClassname" | ||||
|                     title="Trip Value" | ||||
|                 >{{ fault.triggerValueInfo.value }}</div> | ||||
|             </div> | ||||
|             <div class="c-fault-mgmt-item c-fault-mgmt__list-curVal"> | ||||
|                 <div | ||||
|                     class="c-fault-mgmt-item__value" | ||||
|                     :class="liveValueClassname" | ||||
|                     title="Live Value" | ||||
|                 >{{ fault.currentValueInfo.value }}</div> | ||||
|             </div> | ||||
|             <div class="c-fault-mgmt-item c-fault-mgmt__list-trigTime"> | ||||
|                 <div | ||||
|                     class="c-fault-mgmt-item__value" | ||||
|                     title="Last Trigger Time" | ||||
|                 >{{ fault.triggerTime }} | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
|     <div class="c-fault-mgmt-item c-fault-mgmt__list-action-wrapper"> | ||||
|         <button | ||||
|             class="c-fault-mgmt__list-action-button l-browse-bar__actions c-icon-button icon-3-dots" | ||||
|             title="Disposition Actions" | ||||
|             @click="showActionMenu" | ||||
|         ></button> | ||||
|     </div> | ||||
| </div> | ||||
| </template> | ||||
| <script> | ||||
|  | ||||
| const RANGE_CONDITION_CLASS = { | ||||
|     'LOW': 'is-limit--lwr', | ||||
|     'HIGH': 'is-limit--upr' | ||||
| }; | ||||
|  | ||||
| const SEVERITY_CLASS = { | ||||
|     'CRITICAL': 'is-limit--red', | ||||
|     'WARNING': 'is-limit--yellow', | ||||
|     'WATCH': 'is-limit--cyan' | ||||
| }; | ||||
|  | ||||
| export default { | ||||
|     inject: ['openmct', 'domainObject'], | ||||
|     props: { | ||||
|         fault: { | ||||
|             type: Object, | ||||
|             required: true | ||||
|         }, | ||||
|         isSelected: { | ||||
|             type: Boolean, | ||||
|             default: () => { | ||||
|                 return false; | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     computed: { | ||||
|         classesFromState() { | ||||
|             const exclusiveStates = [ | ||||
|                 { | ||||
|                     className: 'is-shelved', | ||||
|                     test: () => this.fault.shelved | ||||
|                 }, | ||||
|                 { | ||||
|                     className: 'is-unacknowledged', | ||||
|                     test: () => !this.fault.acknowledged && !this.fault.shelved | ||||
|                 }, | ||||
|                 { | ||||
|                     className: 'is-acknowledged', | ||||
|                     test: () => this.fault.acknowledged && !this.fault.shelved | ||||
|                 } | ||||
|             ]; | ||||
|  | ||||
|             const classes = []; | ||||
|  | ||||
|             if (this.isSelected) { | ||||
|                 classes.push('is-selected'); | ||||
|             } | ||||
|  | ||||
|             const matchingState = exclusiveStates.find(stateDefinition => stateDefinition.test()); | ||||
|  | ||||
|             if (matchingState !== undefined) { | ||||
|                 classes.push(matchingState.className); | ||||
|             } | ||||
|  | ||||
|             return classes; | ||||
|         }, | ||||
|         liveValueClassname() { | ||||
|             const currentValueInfo = this.fault?.currentValueInfo; | ||||
|             if (!currentValueInfo || currentValueInfo.monitoringResult === 'IN_LIMITS') { | ||||
|                 return ''; | ||||
|             } | ||||
|  | ||||
|             let classname = RANGE_CONDITION_CLASS[currentValueInfo.rangeCondition] || ''; | ||||
|             classname += ' '; | ||||
|             classname += SEVERITY_CLASS[currentValueInfo.monitoringResult] || ''; | ||||
|  | ||||
|             return classname.trim(); | ||||
|         }, | ||||
|         name() { | ||||
|             return `${this.fault?.name}/${this.fault?.namespace}`; | ||||
|         }, | ||||
|         severity() { | ||||
|             return this.fault?.severity?.toLowerCase(); | ||||
|         }, | ||||
|         triggerTime() { | ||||
|             return this.fault?.triggerTime; | ||||
|         }, | ||||
|         triggerValue() { | ||||
|             return this.fault?.triggerValueInfo?.value; | ||||
|         }, | ||||
|         tripValueClassname() { | ||||
|             const triggerValueInfo = this.fault?.triggerValueInfo; | ||||
|             if (!triggerValueInfo || triggerValueInfo.monitoringResult === 'IN_LIMITS') { | ||||
|                 return ''; | ||||
|             } | ||||
|  | ||||
|             let classname = RANGE_CONDITION_CLASS[triggerValueInfo.rangeCondition] || ''; | ||||
|             classname += ' '; | ||||
|             classname += SEVERITY_CLASS[triggerValueInfo.monitoringResult] || ''; | ||||
|  | ||||
|             return classname.trim(); | ||||
|         } | ||||
|     }, | ||||
|     methods: { | ||||
|         showActionMenu(event) { | ||||
|             event.stopPropagation(); | ||||
|  | ||||
|             const menuItems = [ | ||||
|                 { | ||||
|                     cssClass: 'icon-check', | ||||
|                     isDisabled: this.fault.acknowledged, | ||||
|                     name: 'Acknowledge', | ||||
|                     description: '', | ||||
|                     onItemClicked: (e) => { | ||||
|                         this.$emit('acknowledgeSelected', [this.fault]); | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     cssClass: 'icon-timer', | ||||
|                     name: 'Shelve', | ||||
|                     description: '', | ||||
|                     onItemClicked: () => { | ||||
|                         this.$emit('shelveSelected', [this.fault], { shelved: true }); | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     cssClass: 'icon-timer', | ||||
|                     isDisabled: Boolean(!this.fault.shelved), | ||||
|                     name: 'Unshelve', | ||||
|                     description: '', | ||||
|                     onItemClicked: () => { | ||||
|                         this.$emit('shelveSelected', [this.fault], { shelved: false }); | ||||
|                     } | ||||
|                 } | ||||
|             ]; | ||||
|  | ||||
|             this.openmct.menus.showMenu(event.x, event.y, menuItems); | ||||
|         }, | ||||
|         toggleSelected(event) { | ||||
|             const faultData = { | ||||
|                 fault: this.fault, | ||||
|                 selected: event.target.checked | ||||
|             }; | ||||
|  | ||||
|             this.$emit('toggleSelected', faultData); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
							
								
								
									
										307
									
								
								src/plugins/faultManagement/FaultManagementListView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										307
									
								
								src/plugins/faultManagement/FaultManagementListView.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,307 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| <template> | ||||
| <div class="c-faults-list-view"> | ||||
|     <FaultManagementSearch | ||||
|         :search-term="searchTerm" | ||||
|         @filterChanged="updateFilter" | ||||
|         @updateSearchTerm="updateSearchTerm" | ||||
|     /> | ||||
|  | ||||
|     <FaultManagementToolbar | ||||
|         v-if="showToolbar" | ||||
|         :selected-faults="selectedFaults" | ||||
|         @acknowledgeSelected="toggleAcknowledgeSelected" | ||||
|         @shelveSelected="toggleShelveSelected" | ||||
|     /> | ||||
|  | ||||
|     <div class="c-faults-list-view-header-item-container-wrapper"> | ||||
|         <div class="c-faults-list-view-header-item-container"> | ||||
|             <FaultManagementListHeader | ||||
|                 class="header" | ||||
|                 :selected-faults="Object.values(selectedFaults)" | ||||
|                 :total-faults-count="filteredFaultsList.length" | ||||
|                 @selectAll="selectAll" | ||||
|                 @sortChanged="sortChanged" | ||||
|             /> | ||||
|  | ||||
|             <div class="c-faults-list-view-item-body"> | ||||
|                 <template v-if="filteredFaultsList.length > 0"> | ||||
|                     <FaultManagementListItem | ||||
|                         v-for="fault of filteredFaultsList" | ||||
|                         :key="fault.id" | ||||
|                         :fault="fault" | ||||
|                         :is-selected="isSelected(fault)" | ||||
|                         @toggleSelected="toggleSelected" | ||||
|                         @acknowledgeSelected="toggleAcknowledgeSelected" | ||||
|                         @shelveSelected="toggleShelveSelected" | ||||
|                     /> | ||||
|                 </template> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import FaultManagementListHeader from './FaultManagementListHeader.vue'; | ||||
| import FaultManagementListItem from './FaultManagementListItem.vue'; | ||||
| import FaultManagementSearch from './FaultManagementSearch.vue'; | ||||
| import FaultManagementToolbar from './FaultManagementToolbar.vue'; | ||||
|  | ||||
| import { FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS, FILTER_ITEMS, SORT_ITEMS } from './constants'; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
|         FaultManagementListHeader, | ||||
|         FaultManagementListItem, | ||||
|         FaultManagementSearch, | ||||
|         FaultManagementToolbar | ||||
|     }, | ||||
|     inject: ['openmct', 'domainObject'], | ||||
|     props: { | ||||
|         faultsList: { | ||||
|             type: Array, | ||||
|             default: () => [] | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             filterIndex: 0, | ||||
|             searchTerm: '', | ||||
|             selectedFaults: {}, | ||||
|             sortBy: Object.values(SORT_ITEMS)[0].value | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|         filteredFaultsList() { | ||||
|             const filterName = FILTER_ITEMS[this.filterIndex]; | ||||
|             let list = this.faultsList; | ||||
|  | ||||
|             // Exclude shelved alarms from all views except the Shelved view | ||||
|             if (filterName !== 'Shelved') { | ||||
|                 list = list.filter(fault => fault.shelved !== true); | ||||
|             } | ||||
|  | ||||
|             if (filterName === 'Acknowledged') { | ||||
|                 list = list.filter(fault => fault.acknowledged); | ||||
|             } else if (filterName === 'Unacknowledged') { | ||||
|                 list = list.filter(fault => !fault.acknowledged); | ||||
|             } else if (filterName === 'Shelved') { | ||||
|                 list = list.filter(fault => fault.shelved); | ||||
|             } | ||||
|  | ||||
|             if (this.searchTerm.length > 0) { | ||||
|                 list = list.filter(this.filterUsingSearchTerm); | ||||
|             } | ||||
|  | ||||
|             list.sort(SORT_ITEMS[this.sortBy].sortFunction); | ||||
|  | ||||
|             return list; | ||||
|         }, | ||||
|         showToolbar() { | ||||
|             return this.openmct.faults.supportsActions(); | ||||
|         } | ||||
|     }, | ||||
|     methods: { | ||||
|         filterUsingSearchTerm(fault) { | ||||
|             if (fault?.id?.toString().toLowerCase().includes(this.searchTerm)) { | ||||
|                 return true; | ||||
|             } | ||||
|  | ||||
|             if (fault?.triggerValueInfo?.toString().toLowerCase().includes(this.searchTerm)) { | ||||
|                 return true; | ||||
|             } | ||||
|  | ||||
|             if (fault?.currentValueInfo?.toString().toLowerCase().includes(this.searchTerm)) { | ||||
|                 return true; | ||||
|             } | ||||
|  | ||||
|             if (fault?.triggerTime.toString().toLowerCase().includes(this.searchTerm)) { | ||||
|                 return true; | ||||
|             } | ||||
|  | ||||
|             if (fault?.severity.toString().toLowerCase().includes(this.searchTerm)) { | ||||
|                 return true; | ||||
|             } | ||||
|  | ||||
|             return false; | ||||
|         }, | ||||
|         isSelected(fault) { | ||||
|             return Boolean(this.selectedFaults[fault.id]); | ||||
|         }, | ||||
|         selectAll(toggle = false) { | ||||
|             this.faultsList.forEach(fault => { | ||||
|                 const faultData = { | ||||
|                     fault, | ||||
|                     selected: toggle | ||||
|                 }; | ||||
|                 this.toggleSelected(faultData); | ||||
|             }); | ||||
|         }, | ||||
|         sortChanged(sort) { | ||||
|             this.sortBy = sort.value; | ||||
|         }, | ||||
|         toggleSelected({ fault, selected = false}) { | ||||
|             if (selected) { | ||||
|                 this.$set(this.selectedFaults, fault.id, fault); | ||||
|             } else { | ||||
|                 this.$delete(this.selectedFaults, fault.id); | ||||
|             } | ||||
|  | ||||
|             const selectedFaults = Object.values(this.selectedFaults); | ||||
|             this.openmct.selection.select( | ||||
|                 [ | ||||
|                     { | ||||
|                         element: this.$el, | ||||
|                         context: { | ||||
|                             item: this.openmct.router.path[0] | ||||
|                         } | ||||
|                     }, | ||||
|                     { | ||||
|                         element: this.$el, | ||||
|                         context: { | ||||
|                             selectedFaults | ||||
|                         } | ||||
|                     } | ||||
|                 ], | ||||
|                 false); | ||||
|         }, | ||||
|         toggleAcknowledgeSelected(faults = Object.values(this.selectedFaults)) { | ||||
|             let title = ''; | ||||
|             if (faults.length > 1) { | ||||
|                 title = `Acknowledge ${faults.length} selected faults`; | ||||
|             } else { | ||||
|                 title = `Acknowledge fault: ${faults[0].name}`; | ||||
|             } | ||||
|  | ||||
|             const formStructure = { | ||||
|                 title, | ||||
|                 sections: [ | ||||
|                     { | ||||
|                         rows: [ | ||||
|                             { | ||||
|                                 key: 'comment', | ||||
|                                 control: 'textarea', | ||||
|                                 name: 'Optional comment', | ||||
|                                 pattern: '\\S+', | ||||
|                                 required: false, | ||||
|                                 cssClass: 'l-input-lg', | ||||
|                                 value: '' | ||||
|                             } | ||||
|                         ] | ||||
|                     } | ||||
|                 ], | ||||
|                 buttons: { | ||||
|                     submit: { | ||||
|                         label: 'Acknowledge' | ||||
|                     } | ||||
|                 } | ||||
|             }; | ||||
|  | ||||
|             this.openmct.forms.showForm(formStructure) | ||||
|                 .then(data => { | ||||
|                     Object.values(faults) | ||||
|                         .forEach(selectedFault => { | ||||
|                             this.openmct.faults.acknowledgeFault(selectedFault, data); | ||||
|                         }); | ||||
|                 }); | ||||
|  | ||||
|             this.selectedFaults = {}; | ||||
|         }, | ||||
|         async toggleShelveSelected(faults = Object.values(this.selectedFaults), shelveData = {}) { | ||||
|             const { shelved = true } = shelveData; | ||||
|             if (shelved) { | ||||
|                 let title = faults.length > 1 | ||||
|                     ? `Shelve ${faults.length} selected faults` | ||||
|                     : `Shelve fault: ${faults[0].name}` | ||||
|                 ; | ||||
|  | ||||
|                 const formStructure = { | ||||
|                     title, | ||||
|                     sections: [ | ||||
|                         { | ||||
|                             rows: [ | ||||
|                                 { | ||||
|                                     key: 'comment', | ||||
|                                     control: 'textarea', | ||||
|                                     name: 'Optional comment', | ||||
|                                     pattern: '\\S+', | ||||
|                                     required: false, | ||||
|                                     cssClass: 'l-input-lg', | ||||
|                                     value: '' | ||||
|                                 }, | ||||
|                                 { | ||||
|                                     key: 'shelveDuration', | ||||
|                                     control: 'select', | ||||
|                                     name: 'Shelve duration', | ||||
|                                     options: FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS, | ||||
|                                     required: false, | ||||
|                                     cssClass: 'l-input-lg', | ||||
|                                     value: FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS[0].value | ||||
|                                 } | ||||
|                             ] | ||||
|                         } | ||||
|                     ], | ||||
|                     buttons: { | ||||
|                         submit: { | ||||
|                             label: 'Shelve' | ||||
|                         } | ||||
|                     } | ||||
|                 }; | ||||
|  | ||||
|                 let data; | ||||
|                 try { | ||||
|                     data = await this.openmct.forms.showForm(formStructure); | ||||
|                 } catch (e) { | ||||
|                     return; | ||||
|                 } | ||||
|  | ||||
|                 shelveData.comment = data.comment || ''; | ||||
|                 shelveData.shelveDuration = data.shelveDuration !== undefined | ||||
|                     ? data.shelveDuration | ||||
|                     : FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS[0].value; | ||||
|             } else { | ||||
|                 shelveData = { | ||||
|                     shelved: false | ||||
|                 }; | ||||
|             } | ||||
|  | ||||
|             Object.values(faults) | ||||
|                 .forEach(selectedFault => { | ||||
|                     this.openmct.faults.shelveFault(selectedFault, shelveData); | ||||
|                 }); | ||||
|  | ||||
|             this.selectedFaults = {}; | ||||
|         }, | ||||
|         updateFilter(filter) { | ||||
|             this.selectAll(); | ||||
|  | ||||
|             this.filterIndex = filter.model.options.findIndex(option => option.value === filter.value); | ||||
|         }, | ||||
|         updateSearchTerm(term = '') { | ||||
|             this.searchTerm = term.toLowerCase(); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
							
								
								
									
										56
									
								
								src/plugins/faultManagement/FaultManagementObjectProvider.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								src/plugins/faultManagement/FaultManagementObjectProvider.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 { FAULT_MANAGEMENT_TYPE, FAULT_MANAGEMENT_VIEW, FAULT_MANAGEMENT_NAMESPACE } from './constants'; | ||||
|  | ||||
| export default class FaultManagementObjectProvider { | ||||
|     constructor(openmct) { | ||||
|         this.openmct = openmct; | ||||
|         this.namespace = FAULT_MANAGEMENT_NAMESPACE; | ||||
|         this.key = FAULT_MANAGEMENT_VIEW; | ||||
|         this.objects = {}; | ||||
|  | ||||
|         this.createFaultManagementRootObject(); | ||||
|     } | ||||
|  | ||||
|     createFaultManagementRootObject() { | ||||
|         this.rootObject = { | ||||
|             identifier: { | ||||
|                 key: this.key, | ||||
|                 namespace: this.namespace | ||||
|             }, | ||||
|             name: 'Fault Management', | ||||
|             type: FAULT_MANAGEMENT_TYPE, | ||||
|             location: 'ROOT' | ||||
|         }; | ||||
|  | ||||
|         this.openmct.objects.addRoot(this.rootObject.identifier); | ||||
|     } | ||||
|  | ||||
|     get(identifier) { | ||||
|         if (identifier.key === FAULT_MANAGEMENT_VIEW) { | ||||
|             return Promise.resolve(this.rootObject); | ||||
|         } | ||||
|  | ||||
|         return Promise.reject(); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										42
									
								
								src/plugins/faultManagement/FaultManagementPlugin.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/plugins/faultManagement/FaultManagementPlugin.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| /***************************************************************************** | ||||
|  * 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 FaultManagementViewProvider from './FaultManagementViewProvider'; | ||||
| import FaultManagementObjectProvider from './FaultManagementObjectProvider'; | ||||
| import FaultManagementInspectorViewProvider from './FaultManagementInspectorViewProvider'; | ||||
|  | ||||
| import { FAULT_MANAGEMENT_TYPE, FAULT_MANAGEMENT_NAMESPACE } from './constants'; | ||||
|  | ||||
| export default function FaultManagementPlugin() { | ||||
|     return function (openmct) { | ||||
|         openmct.types.addType(FAULT_MANAGEMENT_TYPE, { | ||||
|             name: 'Fault Management', | ||||
|             creatable: false, | ||||
|             description: 'Fault Management View', | ||||
|             cssClass: 'icon-bell' | ||||
|         }); | ||||
|  | ||||
|         openmct.objectViews.addProvider(new FaultManagementViewProvider(openmct)); | ||||
|         openmct.inspectorViews.addProvider(new FaultManagementInspectorViewProvider(openmct)); | ||||
|         openmct.objects.addProvider(FAULT_MANAGEMENT_NAMESPACE, new FaultManagementObjectProvider(openmct)); | ||||
|     }; | ||||
| } | ||||
							
								
								
									
										90
									
								
								src/plugins/faultManagement/FaultManagementSearch.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								src/plugins/faultManagement/FaultManagementSearch.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,90 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| <template> | ||||
| <div class="c-fault-mgmt__search-row"> | ||||
|     <Search | ||||
|         class="c-fault-mgmt-search" | ||||
|         :value="searchTerm" | ||||
|         @input="updateSearchTerm" | ||||
|         @clear="updateSearchTerm" | ||||
|     /> | ||||
|  | ||||
|     <SelectField | ||||
|         class="c-fault-mgmt-viewButton" | ||||
|         title="View Filter" | ||||
|         :model="model" | ||||
|         @onChange="onChange" | ||||
|     /> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import SelectField from '@/api/forms/components/controls/SelectField.vue'; | ||||
| import Search from '@/ui/components/search.vue'; | ||||
|  | ||||
| import { FILTER_ITEMS } from './constants'; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
|         SelectField, | ||||
|         Search | ||||
|     }, | ||||
|     inject: ['openmct', 'domainObject'], | ||||
|     props: { | ||||
|         searchTerm: { | ||||
|             type: String, | ||||
|             default: '' | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             items: [] | ||||
|         }; | ||||
|     }, | ||||
|     computed: { | ||||
|         model() { | ||||
|             return { | ||||
|                 options: this.items, | ||||
|                 value: this.items[0] ? this.items[0].value : FILTER_ITEMS[0].toLowerCase() | ||||
|             }; | ||||
|         } | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.items = FILTER_ITEMS | ||||
|             .map(item => { | ||||
|                 return { | ||||
|                     name: item, | ||||
|                     value: item.toLowerCase() | ||||
|                 }; | ||||
|             }); | ||||
|     }, | ||||
|     methods: { | ||||
|         onChange(data) { | ||||
|             this.$emit('filterChanged', data); | ||||
|         }, | ||||
|         updateSearchTerm(searchTerm) { | ||||
|             this.$emit('updateSearchTerm', searchTerm); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
							
								
								
									
										102
									
								
								src/plugins/faultManagement/FaultManagementToolbar.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								src/plugins/faultManagement/FaultManagementToolbar.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,102 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| <template> | ||||
| <div class="c-fault-mgmt__toolbar"> | ||||
|     <button | ||||
|         class="c-icon-button icon-check" | ||||
|         title="Acknowledge selected faults" | ||||
|         :disabled="disableAcknowledge" | ||||
|         @click="acknowledgeSelected" | ||||
|     > | ||||
|         <div | ||||
|             title="Acknowledge selected faults" | ||||
|             class="c-icon-button__label" | ||||
|         > | ||||
|             Acknowledge | ||||
|         </div> | ||||
|     </button> | ||||
|  | ||||
|     <button | ||||
|         class="c-icon-button icon-timer" | ||||
|         title="Shelve selected faults" | ||||
|         :disabled="disableShelve" | ||||
|         @click="shelveSelected" | ||||
|     > | ||||
|         <div | ||||
|             title="Shelve selected items" | ||||
|             class="c-icon-button__label" | ||||
|         > | ||||
|             Shelve | ||||
|         </div> | ||||
|     </button> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| export default { | ||||
|     inject: ['openmct', 'domainObject'], | ||||
|     props: { | ||||
|         selectedFaults: { | ||||
|             type: Object, | ||||
|             default() { | ||||
|                 return {}; | ||||
|             } | ||||
|         } | ||||
|     }, | ||||
|     data() { | ||||
|         return { | ||||
|             disableAcknowledge: true, | ||||
|             disableShelve: true | ||||
|         }; | ||||
|     }, | ||||
|     watch: { | ||||
|         selectedFaults(newSelectedFaults) { | ||||
|             const selectedfaults = Object.values(newSelectedFaults); | ||||
|  | ||||
|             let disableAcknowledge = true; | ||||
|             let disableShelve = true; | ||||
|  | ||||
|             selectedfaults.forEach(fault => { | ||||
|                 if (!fault.shelved) { | ||||
|                     disableShelve = false; | ||||
|                 } | ||||
|  | ||||
|                 if (!fault.acknowledged) { | ||||
|                     disableAcknowledge = false; | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             this.disableAcknowledge = disableAcknowledge; | ||||
|             this.disableShelve = disableShelve; | ||||
|         } | ||||
|     }, | ||||
|     methods: { | ||||
|         acknowledgeSelected() { | ||||
|             this.$emit('acknowledgeSelected'); | ||||
|         }, | ||||
|         shelveSelected() { | ||||
|             this.$emit('shelveSelected'); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
							
								
								
									
										76
									
								
								src/plugins/faultManagement/FaultManagementView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								src/plugins/faultManagement/FaultManagementView.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2022, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| <template> | ||||
| <FaultManagementListView | ||||
|     :faults-list="faultsList" | ||||
| /> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
|  | ||||
| import FaultManagementListView from './FaultManagementListView.vue'; | ||||
| import { FAULT_MANAGEMENT_ALARMS, FAULT_MANAGEMENT_GLOBAL_ALARMS } from './constants'; | ||||
|  | ||||
| export default { | ||||
|     components: { | ||||
|         FaultManagementListView | ||||
|     }, | ||||
|     inject: ['openmct', 'domainObject'], | ||||
|     data() { | ||||
|         return { | ||||
|             faultsList: [] | ||||
|         }; | ||||
|     }, | ||||
|     mounted() { | ||||
|         this.updateFaultList(); | ||||
|  | ||||
|         this.unsubscribe = this.openmct.faults | ||||
|             .subscribe(this.domainObject, this.updateFault); | ||||
|     }, | ||||
|     beforeDestroy() { | ||||
|         if (this.unsubscribe) { | ||||
|             this.unsubscribe(); | ||||
|         } | ||||
|     }, | ||||
|     methods: { | ||||
|         updateFault({ fault, type }) { | ||||
|             if (type === FAULT_MANAGEMENT_GLOBAL_ALARMS) { | ||||
|                 this.updateFaultList(); | ||||
|             } else if (type === FAULT_MANAGEMENT_ALARMS) { | ||||
|                 this.faultsList.forEach((faultValue, i) => { | ||||
|                     if (fault.id === faultValue.id) { | ||||
|                         this.$set(this.faultsList, i, fault); | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
|         }, | ||||
|         updateFaultList() { | ||||
|             this.openmct.faults | ||||
|                 .request(this.domainObject) | ||||
|                 .then(faultsData => { | ||||
|                     this.faultsList = faultsData.map(fd => fd.fault); | ||||
|                 }); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
| </script> | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user