Compare commits
	
		
			119 Commits
		
	
	
		
			time-condu
			...
			image-tagg
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| ![Mazzella, Jesse D. (ARC-TI)[KBR Wyle Services, LLC]](/assets/img/avatar_default.png)  | 92cba55f88 | ||
| ![Mazzella, Jesse D. (ARC-TI)[KBR Wyle Services, LLC]](/assets/img/avatar_default.png)  | def37263ea | ||
|   | a131fd185a | ||
|   | 929b5fb8f0 | ||
|   | 01cc501f6a | ||
|   | 16dd43d1c8 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 040ef0b998 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | f77287530b | ||
|   | 6878f06b03 | ||
|   | f61eb0a2e9 | ||
|   | a5e3317f8e | ||
|   | 7b0d1d9c8c | ||
|   | 3cc93c0656 | ||
|   | d32913c20f | ||
|   | 15e30f52bc | ||
|   | 076c1425a8 | ||
|   | 5be9a5d04f | ||
|   | 7de80778e3 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | d71287b318 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 943a40680f | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 351e6a0fbf | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 1f514dde3d | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 47121cfbe8 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 44c4d4ff47 | ||
|   | bfa82abc25 | ||
|   | 9a0923801b | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | dc1d046822 | ||
| ![Mazzella, Jesse D. (ARC-TI)[KBR Wyle Services, LLC]](/assets/img/avatar_default.png)  | 11295a8042 | ||
|   | ed7e85c8a0 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | cdb20b9950 | ||
|   | ec90d4d92a | ||
|   | cfe0c7f68e | ||
|   | a9158a90d5 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 07373817b0 | ||
|   | 12b7c0e805 | ||
|   | 39c4e581ac | ||
| ![Mazzella, Jesse D. (ARC-TI)[KBR Wyle Services, LLC]](/assets/img/avatar_default.png)  | 8d8dd34853 | ||
|   | c530c2998a | ||
|   | 9247951456 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 47c5863edf | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 295bfe9294 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 1c6214fe79 | ||
| ![Mazzella, Jesse D. (ARC-TI)[KBR Wyle Services, LLC]](/assets/img/avatar_default.png)  | 37dd237055 | ||
| ![Mazzella, Jesse D. (ARC-TI)[KBR Wyle Services, LLC]](/assets/img/avatar_default.png)  | c7542d0052 | ||
| ![Mazzella, Jesse D. (ARC-TI)[KBR Wyle Services, LLC]](/assets/img/avatar_default.png)  | f4007d3dfc | ||
| ![Mazzella, Jesse D. (ARC-TI)[KBR Wyle Services, LLC]](/assets/img/avatar_default.png)  | b51654efbb | ||
| ![Mazzella, Jesse D. (ARC-TI)[KBR Wyle Services, LLC]](/assets/img/avatar_default.png)  | 3f2c63c9d7 | ||
|   | e35d7a085b | ||
| ![Mazzella, Jesse D. (ARC-TI)[KBR Wyle Services, LLC]](/assets/img/avatar_default.png)  | 5152583c3b | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 4cab97cb4b | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 0bafdad605 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 4d375ec765 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 47b44cebba | ||
|   | fbb37ac382 | ||
|   | fea68381a7 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 356c90ca45 | ||
|   | 71c1fdc298 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 7e12a45960 | ||
|   | efb55ecafc | ||
|   | 609dcb0460 | ||
|   | 3bff7f9f32 | ||
|   | 09df2f64f2 | ||
|   | 6b2adcb7b7 | ||
|   | 2f310a3432 | ||
|   | 02855d2c9c | ||
|   | 6dd6c87ceb | ||
|   | 8945f27eed | ||
|   | 5ddf8a8ff4 | ||
|   | 3a77efb010 | ||
|   | bd356653db | ||
|   | d1e8b3835d | ||
|   | 051a0adbb7 | ||
|   | 87d695a454 | ||
|   | 85482902be | ||
|   | 8015aceaa7 | ||
|   | a381673f21 | ||
|   | 7a808622ae | ||
|   | 5fb78a9604 | ||
|   | 629e884c9b | ||
|   | a8949d39bf | ||
|   | 91ad130f8b | ||
|   | 72d8779736 | ||
|   | e34093eda7 | ||
|   | 501fdf902b | ||
|   | cb32dd94f8 | ||
|   | 2d868cdb58 | ||
|   | 424a2b30ac | ||
|   | 693b8804ba | ||
|   | 9018dcd319 | ||
|   | 0aaa7998f5 | ||
|   | d426ae86b8 | ||
|   | 002d8d11e8 | ||
|   | 804dbf0cab | ||
|   | caa7bc6fae | ||
|   | 172e0b23fd | ||
|   | 5df7971438 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | b39d5e8bcc | ||
|   | c5188397e4 | ||
|   | 225fa22c72 | ||
|   | 2c3b6fa540 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 496ab4d5a3 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | aad9e51262 | ||
|   | ba4353aacb | ||
|   | 9f079255f1 | ||
|   | f5eacc504b | ||
|   | 26fa1653e3 | ||
|   | b7c68f715b | ||
|   | 549a579bf3 | ||
|   | fe677fa359 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 1bbc3789ec | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 636849885b | ||
|   | 6f2b20eee9 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | e38821cc1f | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 4345d216f7 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 84a12c7833 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | ad8445114f | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | bcd50dfa35 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | a798ddf05e | ||
|   | 7af7e68779 | 
| @@ -13,12 +13,12 @@ executors: | ||||
|       docker_layer_caching: true | ||||
| parameters: | ||||
|   BUST_CACHE: | ||||
|     description: "Set this with the CircleCI UI Trigger Workflow button (boolean = true) to bust the cache!" | ||||
|     description: 'Set this with the CircleCI UI Trigger Workflow button (boolean = true) to bust the cache!' | ||||
|     default: false | ||||
|     type: boolean | ||||
| commands: | ||||
|   build_and_install: | ||||
|     description: "All steps used to build and install. Will use cache if found" | ||||
|     description: 'All steps used to build and install. Will use cache if found' | ||||
|     parameters: | ||||
|       node-version: | ||||
|         type: string | ||||
| @@ -30,19 +30,19 @@ commands: | ||||
|           node-version: << parameters.node-version >> | ||||
|       - run: npm install --no-audit --progress=false | ||||
|   restore_cache_cmd: | ||||
|     description: "Custom command for restoring cache with the ability to bust cache. When BUST_CACHE is set to true, jobs will not restore cache" | ||||
|     description: 'Custom command for restoring cache with the ability to bust cache. When BUST_CACHE is set to true, jobs will not restore cache' | ||||
|     parameters: | ||||
|       node-version: | ||||
|         type: string | ||||
|     steps: | ||||
|       - when: | ||||
|           condition: | ||||
|             equal: [false, << pipeline.parameters.BUST_CACHE >> ] | ||||
|             equal: [false, << pipeline.parameters.BUST_CACHE >>] | ||||
|           steps: | ||||
|             - restore_cache: | ||||
|                 key: deps--{{ arch }}--{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}-{{ checksum ".circleci/config.yml" }} | ||||
|   save_cache_cmd: | ||||
|     description: "Custom command for saving cache." | ||||
|     description: 'Custom command for saving cache.' | ||||
|     parameters: | ||||
|       node-version: | ||||
|         type: string | ||||
| @@ -53,7 +53,7 @@ commands: | ||||
|             - ~/.npm | ||||
|             - node_modules | ||||
|   generate_and_store_version_and_filesystem_artifacts: | ||||
|     description: "Track important packages and files" | ||||
|     description: 'Track important packages and files' | ||||
|     steps: | ||||
|       - run: | | ||||
|           [[ $EUID -ne 0 ]] && (sudo mkdir -p /tmp/artifacts && sudo chmod 777 /tmp/artifacts) || (mkdir -p /tmp/artifacts && chmod 777 /tmp/artifacts) | ||||
| @@ -64,13 +64,13 @@ commands: | ||||
|       - store_artifacts: | ||||
|           path: /tmp/artifacts/ | ||||
|   generate_e2e_code_cov_report: | ||||
|    description: "Generate e2e code coverage artifacts and publish to codecov.io. Needed to that we can ignore the exit code status of the npm run test" | ||||
|    parameters: | ||||
|     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 || true | ||||
|     - run: npm run cov:e2e:<<parameters.suite>>:publish | ||||
|     steps: | ||||
|       - run: npm run cov:e2e:report || true | ||||
|       - run: npm run cov:e2e:<<parameters.suite>>:publish | ||||
| orbs: | ||||
|   node: circleci/node@5.1.0 | ||||
|   browser-tools: circleci/browser-tools@1.3.0 | ||||
| @@ -115,7 +115,7 @@ jobs: | ||||
|           path: coverage | ||||
|       - when: | ||||
|           condition: | ||||
|             equal: [ 42, 42 ] # Always generate version artifacts regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2 | ||||
|             equal: [42, 42] # Always generate version artifacts regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2 | ||||
|           steps: | ||||
|             - generate_and_store_version_and_filesystem_artifacts | ||||
|   e2e-test: | ||||
| @@ -131,16 +131,16 @@ jobs: | ||||
|           node-version: <<parameters.node-version>> | ||||
|       - when: #Only install chrome-beta when running the 'full' suite to save $$$ | ||||
|           condition: | ||||
|             equal: [ "full", <<parameters.suite>> ] | ||||
|             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} | ||||
|       - when: | ||||
|           condition: | ||||
|             equal: [ 42, 42 ] # Always run codecov reports regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2 | ||||
|             equal: [42, 42] # Always run codecov reports regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2 | ||||
|           steps: | ||||
|           - generate_e2e_code_cov_report: | ||||
|               suite: <<parameters.suite>>           | ||||
|             - generate_e2e_code_cov_report: | ||||
|                 suite: <<parameters.suite>> | ||||
|       - store_test_results: | ||||
|           path: test-results/results.xml | ||||
|       - store_artifacts: | ||||
| @@ -151,7 +151,7 @@ jobs: | ||||
|           path: html-test-results | ||||
|       - when: | ||||
|           condition: | ||||
|             equal: [ 42, 42 ] # Always generate version artifacts regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2 | ||||
|             equal: [42, 42] # Always generate version artifacts regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2 | ||||
|           steps: | ||||
|             - generate_and_store_version_and_filesystem_artifacts | ||||
|   e2e-couchdb: | ||||
| @@ -168,14 +168,14 @@ jobs: | ||||
|           docker-compose -f src/plugins/persistence/couch/couchdb-compose.yaml up --detach | ||||
|           sleep 3 | ||||
|           bash src/plugins/persistence/couch/setup-couchdb.sh | ||||
|       - run: sh src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh #Replace LocalStorage Plugin with CouchDB  | ||||
|       - run: sh src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh #Replace LocalStorage Plugin with CouchDB | ||||
|       - run: npm run test:e2e:couchdb | ||||
|       - when: | ||||
|           condition: | ||||
|             equal: [ 42, 42 ] # Always run codecov reports regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2 | ||||
|             equal: [42, 42] # Always run codecov reports regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2 | ||||
|           steps: | ||||
|           - generate_e2e_code_cov_report: | ||||
|               suite: full #add to full suite | ||||
|             - generate_e2e_code_cov_report: | ||||
|                 suite: full #add to full suite | ||||
|       - store_test_results: | ||||
|           path: test-results/results.xml | ||||
|       - store_artifacts: | ||||
| @@ -186,7 +186,7 @@ jobs: | ||||
|           path: html-test-results | ||||
|       - when: | ||||
|           condition: | ||||
|             equal: [ 42, 42 ] # Always generate version artifacts regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2 | ||||
|             equal: [42, 42] # Always generate version artifacts regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2 | ||||
|           steps: | ||||
|             - generate_and_store_version_and_filesystem_artifacts | ||||
|   perf-test: | ||||
| @@ -206,7 +206,7 @@ jobs: | ||||
|           path: html-test-results | ||||
|       - when: | ||||
|           condition: | ||||
|             equal: [ 42, 42 ] # Always run codecov reports regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2 | ||||
|             equal: [42, 42] # Always run codecov reports regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2 | ||||
|           steps: | ||||
|             - generate_and_store_version_and_filesystem_artifacts | ||||
|   visual-test: | ||||
| @@ -226,7 +226,7 @@ jobs: | ||||
|           path: html-test-results | ||||
|       - when: | ||||
|           condition: | ||||
|             equal: [ 42, 42 ] # Always generate version artifacts regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2 | ||||
|             equal: [42, 42] # Always generate version artifacts regardless of test failure https://discuss.circleci.com/t/make-custom-command-run-always-with-when-always/38957/2 | ||||
|           steps: | ||||
|             - generate_and_store_version_and_filesystem_artifacts | ||||
| workflows: | ||||
| @@ -246,7 +246,7 @@ workflows: | ||||
|           node-version: lts/hydrogen | ||||
|       - visual-test: | ||||
|           node-version: lts/hydrogen | ||||
|          | ||||
|  | ||||
|   the-nightly: #These jobs do not run on PRs, but against master at night | ||||
|     jobs: | ||||
|       - unit-test: | ||||
| @@ -269,7 +269,7 @@ workflows: | ||||
|           node-version: lts/hydrogen | ||||
|     triggers: | ||||
|       - schedule: | ||||
|           cron: "0 0 * * *" | ||||
|           cron: '0 0 * * *' | ||||
|           filters: | ||||
|             branches: | ||||
|               only: | ||||
|   | ||||
							
								
								
									
										425
									
								
								.eslintrc.js
									
									
									
									
									
								
							
							
						
						
									
										425
									
								
								.eslintrc.js
									
									
									
									
									
								
							| @@ -1,271 +1,166 @@ | ||||
| const LEGACY_FILES = ["example/**"]; | ||||
| const LEGACY_FILES = ['example/**']; | ||||
| module.exports = { | ||||
|     "env": { | ||||
|         "browser": true, | ||||
|         "es6": true, | ||||
|         "jasmine": true, | ||||
|         "amd": true | ||||
|     }, | ||||
|     "globals": { | ||||
|         "_": "readonly" | ||||
|     }, | ||||
|     "extends": [ | ||||
|         "eslint:recommended", | ||||
|         "plugin:compat/recommended", | ||||
|         "plugin:vue/recommended", | ||||
|         "plugin:you-dont-need-lodash-underscore/compatible" | ||||
|   env: { | ||||
|     browser: true, | ||||
|     es6: true, | ||||
|     jasmine: true, | ||||
|     amd: true | ||||
|   }, | ||||
|   globals: { | ||||
|     _: 'readonly' | ||||
|   }, | ||||
|   plugins: ['prettier'], | ||||
|   extends: [ | ||||
|     'eslint:recommended', | ||||
|     'plugin:compat/recommended', | ||||
|     'plugin:vue/recommended', | ||||
|     'plugin:you-dont-need-lodash-underscore/compatible', | ||||
|     'plugin:prettier/recommended' | ||||
|   ], | ||||
|   parser: 'vue-eslint-parser', | ||||
|   parserOptions: { | ||||
|     parser: '@babel/eslint-parser', | ||||
|     requireConfigFile: false, | ||||
|     allowImportExportEverywhere: true, | ||||
|     ecmaVersion: 2015, | ||||
|     ecmaFeatures: { | ||||
|       impliedStrict: true | ||||
|     } | ||||
|   }, | ||||
|   rules: { | ||||
|     'prettier/prettier': 'error', | ||||
|     'you-dont-need-lodash-underscore/omit': 'off', | ||||
|     'you-dont-need-lodash-underscore/throttle': 'off', | ||||
|     'you-dont-need-lodash-underscore/flatten': 'off', | ||||
|     'you-dont-need-lodash-underscore/get': 'off', | ||||
|     'no-bitwise': 'error', | ||||
|     curly: 'error', | ||||
|     eqeqeq: 'error', | ||||
|     'guard-for-in': 'error', | ||||
|     'no-extend-native': 'error', | ||||
|     'no-inner-declarations': 'off', | ||||
|     'no-use-before-define': ['error', 'nofunc'], | ||||
|     'no-caller': 'error', | ||||
|     'no-irregular-whitespace': 'error', | ||||
|     'no-new': 'error', | ||||
|     'no-shadow': 'error', | ||||
|     'no-undef': 'error', | ||||
|     'no-unused-vars': [ | ||||
|       'error', | ||||
|       { | ||||
|         vars: 'all', | ||||
|         args: 'none' | ||||
|       } | ||||
|     ], | ||||
|     "parser": "vue-eslint-parser", | ||||
|     "parserOptions": { | ||||
|         "parser": "@babel/eslint-parser", | ||||
|         "requireConfigFile": false, | ||||
|         "allowImportExportEverywhere": true, | ||||
|         "ecmaVersion": 2015, | ||||
|         "ecmaFeatures": { | ||||
|             "impliedStrict": true | ||||
|         } | ||||
|     }, | ||||
|     "rules": { | ||||
|         "you-dont-need-lodash-underscore/omit": "off", | ||||
|         "you-dont-need-lodash-underscore/throttle": "off", | ||||
|         "you-dont-need-lodash-underscore/flatten": "off", | ||||
|         "you-dont-need-lodash-underscore/get": "off", | ||||
|         "no-bitwise": "error", | ||||
|         "curly": "error", | ||||
|         "eqeqeq": "error", | ||||
|         "guard-for-in": "error", | ||||
|         "no-extend-native": "error", | ||||
|         "no-inner-declarations": "off", | ||||
|         "no-use-before-define": ["error", "nofunc"], | ||||
|         "no-caller": "error", | ||||
|         "no-irregular-whitespace": "error", | ||||
|         "no-new": "error", | ||||
|         "no-shadow": "error", | ||||
|         "no-undef": "error", | ||||
|         "no-unused-vars": [ | ||||
|             "error", | ||||
|             { | ||||
|                 "vars": "all", | ||||
|                 "args": "none" | ||||
|             } | ||||
|         ], | ||||
|         "no-console": "off", | ||||
|         "no-trailing-spaces": "error", | ||||
|         "space-before-function-paren": [ | ||||
|             "error", | ||||
|             { | ||||
|                 "anonymous": "always", | ||||
|                 "asyncArrow": "always", | ||||
|                 "named": "never" | ||||
|             } | ||||
|         ], | ||||
|         "array-bracket-spacing": "error", | ||||
|         "space-in-parens": "error", | ||||
|         "space-before-blocks": "error", | ||||
|         "comma-dangle": "error", | ||||
|         "eol-last": "error", | ||||
|         "new-cap": [ | ||||
|             "error", | ||||
|             { | ||||
|                 "capIsNew": false, | ||||
|                 "properties": false | ||||
|             } | ||||
|         ], | ||||
|         "dot-notation": "error", | ||||
|         "indent": ["error", 4], | ||||
|     'no-console': 'off', | ||||
|     'new-cap': [ | ||||
|       'error', | ||||
|       { | ||||
|         capIsNew: false, | ||||
|         properties: false | ||||
|       } | ||||
|     ], | ||||
|     'dot-notation': 'error', | ||||
|  | ||||
|         // https://eslint.org/docs/rules/no-case-declarations | ||||
|         "no-case-declarations": "error", | ||||
|         // https://eslint.org/docs/rules/max-classes-per-file | ||||
|         "max-classes-per-file": ["error", 1], | ||||
|         // https://eslint.org/docs/rules/no-eq-null | ||||
|         "no-eq-null": "error", | ||||
|         // https://eslint.org/docs/rules/no-eval | ||||
|         "no-eval": "error", | ||||
|         // https://eslint.org/docs/rules/no-floating-decimal | ||||
|         "no-floating-decimal": "error", | ||||
|         // https://eslint.org/docs/rules/no-implicit-globals | ||||
|         "no-implicit-globals": "error", | ||||
|         // https://eslint.org/docs/rules/no-implied-eval | ||||
|         "no-implied-eval": "error", | ||||
|         // https://eslint.org/docs/rules/no-lone-blocks | ||||
|         "no-lone-blocks": "error", | ||||
|         // https://eslint.org/docs/rules/no-loop-func | ||||
|         "no-loop-func": "error", | ||||
|         // https://eslint.org/docs/rules/no-new-func | ||||
|         "no-new-func": "error", | ||||
|         // https://eslint.org/docs/rules/no-new-wrappers | ||||
|         "no-new-wrappers": "error", | ||||
|         // https://eslint.org/docs/rules/no-octal-escape | ||||
|         "no-octal-escape": "error", | ||||
|         // https://eslint.org/docs/rules/no-proto | ||||
|         "no-proto": "error", | ||||
|         // https://eslint.org/docs/rules/no-return-await | ||||
|         "no-return-await": "error", | ||||
|         // https://eslint.org/docs/rules/no-script-url | ||||
|         "no-script-url": "error", | ||||
|         // https://eslint.org/docs/rules/no-self-compare | ||||
|         "no-self-compare": "error", | ||||
|         // https://eslint.org/docs/rules/no-sequences | ||||
|         "no-sequences": "error", | ||||
|         // https://eslint.org/docs/rules/no-unmodified-loop-condition | ||||
|         "no-unmodified-loop-condition": "error", | ||||
|         // https://eslint.org/docs/rules/no-useless-call | ||||
|         "no-useless-call": "error", | ||||
|         // https://eslint.org/docs/rules/wrap-iife | ||||
|         "wrap-iife": "error", | ||||
|         // https://eslint.org/docs/rules/no-nested-ternary | ||||
|         "no-nested-ternary": "error", | ||||
|         // https://eslint.org/docs/rules/switch-colon-spacing | ||||
|         "switch-colon-spacing": "error", | ||||
|         // https://eslint.org/docs/rules/no-useless-computed-key | ||||
|         "no-useless-computed-key": "error", | ||||
|         // https://eslint.org/docs/rules/rest-spread-spacing | ||||
|         "rest-spread-spacing": ["error"], | ||||
|         // https://eslint.org/docs/rules/no-var | ||||
|         "no-var": "error", | ||||
|         // https://eslint.org/docs/rules/one-var | ||||
|         "one-var": ["error", "never"], | ||||
|         // https://eslint.org/docs/rules/default-case-last | ||||
|         "default-case-last": "error", | ||||
|         // https://eslint.org/docs/rules/default-param-last | ||||
|         "default-param-last": "error", | ||||
|         // https://eslint.org/docs/rules/grouped-accessor-pairs | ||||
|         "grouped-accessor-pairs": "error", | ||||
|         // https://eslint.org/docs/rules/no-constructor-return | ||||
|         "no-constructor-return": "error", | ||||
|         // https://eslint.org/docs/rules/array-callback-return | ||||
|         "array-callback-return": "error", | ||||
|         // https://eslint.org/docs/rules/no-invalid-this | ||||
|         "no-invalid-this": "error", // Believe this one actually surfaces some bugs | ||||
|         // https://eslint.org/docs/rules/func-style | ||||
|         "func-style": ["error", "declaration"], | ||||
|         // https://eslint.org/docs/rules/no-unused-expressions | ||||
|         "no-unused-expressions": "error", | ||||
|         // https://eslint.org/docs/rules/no-useless-concat | ||||
|         "no-useless-concat": "error", | ||||
|         // https://eslint.org/docs/rules/radix | ||||
|         "radix": "error", | ||||
|         // https://eslint.org/docs/rules/require-await | ||||
|         "require-await": "error", | ||||
|         // https://eslint.org/docs/rules/no-alert | ||||
|         "no-alert": "error", | ||||
|         // https://eslint.org/docs/rules/no-useless-constructor | ||||
|         "no-useless-constructor": "error", | ||||
|         // https://eslint.org/docs/rules/no-duplicate-imports | ||||
|         "no-duplicate-imports": "error", | ||||
|     // https://eslint.org/docs/rules/no-case-declarations | ||||
|     'no-case-declarations': 'error', | ||||
|     // https://eslint.org/docs/rules/max-classes-per-file | ||||
|     'max-classes-per-file': ['error', 1], | ||||
|     // https://eslint.org/docs/rules/no-eq-null | ||||
|     'no-eq-null': 'error', | ||||
|     // https://eslint.org/docs/rules/no-eval | ||||
|     'no-eval': 'error', | ||||
|     // https://eslint.org/docs/rules/no-implicit-globals | ||||
|     'no-implicit-globals': 'error', | ||||
|     // https://eslint.org/docs/rules/no-implied-eval | ||||
|     'no-implied-eval': 'error', | ||||
|     // https://eslint.org/docs/rules/no-lone-blocks | ||||
|     'no-lone-blocks': 'error', | ||||
|     // https://eslint.org/docs/rules/no-loop-func | ||||
|     'no-loop-func': 'error', | ||||
|     // https://eslint.org/docs/rules/no-new-func | ||||
|     'no-new-func': 'error', | ||||
|     // https://eslint.org/docs/rules/no-new-wrappers | ||||
|     'no-new-wrappers': 'error', | ||||
|     // https://eslint.org/docs/rules/no-octal-escape | ||||
|     'no-octal-escape': 'error', | ||||
|     // https://eslint.org/docs/rules/no-proto | ||||
|     'no-proto': 'error', | ||||
|     // https://eslint.org/docs/rules/no-return-await | ||||
|     'no-return-await': 'error', | ||||
|     // https://eslint.org/docs/rules/no-script-url | ||||
|     'no-script-url': 'error', | ||||
|     // https://eslint.org/docs/rules/no-self-compare | ||||
|     'no-self-compare': 'error', | ||||
|     // https://eslint.org/docs/rules/no-sequences | ||||
|     'no-sequences': 'error', | ||||
|     // https://eslint.org/docs/rules/no-unmodified-loop-condition | ||||
|     'no-unmodified-loop-condition': 'error', | ||||
|     // https://eslint.org/docs/rules/no-useless-call | ||||
|     'no-useless-call': 'error', | ||||
|     // https://eslint.org/docs/rules/no-nested-ternary | ||||
|     'no-nested-ternary': 'error', | ||||
|     // https://eslint.org/docs/rules/no-useless-computed-key | ||||
|     'no-useless-computed-key': 'error', | ||||
|     // https://eslint.org/docs/rules/no-var | ||||
|     'no-var': 'error', | ||||
|     // https://eslint.org/docs/rules/one-var | ||||
|     'one-var': ['error', 'never'], | ||||
|     // https://eslint.org/docs/rules/default-case-last | ||||
|     'default-case-last': 'error', | ||||
|     // https://eslint.org/docs/rules/default-param-last | ||||
|     'default-param-last': 'error', | ||||
|     // https://eslint.org/docs/rules/grouped-accessor-pairs | ||||
|     'grouped-accessor-pairs': 'error', | ||||
|     // https://eslint.org/docs/rules/no-constructor-return | ||||
|     'no-constructor-return': 'error', | ||||
|     // https://eslint.org/docs/rules/array-callback-return | ||||
|     'array-callback-return': 'error', | ||||
|     // https://eslint.org/docs/rules/no-invalid-this | ||||
|     'no-invalid-this': 'error', // Believe this one actually surfaces some bugs | ||||
|     // https://eslint.org/docs/rules/func-style | ||||
|     'func-style': ['error', 'declaration'], | ||||
|     // https://eslint.org/docs/rules/no-unused-expressions | ||||
|     'no-unused-expressions': 'error', | ||||
|     // https://eslint.org/docs/rules/no-useless-concat | ||||
|     'no-useless-concat': 'error', | ||||
|     // https://eslint.org/docs/rules/radix | ||||
|     radix: 'error', | ||||
|     // https://eslint.org/docs/rules/require-await | ||||
|     'require-await': 'error', | ||||
|     // https://eslint.org/docs/rules/no-alert | ||||
|     'no-alert': 'error', | ||||
|     // https://eslint.org/docs/rules/no-useless-constructor | ||||
|     'no-useless-constructor': 'error', | ||||
|     // https://eslint.org/docs/rules/no-duplicate-imports | ||||
|     'no-duplicate-imports': 'error', | ||||
|  | ||||
|         // https://eslint.org/docs/rules/no-implicit-coercion | ||||
|         "no-implicit-coercion": "error", | ||||
|         //https://eslint.org/docs/rules/no-unneeded-ternary | ||||
|         "no-unneeded-ternary": "error", | ||||
|         // https://eslint.org/docs/rules/semi | ||||
|         "semi": ["error", "always"], | ||||
|         // https://eslint.org/docs/rules/no-multi-spaces | ||||
|         "no-multi-spaces": "error", | ||||
|         // https://eslint.org/docs/rules/key-spacing | ||||
|         "key-spacing": ["error", { | ||||
|             "afterColon": true | ||||
|         }], | ||||
|         // https://eslint.org/docs/rules/keyword-spacing | ||||
|         "keyword-spacing": ["error", { | ||||
|             "before": true, | ||||
|             "after": true | ||||
|         }], | ||||
|         // https://eslint.org/docs/rules/comma-spacing | ||||
|         // Also requires one line code fix | ||||
|         "comma-spacing": ["error", { | ||||
|             "after": true | ||||
|         }], | ||||
|         //https://eslint.org/docs/rules/no-whitespace-before-property | ||||
|         "no-whitespace-before-property": "error", | ||||
|         // https://eslint.org/docs/rules/object-curly-newline | ||||
|         "object-curly-newline": ["error", { | ||||
|             "consistent": true, | ||||
|             "multiline": true | ||||
|         }], | ||||
|         // https://eslint.org/docs/rules/object-property-newline | ||||
|         "object-property-newline": "error", | ||||
|         // https://eslint.org/docs/rules/brace-style | ||||
|         "brace-style": "error", | ||||
|         // https://eslint.org/docs/rules/no-multiple-empty-lines | ||||
|         "no-multiple-empty-lines": ["error", {"max": 1}], | ||||
|         // https://eslint.org/docs/rules/operator-linebreak | ||||
|         "operator-linebreak": ["error", "before", {"overrides": {"=": "after"}}], | ||||
|         // https://eslint.org/docs/rules/padding-line-between-statements | ||||
|         "padding-line-between-statements": ["error", { | ||||
|             "blankLine": "always", | ||||
|             "prev": "multiline-block-like", | ||||
|             "next": "*" | ||||
|         }, { | ||||
|             "blankLine": "always", | ||||
|             "prev": "*", | ||||
|             "next": "return" | ||||
|         }], | ||||
|         // https://eslint.org/docs/rules/space-infix-ops | ||||
|         "space-infix-ops": "error", | ||||
|         // https://eslint.org/docs/rules/space-unary-ops | ||||
|         "space-unary-ops": ["error", { | ||||
|             "words": true, | ||||
|             "nonwords": false | ||||
|         }], | ||||
|         // https://eslint.org/docs/rules/arrow-spacing | ||||
|         "arrow-spacing": "error", | ||||
|         // https://eslint.org/docs/rules/semi-spacing | ||||
|         "semi-spacing": ["error", { | ||||
|             "before": false, | ||||
|             "after": true | ||||
|         }], | ||||
|  | ||||
|         "vue/html-indent": [ | ||||
|             "error", | ||||
|             4, | ||||
|             { | ||||
|                 "attribute": 1, | ||||
|                 "baseIndent": 0, | ||||
|                 "closeBracket": 0, | ||||
|                 "alignAttributesVertically": true, | ||||
|                 "ignores": [] | ||||
|             } | ||||
|     // https://eslint.org/docs/rules/no-implicit-coercion | ||||
|     'no-implicit-coercion': 'error', | ||||
|     //https://eslint.org/docs/rules/no-unneeded-ternary | ||||
|     'no-unneeded-ternary': 'error', | ||||
|     'vue/first-attribute-linebreak': 'error', | ||||
|     'vue/multiline-html-element-content-newline': 'off', | ||||
|     'vue/singleline-html-element-content-newline': 'off', | ||||
|     'vue/multi-word-component-names': 'off', // TODO enable, align with conventions | ||||
|     'vue/no-mutating-props': 'off' | ||||
|   }, | ||||
|   overrides: [ | ||||
|     { | ||||
|       files: LEGACY_FILES, | ||||
|       rules: { | ||||
|         'no-unused-vars': [ | ||||
|           'warn', | ||||
|           { | ||||
|             vars: 'all', | ||||
|             args: 'none', | ||||
|             varsIgnorePattern: 'controller' | ||||
|           } | ||||
|         ], | ||||
|         "vue/html-self-closing": ["error", | ||||
|             { | ||||
|                 "html": { | ||||
|                     "void": "never", | ||||
|                     "normal": "never", | ||||
|                     "component": "always" | ||||
|                 }, | ||||
|                 "svg": "always", | ||||
|                 "math": "always" | ||||
|             } | ||||
|         ], | ||||
|         "vue/max-attributes-per-line": ["error", { | ||||
|             "singleline": 1, | ||||
|             "multiline": 1, | ||||
|         }], | ||||
|         "vue/first-attribute-linebreak": "error", | ||||
|         "vue/multiline-html-element-content-newline": "off", | ||||
|         "vue/singleline-html-element-content-newline": "off", | ||||
|         "vue/multi-word-component-names": "off", // TODO enable, align with conventions | ||||
|         "vue/no-mutating-props": "off" | ||||
|  | ||||
|     }, | ||||
|     "overrides": [ | ||||
|         { | ||||
|             "files": LEGACY_FILES, | ||||
|             "rules": { | ||||
|                 "no-unused-vars": [ | ||||
|                     "warn", | ||||
|                     { | ||||
|                         "vars": "all", | ||||
|                         "args": "none", | ||||
|                         "varsIgnorePattern": "controller" | ||||
|                     } | ||||
|                 ], | ||||
|                 "no-nested-ternary": "off", | ||||
|                 "no-var": "off", | ||||
|                 "one-var": "off" | ||||
|             } | ||||
|         } | ||||
|     ] | ||||
|         'no-nested-ternary': 'off', | ||||
|         'no-var': 'off', | ||||
|         'one-var': 'off' | ||||
|       } | ||||
|     } | ||||
|   ] | ||||
| }; | ||||
|   | ||||
							
								
								
									
										12
									
								
								.git-blame-ignore-revs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								.git-blame-ignore-revs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| # git-blame ignored revisions | ||||
| # To configure, run: | ||||
| #   git config blame.ignoreRevsFile .git-blame-ignore-revs | ||||
| # Requires Git > 2.23 | ||||
| # See https://git-scm.com/docs/git-blame#Documentation/git-blame.txt---ignore-revs-fileltfilegt | ||||
|  | ||||
| # Copyright year update 2022 | ||||
| 4a9744e916d24122a81092f6b7950054048ba860 | ||||
| # Copyright year update 2023 | ||||
| 8040b275fcf2ba71b42cd72d4daa64bb25c19c2d | ||||
| # Apply `prettier` formatting | ||||
| caa7bc6faebc204f67aedae3e35fb0d0d3ce27a7 | ||||
							
								
								
									
										57
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										57
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,39 +1,38 @@ | ||||
|  | ||||
| version: 2 | ||||
| updates: | ||||
|   - package-ecosystem: "npm" | ||||
|     directory: "/" | ||||
|   - package-ecosystem: 'npm' | ||||
|     directory: '/' | ||||
|     schedule: | ||||
|       interval: "weekly"   | ||||
|       interval: 'weekly' | ||||
|     open-pull-requests-limit: 10 | ||||
|     labels: | ||||
|       - "pr:daveit" | ||||
|       - "pr:e2e" | ||||
|       - "type:maintenance" | ||||
|       - "dependencies" | ||||
|       - "pr:platform" | ||||
|       - 'pr:daveit' | ||||
|       - 'pr:e2e' | ||||
|       - 'type:maintenance' | ||||
|       - 'dependencies' | ||||
|       - 'pr:platform' | ||||
|     ignore: | ||||
|       #We have to source the playwright container which is not detected by Dependabot | ||||
|       - dependency-name: "@playwright/test" | ||||
|       - dependency-name: "playwright-core" | ||||
|       - dependency-name: '@playwright/test' | ||||
|       - dependency-name: 'playwright-core' | ||||
|       #Lots of noise in these type patch releases. | ||||
|       - dependency-name: "@babel/eslint-parser" | ||||
|         update-types: ["version-update:semver-patch"]  | ||||
|       - dependency-name: "eslint-plugin-vue" | ||||
|         update-types: ["version-update:semver-patch"] | ||||
|       - dependency-name: "babel-loader" | ||||
|         update-types: ["version-update:semver-patch"] | ||||
|       - dependency-name: "sinon" | ||||
|         update-types: ["version-update:semver-patch"] | ||||
|       - dependency-name: "moment-timezone" | ||||
|         update-types: ["version-update:semver-patch"] | ||||
|       - dependency-name: "@types/lodash" | ||||
|         update-types: ["version-update:semver-patch"] | ||||
|   - package-ecosystem: "github-actions" | ||||
|     directory: "/" | ||||
|       - dependency-name: '@babel/eslint-parser' | ||||
|         update-types: ['version-update:semver-patch'] | ||||
|       - dependency-name: 'eslint-plugin-vue' | ||||
|         update-types: ['version-update:semver-patch'] | ||||
|       - dependency-name: 'babel-loader' | ||||
|         update-types: ['version-update:semver-patch'] | ||||
|       - dependency-name: 'sinon' | ||||
|         update-types: ['version-update:semver-patch'] | ||||
|       - dependency-name: 'moment-timezone' | ||||
|         update-types: ['version-update:semver-patch'] | ||||
|       - dependency-name: '@types/lodash' | ||||
|         update-types: ['version-update:semver-patch'] | ||||
|   - package-ecosystem: 'github-actions' | ||||
|     directory: '/' | ||||
|     schedule: | ||||
|       interval: "daily"     | ||||
|       interval: 'daily' | ||||
|     labels: | ||||
|       - "pr:daveit" | ||||
|       - "type:maintenance" | ||||
|       - "dependencies" | ||||
|       - 'pr:daveit' | ||||
|       - 'type:maintenance' | ||||
|       - 'dependencies' | ||||
|   | ||||
							
								
								
									
										25
									
								
								.github/workflows/e2e-couchdb.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										25
									
								
								.github/workflows/e2e-couchdb.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,4 +1,4 @@ | ||||
| name: "e2e-couchdb" | ||||
| name: 'e2e-couchdb' | ||||
| on: | ||||
|   workflow_dispatch: | ||||
|   pull_request: | ||||
| @@ -17,7 +17,7 @@ jobs: | ||||
|       - run: npx playwright@1.32.3 install | ||||
|       - run: npm install | ||||
|       - name: Start CouchDB Docker Container and Init with Setup Scripts | ||||
|         run : | | ||||
|         run: | | ||||
|           export $(cat src/plugins/persistence/couch/.env.ci | xargs) | ||||
|           docker-compose -f src/plugins/persistence/couch/couchdb-compose.yaml up --detach | ||||
|           sleep 3 | ||||
| @@ -25,10 +25,10 @@ jobs: | ||||
|           bash src/plugins/persistence/couch/replace-localstorage-with-couchdb-indexhtml.sh | ||||
|       - name: Run CouchDB Tests and publish to deploysentinel | ||||
|         env: | ||||
|           DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }}  | ||||
|           DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }} | ||||
|         run: npm run test:e2e:couchdb | ||||
|       - name: Publish Results to Codecov.io | ||||
|         env:  | ||||
|         env: | ||||
|           SUPER_SECRET: ${{ secrets.CODECOV_TOKEN }} | ||||
|         run: npm run cov:e2e:full:publish | ||||
|       - name: Archive test results | ||||
| @@ -41,3 +41,20 @@ jobs: | ||||
|         uses: actions/upload-artifact@v3 | ||||
|         with: | ||||
|           path: html-test-results | ||||
|       - name: Remove pr:e2e:couchdb label (if present) | ||||
|         if: ${{ contains(github.event.pull_request.labels.*.name, 'pr:e2e:couchdb') }} | ||||
|         uses: actions/github-script@v6 | ||||
|         with: | ||||
|           script: | | ||||
|             const { owner, repo, number } = context.issue; | ||||
|             const labelToRemove = 'pr:e2e:couchdb'; | ||||
|             try { | ||||
|               await github.rest.issues.removeLabel({ | ||||
|                 owner, | ||||
|                 repo, | ||||
|                 issue_number: number, | ||||
|                 name: labelToRemove | ||||
|               }); | ||||
|             } catch (error) { | ||||
|               core.warning(`Failed to remove 'pr:e2e:couchdb' label: ${error.message}`); | ||||
|             } | ||||
|   | ||||
							
								
								
									
										19
									
								
								.github/workflows/e2e-pr.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										19
									
								
								.github/workflows/e2e-pr.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,4 +1,4 @@ | ||||
| name: "e2e-pr" | ||||
| name: 'e2e-pr' | ||||
| on: | ||||
|   workflow_dispatch: | ||||
|   pull_request: | ||||
| @@ -66,3 +66,20 @@ jobs: | ||||
|               repo: "openmct", | ||||
|               body: 'Failure ❌ ! Build artifacts are here: https://github.com/nasa/openmct/actions/runs/' + context.runId | ||||
|             }) | ||||
|       - name: Remove pr:e2e label (if present) | ||||
|         if: ${{ contains(github.event.pull_request.labels.*.name, 'pr:e2e') }} | ||||
|         uses: actions/github-script@v6 | ||||
|         with: | ||||
|           script: | | ||||
|             const { owner, repo, number } = context.issue; | ||||
|             const labelToRemove = 'pr:e2e'; | ||||
|             try { | ||||
|               await github.rest.issues.removeLabel({ | ||||
|                 owner, | ||||
|                 repo, | ||||
|                 issue_number: number, | ||||
|                 name: labelToRemove | ||||
|               }); | ||||
|             } catch (error) { | ||||
|               core.warning(`Failed to remove 'pr:e2e' label: ${error.message}`); | ||||
|             } | ||||
|   | ||||
							
								
								
									
										4
									
								
								.github/workflows/pr-platform.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/pr-platform.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,8 +1,8 @@ | ||||
| name: "pr-platform" | ||||
| name: 'pr-platform' | ||||
| on: | ||||
|   workflow_dispatch: | ||||
|   pull_request: | ||||
|     types: [ labeled ] | ||||
|     types: [labeled] | ||||
|  | ||||
| jobs: | ||||
|   e2e-full: | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/workflows/prcop.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/prcop.yml
									
									
									
									
										vendored
									
									
								
							| @@ -22,5 +22,5 @@ jobs: | ||||
|       - name: Linting Pull Request | ||||
|         uses: makaroni4/prcop@v1.0.35 | ||||
|         with: | ||||
|           config-file: ".github/workflows/prcop-config.json" | ||||
|           config-file: '.github/workflows/prcop-config.json' | ||||
|           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||||
|   | ||||
							
								
								
									
										27
									
								
								.prettierignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								.prettierignore
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| # Docs | ||||
| *.md | ||||
|  | ||||
| # Build output | ||||
| target | ||||
| dist | ||||
|  | ||||
| # Mac OS X Finder | ||||
| .DS_Store | ||||
|  | ||||
| # Node dependencies | ||||
| node_modules | ||||
|  | ||||
| # npm-debug log | ||||
| npm-debug.log | ||||
|  | ||||
| # karma reports | ||||
| report.*.json | ||||
|  | ||||
| # e2e test artifacts | ||||
| test-results | ||||
| html-test-results | ||||
|  | ||||
| # codecov artifacts | ||||
| .nyc_output | ||||
| coverage | ||||
| codecov | ||||
							
								
								
									
										5
									
								
								.prettierrc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								.prettierrc
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| { | ||||
|   "trailingComma": "none", | ||||
|   "singleQuote": true, | ||||
|   "printWidth": 100 | ||||
| } | ||||
| @@ -8,169 +8,155 @@ This is the OpenMCT common webpack file. It is imported by the other three webpa | ||||
| There are separate npm scripts to use these configurations, though simply running `npm install` | ||||
| will use the default production configuration. | ||||
| */ | ||||
| const path = require("path"); | ||||
| const packageDefinition = require("../package.json"); | ||||
| const CopyWebpackPlugin = require("copy-webpack-plugin"); | ||||
| const webpack = require("webpack"); | ||||
| const MiniCssExtractPlugin = require("mini-css-extract-plugin"); | ||||
| const path = require('path'); | ||||
| const packageDefinition = require('../package.json'); | ||||
| const CopyWebpackPlugin = require('copy-webpack-plugin'); | ||||
| const webpack = require('webpack'); | ||||
| const MiniCssExtractPlugin = require('mini-css-extract-plugin'); | ||||
|  | ||||
| const { VueLoaderPlugin } = require("vue-loader"); | ||||
| let gitRevision = "error-retrieving-revision"; | ||||
| let gitBranch = "error-retrieving-branch"; | ||||
| const { VueLoaderPlugin } = require('vue-loader'); | ||||
| let gitRevision = 'error-retrieving-revision'; | ||||
| let gitBranch = 'error-retrieving-branch'; | ||||
|  | ||||
| try { | ||||
|     gitRevision = require("child_process") | ||||
|         .execSync("git rev-parse HEAD") | ||||
|         .toString() | ||||
|         .trim(); | ||||
|     gitBranch = require("child_process") | ||||
|         .execSync("git rev-parse --abbrev-ref HEAD") | ||||
|         .toString() | ||||
|         .trim(); | ||||
|   gitRevision = require('child_process').execSync('git rev-parse HEAD').toString().trim(); | ||||
|   gitBranch = require('child_process') | ||||
|     .execSync('git rev-parse --abbrev-ref HEAD') | ||||
|     .toString() | ||||
|     .trim(); | ||||
| } catch (err) { | ||||
|     console.warn(err); | ||||
|   console.warn(err); | ||||
| } | ||||
|  | ||||
| const projectRootDir = path.resolve(__dirname, ".."); | ||||
| const projectRootDir = path.resolve(__dirname, '..'); | ||||
|  | ||||
| /** @type {import('webpack').Configuration} */ | ||||
| const config = { | ||||
|     context: projectRootDir, | ||||
|     entry: { | ||||
|         openmct: "./openmct.js", | ||||
|         generatorWorker: "./example/generator/generatorWorker.js", | ||||
|         couchDBChangesFeed: | ||||
|             "./src/plugins/persistence/couch/CouchChangesFeed.js", | ||||
|         inMemorySearchWorker: "./src/api/objects/InMemorySearchWorker.js", | ||||
|         espressoTheme: "./src/plugins/themes/espresso-theme.scss", | ||||
|         snowTheme: "./src/plugins/themes/snow-theme.scss" | ||||
|     }, | ||||
|     output: { | ||||
|         globalObject: "this", | ||||
|         filename: "[name].js", | ||||
|         path: path.resolve(projectRootDir, "dist"), | ||||
|         library: "openmct", | ||||
|         libraryTarget: "umd", | ||||
|         publicPath: "", | ||||
|         hashFunction: "xxhash64", | ||||
|         clean: true | ||||
|     }, | ||||
|     resolve: { | ||||
|         alias: { | ||||
|             "@": path.join(projectRootDir, "src"), | ||||
|             legacyRegistry: path.join(projectRootDir, "src/legacyRegistry"), | ||||
|             saveAs: "file-saver/src/FileSaver.js", | ||||
|             csv: "comma-separated-values", | ||||
|             EventEmitter: "eventemitter3", | ||||
|             bourbon: "bourbon.scss", | ||||
|             "plotly-basic": "plotly.js-basic-dist", | ||||
|             "plotly-gl2d": "plotly.js-gl2d-dist", | ||||
|             "d3-scale": path.join( | ||||
|                 projectRootDir, | ||||
|                 "node_modules/d3-scale/dist/d3-scale.min.js" | ||||
|             ), | ||||
|             printj: path.join( | ||||
|                 projectRootDir, | ||||
|                 "node_modules/printj/dist/printj.min.js" | ||||
|             ), | ||||
|             styles: path.join(projectRootDir, "src/styles"), | ||||
|             MCT: path.join(projectRootDir, "src/MCT"), | ||||
|             testUtils: path.join(projectRootDir, "src/utils/testUtils.js"), | ||||
|             objectUtils: path.join( | ||||
|                 projectRootDir, | ||||
|                 "src/api/objects/object-utils.js" | ||||
|             ), | ||||
|             "kdbush": path.join(projectRootDir, "node_modules/kdbush/kdbush.min.js"), | ||||
|             utils: path.join(projectRootDir, "src/utils") | ||||
|         } | ||||
|     }, | ||||
|     plugins: [ | ||||
|         new webpack.DefinePlugin({ | ||||
|             __OPENMCT_VERSION__: `'${packageDefinition.version}'`, | ||||
|             __OPENMCT_BUILD_DATE__: `'${new Date()}'`, | ||||
|             __OPENMCT_REVISION__: `'${gitRevision}'`, | ||||
|             __OPENMCT_BUILD_BRANCH__: `'${gitBranch}'` | ||||
|         }), | ||||
|         new VueLoaderPlugin(), | ||||
|         new CopyWebpackPlugin({ | ||||
|             patterns: [ | ||||
|                 { | ||||
|                     from: "src/images/favicons", | ||||
|                     to: "favicons" | ||||
|                 }, | ||||
|                 { | ||||
|                     from: "./index.html", | ||||
|                     transform: function (content) { | ||||
|                         return content.toString().replace(/dist\//g, ""); | ||||
|                     } | ||||
|                 }, | ||||
|                 { | ||||
|                     from: "src/plugins/imagery/layers", | ||||
|                     to: "imagery" | ||||
|                 } | ||||
|             ] | ||||
|         }), | ||||
|         new MiniCssExtractPlugin({ | ||||
|             filename: "[name].css", | ||||
|             chunkFilename: "[name].css" | ||||
|         }) | ||||
|     ], | ||||
|     module: { | ||||
|         rules: [ | ||||
|             { | ||||
|                 test: /\.(sc|sa|c)ss$/, | ||||
|                 use: [ | ||||
|                     MiniCssExtractPlugin.loader, | ||||
|                     { | ||||
|                         loader: "css-loader" | ||||
|                     }, | ||||
|                     { | ||||
|                         loader: "resolve-url-loader" | ||||
|                     }, | ||||
|                     { | ||||
|                         loader: "sass-loader", | ||||
|                         options: { sourceMap: true } | ||||
|                     } | ||||
|                 ] | ||||
|             }, | ||||
|             { | ||||
|                 test: /\.vue$/, | ||||
|                 use: "vue-loader" | ||||
|             }, | ||||
|             { | ||||
|                 test: /\.html$/, | ||||
|                 type: "asset/source" | ||||
|             }, | ||||
|             { | ||||
|                 test: /\.(jpg|jpeg|png|svg)$/, | ||||
|                 type: "asset/resource", | ||||
|                 generator: { | ||||
|                     filename: "images/[name][ext]" | ||||
|                 } | ||||
|             }, | ||||
|             { | ||||
|                 test: /\.ico$/, | ||||
|                 type: "asset/resource", | ||||
|                 generator: { | ||||
|                     filename: "icons/[name][ext]" | ||||
|                 } | ||||
|             }, | ||||
|             { | ||||
|                 test: /\.(woff|woff2?|eot|ttf)$/, | ||||
|                 type: "asset/resource", | ||||
|                 generator: { | ||||
|                     filename: "fonts/[name][ext]" | ||||
|                 } | ||||
|             } | ||||
|         ] | ||||
|     }, | ||||
|     stats: "errors-warnings", | ||||
|     performance: { | ||||
|         // We should eventually consider chunking to decrease | ||||
|         // these values | ||||
|         maxEntrypointSize: 27000000, | ||||
|         maxAssetSize: 27000000 | ||||
|   context: projectRootDir, | ||||
|   entry: { | ||||
|     openmct: './openmct.js', | ||||
|     generatorWorker: './example/generator/generatorWorker.js', | ||||
|     couchDBChangesFeed: './src/plugins/persistence/couch/CouchChangesFeed.js', | ||||
|     inMemorySearchWorker: './src/api/objects/InMemorySearchWorker.js', | ||||
|     espressoTheme: './src/plugins/themes/espresso-theme.scss', | ||||
|     snowTheme: './src/plugins/themes/snow-theme.scss' | ||||
|   }, | ||||
|   output: { | ||||
|     globalObject: 'this', | ||||
|     filename: '[name].js', | ||||
|     path: path.resolve(projectRootDir, 'dist'), | ||||
|     library: 'openmct', | ||||
|     libraryTarget: 'umd', | ||||
|     publicPath: '', | ||||
|     hashFunction: 'xxhash64', | ||||
|     clean: true | ||||
|   }, | ||||
|   resolve: { | ||||
|     alias: { | ||||
|       '@': path.join(projectRootDir, 'src'), | ||||
|       legacyRegistry: path.join(projectRootDir, 'src/legacyRegistry'), | ||||
|       saveAs: 'file-saver/src/FileSaver.js', | ||||
|       csv: 'comma-separated-values', | ||||
|       EventEmitter: 'eventemitter3', | ||||
|       bourbon: 'bourbon.scss', | ||||
|       'plotly-basic': 'plotly.js-basic-dist', | ||||
|       'plotly-gl2d': 'plotly.js-gl2d-dist', | ||||
|       'd3-scale': path.join(projectRootDir, 'node_modules/d3-scale/dist/d3-scale.min.js'), | ||||
|       printj: path.join(projectRootDir, 'node_modules/printj/dist/printj.min.js'), | ||||
|       styles: path.join(projectRootDir, 'src/styles'), | ||||
|       MCT: path.join(projectRootDir, 'src/MCT'), | ||||
|       testUtils: path.join(projectRootDir, 'src/utils/testUtils.js'), | ||||
|       objectUtils: path.join(projectRootDir, 'src/api/objects/object-utils.js'), | ||||
|       utils: path.join(projectRootDir, 'src/utils') | ||||
|     } | ||||
|   }, | ||||
|   plugins: [ | ||||
|     new webpack.DefinePlugin({ | ||||
|       __OPENMCT_VERSION__: `'${packageDefinition.version}'`, | ||||
|       __OPENMCT_BUILD_DATE__: `'${new Date()}'`, | ||||
|       __OPENMCT_REVISION__: `'${gitRevision}'`, | ||||
|       __OPENMCT_BUILD_BRANCH__: `'${gitBranch}'` | ||||
|     }), | ||||
|     new VueLoaderPlugin(), | ||||
|     new CopyWebpackPlugin({ | ||||
|       patterns: [ | ||||
|         { | ||||
|           from: 'src/images/favicons', | ||||
|           to: 'favicons' | ||||
|         }, | ||||
|         { | ||||
|           from: './index.html', | ||||
|           transform: function (content) { | ||||
|             return content.toString().replace(/dist\//g, ''); | ||||
|           } | ||||
|         }, | ||||
|         { | ||||
|           from: 'src/plugins/imagery/layers', | ||||
|           to: 'imagery' | ||||
|         } | ||||
|       ] | ||||
|     }), | ||||
|     new MiniCssExtractPlugin({ | ||||
|       filename: '[name].css', | ||||
|       chunkFilename: '[name].css' | ||||
|     }) | ||||
|   ], | ||||
|   module: { | ||||
|     rules: [ | ||||
|       { | ||||
|         test: /\.(sc|sa|c)ss$/, | ||||
|         use: [ | ||||
|           MiniCssExtractPlugin.loader, | ||||
|           { | ||||
|             loader: 'css-loader' | ||||
|           }, | ||||
|           { | ||||
|             loader: 'resolve-url-loader' | ||||
|           }, | ||||
|           { | ||||
|             loader: 'sass-loader', | ||||
|             options: { sourceMap: true } | ||||
|           } | ||||
|         ] | ||||
|       }, | ||||
|       { | ||||
|         test: /\.vue$/, | ||||
|         use: 'vue-loader' | ||||
|       }, | ||||
|       { | ||||
|         test: /\.html$/, | ||||
|         type: 'asset/source' | ||||
|       }, | ||||
|       { | ||||
|         test: /\.(jpg|jpeg|png|svg)$/, | ||||
|         type: 'asset/resource', | ||||
|         generator: { | ||||
|           filename: 'images/[name][ext]' | ||||
|         } | ||||
|       }, | ||||
|       { | ||||
|         test: /\.ico$/, | ||||
|         type: 'asset/resource', | ||||
|         generator: { | ||||
|           filename: 'icons/[name][ext]' | ||||
|         } | ||||
|       }, | ||||
|       { | ||||
|         test: /\.(woff|woff2?|eot|ttf)$/, | ||||
|         type: 'asset/resource', | ||||
|         generator: { | ||||
|           filename: 'fonts/[name][ext]' | ||||
|         } | ||||
|       } | ||||
|     ] | ||||
|   }, | ||||
|   stats: 'errors-warnings', | ||||
|   performance: { | ||||
|     // We should eventually consider chunking to decrease | ||||
|     // these values | ||||
|     maxEntrypointSize: 27000000, | ||||
|     maxAssetSize: 27000000 | ||||
|   } | ||||
| }; | ||||
|  | ||||
| module.exports = config; | ||||
|   | ||||
| @@ -6,32 +6,32 @@ OpenMCT Continuous Integration servers use this configuration to add code covera | ||||
| information to pull requests. | ||||
| */ | ||||
|  | ||||
| const config = require("./webpack.dev"); | ||||
| const config = require('./webpack.dev'); | ||||
| // eslint-disable-next-line no-undef | ||||
| const CI = process.env.CI === "true"; | ||||
| const CI = process.env.CI === 'true'; | ||||
|  | ||||
| config.devtool = CI ? false : undefined; | ||||
|  | ||||
| config.devServer.hot = false; | ||||
|  | ||||
| config.module.rules.push({ | ||||
|     test: /\.js$/, | ||||
|     exclude: /(Spec\.js$)|(node_modules)/, | ||||
|     use: { | ||||
|         loader: "babel-loader", | ||||
|         options: { | ||||
|             retainLines: true, | ||||
|             // eslint-disable-next-line no-undef | ||||
|             plugins: [ | ||||
|                 [ | ||||
|                     "babel-plugin-istanbul", | ||||
|                     { | ||||
|                         extension: [".js", ".vue"] | ||||
|                     } | ||||
|                 ] | ||||
|             ] | ||||
|         } | ||||
|   test: /\.js$/, | ||||
|   exclude: /(Spec\.js$)|(node_modules)/, | ||||
|   use: { | ||||
|     loader: 'babel-loader', | ||||
|     options: { | ||||
|       retainLines: true, | ||||
|       // eslint-disable-next-line no-undef | ||||
|       plugins: [ | ||||
|         [ | ||||
|           'babel-plugin-istanbul', | ||||
|           { | ||||
|             extension: ['.js', '.vue'] | ||||
|           } | ||||
|         ] | ||||
|       ] | ||||
|     } | ||||
|   } | ||||
| }); | ||||
|  | ||||
| module.exports = config; | ||||
|   | ||||
| @@ -5,59 +5,59 @@ This configuration should be used for development purposes. It contains full sou | ||||
| devServer (which be invoked using by `npm start`), and a non-minified Vue.js distribution. | ||||
| If OpenMCT is to be used for a production server, use webpack.prod.js instead. | ||||
| */ | ||||
| const path = require("path"); | ||||
| const webpack = require("webpack"); | ||||
| const { merge } = require("webpack-merge"); | ||||
| const path = require('path'); | ||||
| const webpack = require('webpack'); | ||||
| const { merge } = require('webpack-merge'); | ||||
|  | ||||
| const common = require("./webpack.common"); | ||||
| const projectRootDir = path.resolve(__dirname, ".."); | ||||
| const common = require('./webpack.common'); | ||||
| const projectRootDir = path.resolve(__dirname, '..'); | ||||
|  | ||||
| module.exports = merge(common, { | ||||
|     mode: "development", | ||||
|     watchOptions: { | ||||
|         // Since we use require.context, webpack is watching the entire directory. | ||||
|         // We need to exclude any files we don't want webpack to watch. | ||||
|         // See: https://webpack.js.org/configuration/watch/#watchoptions-exclude | ||||
|         ignored: [ | ||||
|             "**/{node_modules,dist,docs,e2e}", // All files in node_modules, dist, docs, e2e, | ||||
|             "**/{*.yml,Procfile,webpack*.js,babel*.js,package*.json,tsconfig.json}", // Config files | ||||
|             "**/*.{sh,md,png,ttf,woff,svg}", // Non source files | ||||
|             "**/.*" // dotfiles and dotfolders | ||||
|         ] | ||||
|     }, | ||||
|     resolve: { | ||||
|         alias: { | ||||
|             vue: path.join(projectRootDir, "node_modules/vue/dist/vue.js") | ||||
|         } | ||||
|     }, | ||||
|     plugins: [ | ||||
|         new webpack.DefinePlugin({ | ||||
|             __OPENMCT_ROOT_RELATIVE__: '"dist/"' | ||||
|         }) | ||||
|     ], | ||||
|     devtool: "eval-source-map", | ||||
|     devServer: { | ||||
|         devMiddleware: { | ||||
|             writeToDisk: (filePathString) => { | ||||
|                 const filePath = path.parse(filePathString); | ||||
|                 const shouldWrite = !filePath.base.includes("hot-update"); | ||||
|  | ||||
|                 return shouldWrite; | ||||
|             } | ||||
|         }, | ||||
|         watchFiles: ["**/*.css"], | ||||
|         static: { | ||||
|             directory: path.join(__dirname, "..", "/dist"), | ||||
|             publicPath: "/dist", | ||||
|             watch: false | ||||
|         }, | ||||
|         client: { | ||||
|             progress: true, | ||||
|             overlay: { | ||||
|                 // Disable overlay for runtime errors. | ||||
|                 // See: https://github.com/webpack/webpack-dev-server/issues/4771 | ||||
|                 runtimeErrors: false | ||||
|             } | ||||
|         } | ||||
|   mode: 'development', | ||||
|   watchOptions: { | ||||
|     // Since we use require.context, webpack is watching the entire directory. | ||||
|     // We need to exclude any files we don't want webpack to watch. | ||||
|     // See: https://webpack.js.org/configuration/watch/#watchoptions-exclude | ||||
|     ignored: [ | ||||
|       '**/{node_modules,dist,docs,e2e}', // All files in node_modules, dist, docs, e2e, | ||||
|       '**/{*.yml,Procfile,webpack*.js,babel*.js,package*.json,tsconfig.json}', // Config files | ||||
|       '**/*.{sh,md,png,ttf,woff,svg}', // Non source files | ||||
|       '**/.*' // dotfiles and dotfolders | ||||
|     ] | ||||
|   }, | ||||
|   resolve: { | ||||
|     alias: { | ||||
|       vue: path.join(projectRootDir, 'node_modules/vue/dist/vue.js') | ||||
|     } | ||||
|   }, | ||||
|   plugins: [ | ||||
|     new webpack.DefinePlugin({ | ||||
|       __OPENMCT_ROOT_RELATIVE__: '"dist/"' | ||||
|     }) | ||||
|   ], | ||||
|   devtool: 'eval-source-map', | ||||
|   devServer: { | ||||
|     devMiddleware: { | ||||
|       writeToDisk: (filePathString) => { | ||||
|         const filePath = path.parse(filePathString); | ||||
|         const shouldWrite = !filePath.base.includes('hot-update'); | ||||
|  | ||||
|         return shouldWrite; | ||||
|       } | ||||
|     }, | ||||
|     watchFiles: ['**/*.css'], | ||||
|     static: { | ||||
|       directory: path.join(__dirname, '..', '/dist'), | ||||
|       publicPath: '/dist', | ||||
|       watch: false | ||||
|     }, | ||||
|     client: { | ||||
|       progress: true, | ||||
|       overlay: { | ||||
|         // Disable overlay for runtime errors. | ||||
|         // See: https://github.com/webpack/webpack-dev-server/issues/4771 | ||||
|         runtimeErrors: false | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| }); | ||||
|   | ||||
| @@ -4,24 +4,24 @@ | ||||
| This configuration should be used for production installs. | ||||
| It is the default webpack configuration. | ||||
| */ | ||||
| const path = require("path"); | ||||
| const webpack = require("webpack"); | ||||
| const { merge } = require("webpack-merge"); | ||||
| const path = require('path'); | ||||
| const webpack = require('webpack'); | ||||
| const { merge } = require('webpack-merge'); | ||||
|  | ||||
| const common = require("./webpack.common"); | ||||
| const projectRootDir = path.resolve(__dirname, ".."); | ||||
| const common = require('./webpack.common'); | ||||
| const projectRootDir = path.resolve(__dirname, '..'); | ||||
|  | ||||
| module.exports = merge(common, { | ||||
|     mode: "production", | ||||
|     resolve: { | ||||
|         alias: { | ||||
|             vue: path.join(projectRootDir, "node_modules/vue/dist/vue.min.js") | ||||
|         } | ||||
|     }, | ||||
|     plugins: [ | ||||
|         new webpack.DefinePlugin({ | ||||
|             __OPENMCT_ROOT_RELATIVE__: '""' | ||||
|         }) | ||||
|     ], | ||||
|     devtool: "source-map" | ||||
|   mode: 'production', | ||||
|   resolve: { | ||||
|     alias: { | ||||
|       vue: path.join(projectRootDir, 'node_modules/vue/dist/vue.min.js') | ||||
|     } | ||||
|   }, | ||||
|   plugins: [ | ||||
|     new webpack.DefinePlugin({ | ||||
|       __OPENMCT_ROOT_RELATIVE__: '""' | ||||
|     }) | ||||
|   ], | ||||
|   devtool: 'eval-source-map' | ||||
| }); | ||||
|   | ||||
| @@ -18,13 +18,13 @@ The short version: | ||||
|    for review.) | ||||
| 4. Respond to any discussion. When the reviewer decides it's ready, they | ||||
|    will merge back `master` and fill out their own check list. | ||||
| 5. If you are a first-time contributor, please see [this discussion](https://github.com/nasa/openmct/discussions/3821) for further information.    | ||||
| 5. If you are a first-time contributor, please see [this discussion](https://github.com/nasa/openmct/discussions/3821) for further information. | ||||
|  | ||||
| ## Contribution Process | ||||
|  | ||||
| Open MCT uses git for software version control, and for branching and | ||||
| merging. The central repository is at | ||||
| https://github.com/nasa/openmct.git. | ||||
| <https://github.com/nasa/openmct.git>. | ||||
|  | ||||
| ### Roles | ||||
|  | ||||
| @@ -116,6 +116,7 @@ the pull request containing the reviewer checklist (from below) and complete | ||||
| the merge back to the master branch. | ||||
|  | ||||
| Additionally: | ||||
|  | ||||
| * Every pull request must link to the issue that it addresses. Eg. “Addresses #1234” or “Closes #1234”. This is the responsibility of the pull request’s __author__. If no issue exists, [create one](https://github.com/nasa/openmct/issues/new/choose). | ||||
| * Every __author__ must include testing instructions. These instructions should identify the areas of code affected, and some minimal test steps. If addressing a bug, reproduction steps should be included, if they were not included in the original issue. If reproduction steps were included on the original issue, and are sufficient, refer to them. | ||||
| * A pull request that closes an issue should say so in the description. Including the text “Closes #1234” will cause the linked issue to be automatically closed when the pull request is merged. This is the responsibility of the pull request’s __author__. | ||||
| @@ -132,25 +133,26 @@ changes. | ||||
|  | ||||
| ### Code Standards | ||||
|  | ||||
| JavaScript sources in Open MCT must satisfy the ESLint rules defined in  | ||||
| this repository. This is verified by the command line build. | ||||
| JavaScript sources in Open MCT must satisfy the [ESLint](https://eslint.org/) rules defined in | ||||
| this repository. [Prettier](https://prettier.io/) is used in conjunction with ESLint to enforce code style | ||||
| via automated formatting. These are verified by the command line build. | ||||
|  | ||||
| #### Code Guidelines | ||||
|  | ||||
| The following guidelines are provided for anyone contributing source code to the Open MCT project: | ||||
|  | ||||
| 1. Write clean code. Here’s a good summary - https://github.com/ryanmcdermott/clean-code-javascript. | ||||
| 1. Write clean code. Here’s a good summary - <https://github.com/ryanmcdermott/clean-code-javascript>. | ||||
| 1. Include JSDoc for any exposed API (e.g. public methods, classes). | ||||
| 1. Include non-JSDoc comments as-needed for explaining private variables, | ||||
|    methods, or algorithms when they are non-obvious. Otherwise code  | ||||
|    methods, or algorithms when they are non-obvious. Otherwise code | ||||
|    should be self-documenting. | ||||
| 1. Classes and Vue components should use camel case, first letter capitalized | ||||
|    (e.g. SomeClassName). | ||||
| 1. Methods, variables, fields, events, and function names should use camelCase, | ||||
|    first letter lower-case (e.g. someVariableName). | ||||
| 1. Source files that export functions should use camelCase, first letter lower-case (eg. testTools.js) | ||||
| 1. Constants (variables or fields which are meant to be declared and  | ||||
|    initialized statically, and never changed) should use only capital  | ||||
| 1. Constants (variables or fields which are meant to be declared and | ||||
|    initialized statically, and never changed) should use only capital | ||||
|    letters, with underscores between words (e.g. SOME_CONSTANT). They should always be declared as `const`s | ||||
| 1. File names should be the name of the exported class, plus a .js extension | ||||
|    (e.g. SomeClassName.js). | ||||
| @@ -159,21 +161,25 @@ The following guidelines are provided for anyone contributing source code to the | ||||
|    (e.g. as arguments to a forEach call). Anonymous functions should always be arrow functions. | ||||
| 1. Named functions are preferred over functions assigned to variables. | ||||
|    eg. | ||||
|  | ||||
|    ```JavaScript | ||||
|    function renameObject(object, newName) { | ||||
|        Object.name = newName; | ||||
|    } | ||||
|    ``` | ||||
|  | ||||
|    is preferable to | ||||
|  | ||||
|    ```JavaScript | ||||
|    const rename = (object, newName) => { | ||||
|        Object.name = newName; | ||||
|    } | ||||
|    ``` | ||||
|  | ||||
| 1. Avoid deep nesting (especially of functions), except where necessary | ||||
|    (e.g. due to closure scope). | ||||
| 1. End with a single new-line character. | ||||
| 1. Always use ES6 `Class`es and inheritance rather than the pre-ES6 prototypal  | ||||
| 1. Always use ES6 `Class`es and inheritance rather than the pre-ES6 prototypal | ||||
|    pattern. | ||||
| 1. Within a given function's scope, do not mix declarations and imperative | ||||
|    code, and  present these in the following order: | ||||
| @@ -182,19 +188,24 @@ The following guidelines are provided for anyone contributing source code to the | ||||
|    * Finally, the returned value. A single return statement at the end of the function should be used, except where an early return would improve code clarity. | ||||
| 1. Avoid the use of "magic" values. | ||||
|    eg. | ||||
|  | ||||
|    ```JavaScript | ||||
|    const UNAUTHORIZED = 401; | ||||
|    if (responseCode === UNAUTHORIZED) | ||||
|    ``` | ||||
|  | ||||
|    is preferable to | ||||
|  | ||||
|    ```JavaScript | ||||
|    if (responseCode === 401) | ||||
|    ``` | ||||
|  | ||||
| 1. Use the ternary operator only for simple cases such as variable assignment. Nested ternaries should be avoided in all cases. | ||||
| 1. Unit Test specs should reside alongside the source code they test, not in a separate directory. | ||||
| 1. Organize code by feature, not by type. | ||||
|    eg. | ||||
|    ``` | ||||
|  | ||||
|    ```txt | ||||
|    - telemetryTable | ||||
|        - row | ||||
|            TableRow.js | ||||
| @@ -206,8 +217,10 @@ The following guidelines are provided for anyone contributing source code to the | ||||
|        plugin.js | ||||
|        pluginSpec.js | ||||
|    ``` | ||||
|  | ||||
|    is preferable to | ||||
|    ``` | ||||
|  | ||||
|    ```txt | ||||
|    - telemetryTable | ||||
|        - components | ||||
|            TableRow.vue | ||||
| @@ -219,6 +232,7 @@ The following guidelines are provided for anyone contributing source code to the | ||||
|        plugin.js | ||||
|        pluginSpec.js | ||||
|    ``` | ||||
|  | ||||
| Deviations from Open MCT code style guidelines require two-party agreement, | ||||
| typically from the author of the change and its reviewer. | ||||
|  | ||||
| @@ -257,7 +271,7 @@ these standards. | ||||
|  | ||||
| ## Issue Reporting | ||||
|  | ||||
| Issues are tracked at https://github.com/nasa/openmct/issues. | ||||
| Issues are tracked at <https://github.com/nasa/openmct/issues>. | ||||
|  | ||||
| Issue severity is categorized as follows (in ascending order): | ||||
|  | ||||
|   | ||||
| @@ -11,7 +11,7 @@ coverage: | ||||
|         informational: true | ||||
|   precision: 2 | ||||
|   round: down | ||||
|   range: "66...100" | ||||
|   range: '66...100' | ||||
|  | ||||
| flags: | ||||
|   unit: | ||||
| @@ -19,10 +19,10 @@ flags: | ||||
|   e2e-stable: | ||||
|     carryforward: false | ||||
|   e2e-full: | ||||
|     carryforward: true     | ||||
|     carryforward: true | ||||
|  | ||||
| comment: | ||||
|   layout: "diff,flags,files,footer" | ||||
|   layout: 'diff,flags,files,footer' | ||||
|   behavior: default | ||||
|   require_changes: false | ||||
|   show_carryforward_flags: true | ||||
|   | ||||
| @@ -1,15 +1,15 @@ | ||||
| /* eslint-disable no-undef */ | ||||
| module.exports = { | ||||
|     "extends": ["plugin:playwright/playwright-test"], | ||||
|     "rules": { | ||||
|         "playwright/max-nested-describe": ["error", { "max": 1 }] | ||||
|     }, | ||||
|     "overrides": [ | ||||
|         { | ||||
|             "files": ["tests/visual/*.spec.js"], | ||||
|             "rules": { | ||||
|                 "playwright/no-wait-for-timeout": "off" | ||||
|             } | ||||
|         } | ||||
|     ] | ||||
|   extends: ['plugin:playwright/playwright-test'], | ||||
|   rules: { | ||||
|     'playwright/max-nested-describe': ['error', { max: 1 }] | ||||
|   }, | ||||
|   overrides: [ | ||||
|     { | ||||
|       files: ['tests/visual/*.spec.js'], | ||||
|       rules: { | ||||
|         'playwright/no-wait-for-timeout': 'off' | ||||
|       } | ||||
|     } | ||||
|   ] | ||||
| }; | ||||
|   | ||||
| @@ -139,16 +139,18 @@ These tests are expected to become blocking and gating with assertions as we ext | ||||
|  | ||||
| Our file structure follows the type of type of testing being excercised at the e2e layer and files containing test suites which matcher application behavior or our `src` and `example` layout. This area is not well refined as we figure out what works best for closed source and downstream projects. This may change altogether if we move `e2e` to it's own npm package. | ||||
|  | ||||
| - `./helper` - contains helper functions or scripts which are leveraged directly within the testsuites. i.e. non-default plugin scripts injected into DOM | ||||
| - `./test-data` - contains test data which is leveraged or generated in the functional, performance, or visual test suites. i.e. localStorage data | ||||
| - `./tests/functional` - the bulk of the tests are contained within this folder to verify the functionality of open mct | ||||
| - `./tests/functional/example/` - tests which specifically verify the example plugins | ||||
| - `./tests/functional/plugins/` - tests which loosely test each plugin. This folder is the most likely to change. Note: some @snapshot tests are still contained within this structure | ||||
| - `./tests/framework/` - tests which verify that our testframework functionality and assumptions will continue to work based on further refactoring or playwright version changes | ||||
| - `./tests/performance/` - performance tests | ||||
| - `./tests/visual/` - Visual tests | ||||
| - `./appActions.js` - Contains common fixtures which can be leveraged by testcase authors to quickly move through the application when writing new tests. | ||||
| - `./baseFixture.js` - Contains base fixtures which only extend default `@playwright/test` functionality. The goal is to remove these fixtures as native Playwright APIs improve. | ||||
| |File Path|Description| | ||||
| |:-:|-| | ||||
| |`./helper`                    | Contains helper functions or scripts which are leveraged directly within the test suites (e.g.: non-default plugin scripts injected into the DOM)| | ||||
| |`./test-data`                 | Contains test data which is leveraged or generated in the functional, performance, or visual test suites (e.g.: localStorage data).| | ||||
| |`./tests/functional`          | The bulk of the tests are contained within this folder to verify the functionality of Open MCT.| | ||||
| |`./tests/functional/example/` | Tests which specifically verify the example plugins (e.g.: Sine Wave Generator).| | ||||
| |`./tests/functional/plugins/` | Tests which loosely test each plugin. This folder is the most likely to change. Note: some `@snapshot` tests are still contained within this structure.| | ||||
| |`./tests/framework/`          | Tests which verify that our testing framework's functionality and assumptions will continue to work based on further refactoring or Playwright version changes (e.g.: verifying custom fixtures and appActions).| | ||||
| |`./tests/performance/`        | Performance tests.| | ||||
| |`./tests/visual/`             | Visual tests.| | ||||
| |`./appActions.js`             | Contains common methods which can be leveraged by test case authors to quickly move through the application when writing new tests.| | ||||
| |`./baseFixture.js`            | Contains base fixtures which only extend default `@playwright/test` functionality. The expectation is that these fixtures will be removed as the native Playwright API improves| | ||||
|  | ||||
| Our functional tests end in `*.e2e.spec.js`, visual tests in `*.visual.spec.js` and performance tests in `*.perf.spec.js`. | ||||
|  | ||||
| @@ -158,10 +160,12 @@ Where possible, we try to run Open MCT without modification or configuration cha | ||||
|  | ||||
| Open MCT is leveraging the [config file](https://playwright.dev/docs/test-configuration) pattern to describe the capabilities of Open MCT e2e _where_ it's run | ||||
|  | ||||
| - `./playwright-ci.config.js` - Used when running in CI or to debug CI issues locally | ||||
| - `./playwright-local.config.js` - Used when running locally | ||||
| - `./playwright-performance.config.js` - Used when running performance tests in CI or locally | ||||
| - `./playwright-visual.config.js` - Used to run the visual tests in CI or locally | ||||
| |Config File|Description| | ||||
| |:-:|-| | ||||
| |`./playwright-ci.config.js` | Used when running in CI or to debug CI issues locally| | ||||
| |`./playwright-local.config.js` | Used when running locally| | ||||
| |`./playwright-performance.config.js` | Used when running performance tests in CI or locally| | ||||
| |`./playwright-visual.config.js` | Used to run the visual tests in CI or locally| | ||||
|  | ||||
| #### Test Tags | ||||
|  | ||||
| @@ -169,13 +173,15 @@ Test tags are a great way of organizing tests outside of a file structure. To le | ||||
|  | ||||
| Current list of test tags: | ||||
|  | ||||
| - `@ipad` - Test case or test suite is compatible with Playwright's iPad support and Open MCT's read-only mobile view (i.e. no Create button). | ||||
| - `@gds` - Denotes a GDS Test Case used in the VIPER Mission. | ||||
| - `@addInit` - Initializes the browser with an injected and artificial state. Useful for loading non-default plugins. Likely will not work outside of `npm start`. | ||||
| - `@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 the playwright container. | ||||
| - `@unstable` - A new test or test which is known to be flaky. | ||||
| - `@2p` - Indicates that multiple users are involved, or multiple tabs/pages are used. Useful for testing multi-user interactivity. | ||||
| |Test Tag|Description| | ||||
| |:-:|-| | ||||
| |`@ipad` | Test case or test suite is compatible with Playwright's iPad support and Open MCT's read-only mobile view (i.e. no create button).| | ||||
| |`@gds` | Denotes a GDS Test Case used in the VIPER Mission.| | ||||
| |`@addInit` | Initializes the browser with an injected and artificial state. Useful for loading non-default plugins. Likely will not work outside of `npm start`.| | ||||
| |`@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 the playwright container.| | ||||
| |`@unstable` | A new test or test which is known to be flaky.| | ||||
| |`@2p` | Indicates that multiple users are involved, or multiple tabs/pages are used. Useful for testing multi-user interactivity.| | ||||
|  | ||||
| ### Continuous Integration | ||||
|  | ||||
| @@ -232,7 +238,8 @@ At the same time, we don't want to waste CI resources on parallel runs, so we've | ||||
|  | ||||
| In order to maintain fast and reliable feedback, tests go through a promotion process. All new test cases or test suites must be labeled with the `@unstable` annotation. The Open MCT dev team runs these unstable tests in our private repos to ensure they work downstream and are reliable. | ||||
|  | ||||
| To run the stable tests, use the ```npm run test:e2e:stable``` command. To run the new and flaky tests, use the ```npm run test:e2e:unstable``` command. | ||||
| - To run the stable tests, use the `npm run test:e2e:stable` command. | ||||
| - To run the new and flaky tests, use the `npm run test:e2e:unstable` command. | ||||
|  | ||||
| A testcase and testsuite are to be unmarked as @unstable when: | ||||
|  | ||||
| @@ -293,13 +300,24 @@ Skipping based on browser version (Rarely used): <https://github.com/microsoft/p | ||||
| - How to make tests robust to function in other contexts (VISTA, VIPER, etc.) | ||||
|   - Leverage the use of `appActions.js` methods such as `createDomainObjectWithDefaults()` | ||||
| - How to make tests faster and more resilient | ||||
|   - When possible, navigate directly by URL | ||||
|   - Leverage `await page.goto('./', { waitUntil: 'networkidle' });` | ||||
|   - When possible, navigate directly by URL: | ||||
|  | ||||
|   ```javascript | ||||
|     // You can capture the CreatedObjectInfo returned from this appAction: | ||||
|     const clock = await createDomainObjectWithDefaults(page, { type: 'Clock' }); | ||||
|  | ||||
|     // ...and use its `url` property to navigate directly to it later in the test: | ||||
|     await page.goto(clock.url); | ||||
|   ``` | ||||
|  | ||||
|   - Leverage `await page.goto('./', { waitUntil: 'domcontentloaded' });` | ||||
|     - Initial navigation should _almost_ always use the `{ waitUntil: 'domcontentloaded' }` option. | ||||
|   - Avoid repeated setup to test to test a single assertion. Write longer tests with multiple soft assertions. | ||||
|  | ||||
| ### How to write a great test (WIP) | ||||
|  | ||||
| - Use our [App Actions](./appActions.js) for performing common actions whenever applicable. | ||||
|   - Use `waitForPlotsToRender()` before asserting against anything that is dependent upon plot series data being loaded and drawn. | ||||
| - If you create an object outside of using the `createDomainObjectWithDefaults` App Action, make sure to fill in the 'Notes' section of your object with `page.testNotes`: | ||||
|  | ||||
|   ```js | ||||
| @@ -346,7 +364,7 @@ We leverage the following official Playwright reporters: | ||||
| - Tracefile | ||||
| - Screenshots | ||||
|  | ||||
| When running the tests locally with the `npm run test:local` command, the html report will open automatically on failure. Inside this HTML report will be a complete summary of the finished tests. If the tests failed, you'll see embedded links to screenshot failure, execution logs, and the Tracefile. | ||||
| When running the tests locally with the `npm run test:e2e:local` command, the html report will open automatically on failure. Inside this HTML report will be a complete summary of the finished tests. If the tests failed, you'll see embedded links to screenshot failure, execution logs, and the Tracefile. | ||||
|  | ||||
| When looking at the reports run in CI, you'll leverage this same HTML Report which is hosted either in CircleCI or Github Actions as a build artifact. | ||||
|  | ||||
|   | ||||
| @@ -55,6 +55,7 @@ | ||||
|  | ||||
| const Buffer = require('buffer').Buffer; | ||||
| const genUuid = require('uuid').v4; | ||||
| const { expect } = require('@playwright/test'); | ||||
|  | ||||
| /** | ||||
|  * This common function creates a domain object with the default options. It is the preferred way of creating objects | ||||
| @@ -65,58 +66,58 @@ const genUuid = require('uuid').v4; | ||||
|  * @returns {Promise<CreatedObjectInfo>} An object containing information about the newly created domain object. | ||||
|  */ | ||||
| async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine' }) { | ||||
|     if (!name) { | ||||
|         name = `${type}:${genUuid()}`; | ||||
|     } | ||||
|   if (!name) { | ||||
|     name = `${type}:${genUuid()}`; | ||||
|   } | ||||
|  | ||||
|     const parentUrl = await getHashUrlToDomainObject(page, parent); | ||||
|   const parentUrl = await getHashUrlToDomainObject(page, parent); | ||||
|  | ||||
|     // Navigate to the parent object. This is necessary to create the object | ||||
|     // in the correct location, such as a folder, layout, or plot. | ||||
|     await page.goto(`${parentUrl}?hideTree=true`); | ||||
|   // Navigate to the parent object. This is necessary to create the object | ||||
|   // in the correct location, such as a folder, layout, or plot. | ||||
|   await page.goto(`${parentUrl}?hideTree=true`); | ||||
|  | ||||
|     //Click the Create button | ||||
|     await page.click('button:has-text("Create")'); | ||||
|   //Click the Create button | ||||
|   await page.click('button:has-text("Create")'); | ||||
|  | ||||
|     // Click the object specified by 'type' | ||||
|     await page.click(`li[role='menuitem']:text("${type}")`); | ||||
|   // Click the object specified by 'type' | ||||
|   await page.click(`li[role='menuitem']:text("${type}")`); | ||||
|  | ||||
|     // Modify the name input field of the domain object to accept 'name' | ||||
|     const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]'); | ||||
|     await nameInput.fill(""); | ||||
|     await nameInput.fill(name); | ||||
|   // Modify the name input field of the domain object to accept 'name' | ||||
|   const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]'); | ||||
|   await nameInput.fill(''); | ||||
|   await nameInput.fill(name); | ||||
|  | ||||
|     if (page.testNotes) { | ||||
|         // Fill the "Notes" section with information about the | ||||
|         // currently running test and its project. | ||||
|         const notesInput = page.locator('form[name="mctForm"] #notes-textarea'); | ||||
|         await notesInput.fill(page.testNotes); | ||||
|     } | ||||
|   if (page.testNotes) { | ||||
|     // Fill the "Notes" section with information about the | ||||
|     // currently running test and its project. | ||||
|     const notesInput = page.locator('form[name="mctForm"] #notes-textarea'); | ||||
|     await notesInput.fill(page.testNotes); | ||||
|   } | ||||
|  | ||||
|     // Click OK button and wait for Navigate event | ||||
|     await Promise.all([ | ||||
|         page.waitForLoadState(), | ||||
|         page.click('[aria-label="Save"]'), | ||||
|         // Wait for Save Banner to appear | ||||
|         page.waitForSelector('.c-message-banner__message') | ||||
|     ]); | ||||
|   // Click OK button and wait for Navigate event | ||||
|   await Promise.all([ | ||||
|     page.waitForLoadState(), | ||||
|     page.click('[aria-label="Save"]'), | ||||
|     // Wait for Save Banner to appear | ||||
|     page.waitForSelector('.c-message-banner__message') | ||||
|   ]); | ||||
|  | ||||
|     // Wait until the URL is updated | ||||
|     await page.waitForURL(`**/${parent}/*`); | ||||
|     const uuid = await getFocusedObjectUuid(page); | ||||
|     const objectUrl = await getHashUrlToDomainObject(page, uuid); | ||||
|   // Wait until the URL is updated | ||||
|   await page.waitForURL(`**/${parent}/*`); | ||||
|   const uuid = await getFocusedObjectUuid(page); | ||||
|   const objectUrl = await getHashUrlToDomainObject(page, uuid); | ||||
|  | ||||
|     if (await _isInEditMode(page, uuid)) { | ||||
|         // Save (exit edit mode) | ||||
|         await page.locator('button[title="Save"]').click(); | ||||
|         await page.locator('li[title="Save and Finish Editing"]').click(); | ||||
|     } | ||||
|   if (await _isInEditMode(page, uuid)) { | ||||
|     // Save (exit edit mode) | ||||
|     await page.locator('button[title="Save"]').click(); | ||||
|     await page.locator('li[title="Save and Finish Editing"]').click(); | ||||
|   } | ||||
|  | ||||
|     return { | ||||
|         name, | ||||
|         uuid, | ||||
|         url: objectUrl | ||||
|     }; | ||||
|   return { | ||||
|     name, | ||||
|     uuid, | ||||
|     url: objectUrl | ||||
|   }; | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -125,17 +126,17 @@ async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine | ||||
|  * @param {CreateNotificationOptions} createNotificationOptions | ||||
|  */ | ||||
| async function createNotification(page, createNotificationOptions) { | ||||
|     await page.evaluate((_createNotificationOptions) => { | ||||
|         const { message, severity, options } = _createNotificationOptions; | ||||
|         const notificationApi = window.openmct.notifications; | ||||
|         if (severity === 'info') { | ||||
|             notificationApi.info(message, options); | ||||
|         } else if (severity === 'alert') { | ||||
|             notificationApi.alert(message, options); | ||||
|         } else { | ||||
|             notificationApi.error(message, options); | ||||
|         } | ||||
|     }, createNotificationOptions); | ||||
|   await page.evaluate((_createNotificationOptions) => { | ||||
|     const { message, severity, options } = _createNotificationOptions; | ||||
|     const notificationApi = window.openmct.notifications; | ||||
|     if (severity === 'info') { | ||||
|       notificationApi.info(message, options); | ||||
|     } else if (severity === 'alert') { | ||||
|       notificationApi.alert(message, options); | ||||
|     } else { | ||||
|       notificationApi.error(message, options); | ||||
|     } | ||||
|   }, createNotificationOptions); | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -144,12 +145,12 @@ async function createNotification(page, createNotificationOptions) { | ||||
|  * @param {string} name | ||||
|  */ | ||||
| async function expandTreePaneItemByName(page, name) { | ||||
|     const treePane = page.getByRole('tree', { | ||||
|         name: 'Main Tree' | ||||
|     }); | ||||
|     const treeItem = treePane.locator(`role=treeitem[expanded=false][name=/${name}/]`); | ||||
|     const expandTriangle = treeItem.locator('.c-disclosure-triangle'); | ||||
|     await expandTriangle.click(); | ||||
|   const treePane = page.getByRole('tree', { | ||||
|     name: 'Main Tree' | ||||
|   }); | ||||
|   const treeItem = treePane.locator(`role=treeitem[expanded=false][name=/${name}/]`); | ||||
|   const expandTriangle = treeItem.locator('.c-disclosure-triangle'); | ||||
|   await expandTriangle.click(); | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -159,67 +160,67 @@ async function expandTreePaneItemByName(page, name) { | ||||
|  * @returns {Promise<CreatedObjectInfo>} An object containing information about the newly created domain object. | ||||
|  */ | ||||
| async function createPlanFromJSON(page, { name, json, parent = 'mine' }) { | ||||
|     if (!name) { | ||||
|         name = `Plan:${genUuid()}`; | ||||
|     } | ||||
|   if (!name) { | ||||
|     name = `Plan:${genUuid()}`; | ||||
|   } | ||||
|  | ||||
|     const parentUrl = await getHashUrlToDomainObject(page, parent); | ||||
|   const parentUrl = await getHashUrlToDomainObject(page, parent); | ||||
|  | ||||
|     // Navigate to the parent object. This is necessary to create the object | ||||
|     // in the correct location, such as a folder, layout, or plot. | ||||
|     await page.goto(`${parentUrl}?hideTree=true`); | ||||
|   // Navigate to the parent object. This is necessary to create the object | ||||
|   // in the correct location, such as a folder, layout, or plot. | ||||
|   await page.goto(`${parentUrl}?hideTree=true`); | ||||
|  | ||||
|     // Click the Create button | ||||
|     await page.click('button:has-text("Create")'); | ||||
|   // Click the Create button | ||||
|   await page.click('button:has-text("Create")'); | ||||
|  | ||||
|     // Click 'Plan' menu option | ||||
|     await page.click(`li:text("Plan")`); | ||||
|   // Click 'Plan' menu option | ||||
|   await page.click(`li:text("Plan")`); | ||||
|  | ||||
|     // Modify the name input field of the domain object to accept 'name' | ||||
|     const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]'); | ||||
|     await nameInput.fill(""); | ||||
|     await nameInput.fill(name); | ||||
|   // Modify the name input field of the domain object to accept 'name' | ||||
|   const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]'); | ||||
|   await nameInput.fill(''); | ||||
|   await nameInput.fill(name); | ||||
|  | ||||
|     // Upload buffer from memory | ||||
|     await page.locator('input#fileElem').setInputFiles({ | ||||
|         name: 'plan.txt', | ||||
|         mimeType: 'text/plain', | ||||
|         buffer: Buffer.from(JSON.stringify(json)) | ||||
|     }); | ||||
|   // Upload buffer from memory | ||||
|   await page.locator('input#fileElem').setInputFiles({ | ||||
|     name: 'plan.txt', | ||||
|     mimeType: 'text/plain', | ||||
|     buffer: Buffer.from(JSON.stringify(json)) | ||||
|   }); | ||||
|  | ||||
|     // Click OK button and wait for Navigate event | ||||
|     await Promise.all([ | ||||
|         page.waitForLoadState(), | ||||
|         page.click('[aria-label="Save"]'), | ||||
|         // Wait for Save Banner to appear | ||||
|         page.waitForSelector('.c-message-banner__message') | ||||
|     ]); | ||||
|   // Click OK button and wait for Navigate event | ||||
|   await Promise.all([ | ||||
|     page.waitForLoadState(), | ||||
|     page.click('[aria-label="Save"]'), | ||||
|     // Wait for Save Banner to appear | ||||
|     page.waitForSelector('.c-message-banner__message') | ||||
|   ]); | ||||
|  | ||||
|     // Wait until the URL is updated | ||||
|     await page.waitForURL(`**/${parent}/*`); | ||||
|     const uuid = await getFocusedObjectUuid(page); | ||||
|     const objectUrl = await getHashUrlToDomainObject(page, uuid); | ||||
|   // Wait until the URL is updated | ||||
|   await page.waitForURL(`**/${parent}/*`); | ||||
|   const uuid = await getFocusedObjectUuid(page); | ||||
|   const objectUrl = await getHashUrlToDomainObject(page, uuid); | ||||
|  | ||||
|     return { | ||||
|         uuid, | ||||
|         name, | ||||
|         url: objectUrl | ||||
|     }; | ||||
|   return { | ||||
|     uuid, | ||||
|     name, | ||||
|     url: objectUrl | ||||
|   }; | ||||
| } | ||||
|  | ||||
| /** | ||||
| * Open the given `domainObject`'s context menu from the object tree. | ||||
| * Expands the path to the object and scrolls to it if necessary. | ||||
| * | ||||
| * @param {import('@playwright/test').Page} page | ||||
| * @param {string} url the url to the object | ||||
| */ | ||||
|  * Open the given `domainObject`'s context menu from the object tree. | ||||
|  * Expands the path to the object and scrolls to it if necessary. | ||||
|  * | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  * @param {string} url the url to the object | ||||
|  */ | ||||
| async function openObjectTreeContextMenu(page, url) { | ||||
|     await page.goto(url); | ||||
|     await page.click('button[title="Show selected item in tree"]'); | ||||
|     await page.locator('.is-navigated-object').click({ | ||||
|         button: 'right' | ||||
|     }); | ||||
|   await page.goto(url); | ||||
|   await page.click('button[title="Show selected item in tree"]'); | ||||
|   await page.locator('.is-navigated-object').click({ | ||||
|     button: 'right' | ||||
|   }); | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -227,23 +228,25 @@ async function openObjectTreeContextMenu(page, url) { | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  * @param {"Main Tree" | "Create Modal Tree"} [treeName="Main Tree"] | ||||
|  */ | ||||
| async function expandEntireTree(page, treeName = "Main Tree") { | ||||
|     const treeLocator = page.getByRole('tree', { | ||||
|         name: treeName | ||||
|     }); | ||||
|     const collapsedTreeItems = treeLocator.getByRole('treeitem', { | ||||
|         expanded: false | ||||
|     }).locator('span.c-disclosure-triangle.is-enabled'); | ||||
| async function expandEntireTree(page, treeName = 'Main Tree') { | ||||
|   const treeLocator = page.getByRole('tree', { | ||||
|     name: treeName | ||||
|   }); | ||||
|   const collapsedTreeItems = treeLocator | ||||
|     .getByRole('treeitem', { | ||||
|       expanded: false | ||||
|     }) | ||||
|     .locator('span.c-disclosure-triangle.is-enabled'); | ||||
|  | ||||
|     while (await collapsedTreeItems.count() > 0) { | ||||
|         await collapsedTreeItems.nth(0).click(); | ||||
|   while ((await collapsedTreeItems.count()) > 0) { | ||||
|     await collapsedTreeItems.nth(0).click(); | ||||
|  | ||||
|         // FIXME: Replace hard wait with something event-driven. | ||||
|         // Without the wait, this fails periodically due to a race condition | ||||
|         // with Vue rendering (loop exits prematurely). | ||||
|         // eslint-disable-next-line playwright/no-wait-for-timeout | ||||
|         await page.waitForTimeout(200); | ||||
|     } | ||||
|     // FIXME: Replace hard wait with something event-driven. | ||||
|     // Without the wait, this fails periodically due to a race condition | ||||
|     // with Vue rendering (loop exits prematurely). | ||||
|     // eslint-disable-next-line playwright/no-wait-for-timeout | ||||
|     await page.waitForTimeout(200); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -253,12 +256,12 @@ async function expandEntireTree(page, treeName = "Main Tree") { | ||||
|  * @returns {Promise<string>} the uuid of the focused object | ||||
|  */ | ||||
| async function getFocusedObjectUuid(page) { | ||||
|     const UUIDv4Regexp = /[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/gi; | ||||
|     const focusedObjectUuid = await page.evaluate((regexp) => { | ||||
|         return window.location.href.split('?')[0].match(regexp).at(-1); | ||||
|     }, UUIDv4Regexp); | ||||
|   const UUIDv4Regexp = /[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/gi; | ||||
|   const focusedObjectUuid = await page.evaluate((regexp) => { | ||||
|     return window.location.href.split('?')[0].match(regexp).at(-1); | ||||
|   }, UUIDv4Regexp); | ||||
|  | ||||
|     return focusedObjectUuid; | ||||
|   return focusedObjectUuid; | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -272,22 +275,25 @@ async function getFocusedObjectUuid(page) { | ||||
|  * @returns {Promise<string>} the url of the object | ||||
|  */ | ||||
| async function getHashUrlToDomainObject(page, uuid) { | ||||
|     await page.waitForLoadState('load'); //Add some determinism | ||||
|     const hashUrl = await page.evaluate(async (objectUuid) => { | ||||
|         const path = await window.openmct.objects.getOriginalPath(objectUuid); | ||||
|         let url = './#/browse/' + [...path].reverse() | ||||
|             .map((object) => window.openmct.objects.makeKeyString(object.identifier)) | ||||
|             .join('/'); | ||||
|   await page.waitForLoadState('load'); //Add some determinism | ||||
|   const hashUrl = await page.evaluate(async (objectUuid) => { | ||||
|     const path = await window.openmct.objects.getOriginalPath(objectUuid); | ||||
|     let url = | ||||
|       './#/browse/' + | ||||
|       [...path] | ||||
|         .reverse() | ||||
|         .map((object) => window.openmct.objects.makeKeyString(object.identifier)) | ||||
|         .join('/'); | ||||
|  | ||||
|         // Drop the vestigial '/ROOT' if it exists | ||||
|         if (url.includes('/ROOT')) { | ||||
|             url = url.split('/ROOT').join(''); | ||||
|         } | ||||
|     // Drop the vestigial '/ROOT' if it exists | ||||
|     if (url.includes('/ROOT')) { | ||||
|       url = url.split('/ROOT').join(''); | ||||
|     } | ||||
|  | ||||
|         return url; | ||||
|     }, uuid); | ||||
|     return url; | ||||
|   }, uuid); | ||||
|  | ||||
|     return hashUrl; | ||||
|   return hashUrl; | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -297,8 +303,8 @@ async function getHashUrlToDomainObject(page, uuid) { | ||||
|  * @return {Promise<boolean>} true if the Open MCT is in Edit Mode | ||||
|  */ | ||||
| async function _isInEditMode(page, identifier) { | ||||
|     // eslint-disable-next-line no-return-await | ||||
|     return await page.evaluate(() => window.openmct.editor.isEditing()); | ||||
|   // eslint-disable-next-line no-return-await | ||||
|   return await page.evaluate(() => window.openmct.editor.isEditing()); | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -307,15 +313,15 @@ async function _isInEditMode(page, identifier) { | ||||
|  * @param {boolean} [isFixedTimespan=true] true for fixed timespan mode, false for realtime mode; default is true | ||||
|  */ | ||||
| async function setTimeConductorMode(page, isFixedTimespan = true) { | ||||
|     // Click 'mode' button | ||||
|     await page.locator('.c-mode-button').click(); | ||||
|   // Click 'mode' button | ||||
|   await page.locator('.c-mode-button').click(); | ||||
|  | ||||
|     // Switch time conductor mode | ||||
|     if (isFixedTimespan) { | ||||
|         await page.locator('data-testid=conductor-modeOption-fixed').click(); | ||||
|     } else { | ||||
|         await page.locator('data-testid=conductor-modeOption-realtime').click(); | ||||
|     } | ||||
|   // Switch time conductor mode | ||||
|   if (isFixedTimespan) { | ||||
|     await page.locator('data-testid=conductor-modeOption-fixed').click(); | ||||
|   } else { | ||||
|     await page.locator('data-testid=conductor-modeOption-realtime').click(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -323,7 +329,7 @@ async function setTimeConductorMode(page, isFixedTimespan = true) { | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function setFixedTimeMode(page) { | ||||
|     await setTimeConductorMode(page, true); | ||||
|   await setTimeConductorMode(page, true); | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -331,7 +337,7 @@ async function setFixedTimeMode(page) { | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function setRealTimeMode(page) { | ||||
|     await setTimeConductorMode(page, false); | ||||
|   await setTimeConductorMode(page, false); | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -347,23 +353,23 @@ async function setRealTimeMode(page) { | ||||
|  * @param {OffsetValues} offset | ||||
|  * @param {import('@playwright/test').Locator} offsetButton | ||||
|  */ | ||||
| async function setTimeConductorOffset(page, {hours, mins, secs}, offsetButton) { | ||||
|     await offsetButton.click(); | ||||
| async function setTimeConductorOffset(page, { hours, mins, secs }, offsetButton) { | ||||
|   await offsetButton.click(); | ||||
|  | ||||
|     if (hours) { | ||||
|         await page.fill('.pr-time-controls__hrs', hours); | ||||
|     } | ||||
|   if (hours) { | ||||
|     await page.fill('.pr-time-controls__hrs', hours); | ||||
|   } | ||||
|  | ||||
|     if (mins) { | ||||
|         await page.fill('.pr-time-controls__mins', mins); | ||||
|     } | ||||
|   if (mins) { | ||||
|     await page.fill('.pr-time-controls__mins', mins); | ||||
|   } | ||||
|  | ||||
|     if (secs) { | ||||
|         await page.fill('.pr-time-controls__secs', secs); | ||||
|     } | ||||
|   if (secs) { | ||||
|     await page.fill('.pr-time-controls__secs', secs); | ||||
|   } | ||||
|  | ||||
|     // Click the check button | ||||
|     await page.locator('.pr-time__buttons .icon-check').click(); | ||||
|   // Click the check button | ||||
|   await page.locator('.pr-time__buttons .icon-check').click(); | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -372,8 +378,8 @@ async function setTimeConductorOffset(page, {hours, mins, secs}, offsetButton) { | ||||
|  * @param {OffsetValues} offset | ||||
|  */ | ||||
| async function setStartOffset(page, offset) { | ||||
|     const startOffsetButton = page.locator('data-testid=conductor-start-offset-button'); | ||||
|     await setTimeConductorOffset(page, offset, startOffsetButton); | ||||
|   const startOffsetButton = page.locator('data-testid=conductor-start-offset-button'); | ||||
|   await setTimeConductorOffset(page, offset, startOffsetButton); | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -382,8 +388,8 @@ async function setStartOffset(page, offset) { | ||||
|  * @param {OffsetValues} offset | ||||
|  */ | ||||
| async function setEndOffset(page, offset) { | ||||
|     const endOffsetButton = page.locator('data-testid=conductor-end-offset-button'); | ||||
|     await setTimeConductorOffset(page, offset, endOffsetButton); | ||||
|   const endOffsetButton = page.locator('data-testid=conductor-end-offset-button'); | ||||
|   await setTimeConductorOffset(page, offset, endOffsetButton); | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -393,31 +399,117 @@ async function setEndOffset(page, offset) { | ||||
|  * @param {String} name the name of the tab | ||||
|  */ | ||||
| async function selectInspectorTab(page, name) { | ||||
|     const inspectorTabs = page.getByRole('tablist'); | ||||
|     const inspectorTab = inspectorTabs.getByTitle(name); | ||||
|     const inspectorTabClass = await inspectorTab.getAttribute('class'); | ||||
|     const isSelectedInspectorTab = inspectorTabClass.includes('is-current'); | ||||
|   const inspectorTabs = page.getByRole('tablist'); | ||||
|   const inspectorTab = inspectorTabs.getByTitle(name); | ||||
|   const inspectorTabClass = await inspectorTab.getAttribute('class'); | ||||
|   const isSelectedInspectorTab = inspectorTabClass.includes('is-current'); | ||||
|  | ||||
|     // do not click a tab that is already selected or it will timeout your test | ||||
|     // do to a { pointer-events: none; } on selected tabs | ||||
|     if (!isSelectedInspectorTab) { | ||||
|         await inspectorTab.click(); | ||||
|     } | ||||
|   // do not click a tab that is already selected or it will timeout your test | ||||
|   // do to a { pointer-events: none; } on selected tabs | ||||
|   if (!isSelectedInspectorTab) { | ||||
|     await inspectorTab.click(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Waits and asserts that all plot series data on the page | ||||
|  * is loaded and drawn. | ||||
|  * | ||||
|  * In lieu of a better way to detect when a plot is done rendering, | ||||
|  * we [attach a class to the '.gl-plot' element](https://github.com/nasa/openmct/blob/5924d7ea95a0c2d4141c602a3c7d0665cb91095f/src/plugins/plot/MctPlot.vue#L27) | ||||
|  * once all pending series data has been loaded. The following appAction retrieves | ||||
|  * all plots on the page and waits up to the default timeout for the class to be | ||||
|  * attached to each plot. | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function waitForPlotsToRender(page) { | ||||
|   const plotLocator = page.locator('.gl-plot'); | ||||
|   for (const plot of await plotLocator.all()) { | ||||
|     await expect(plot).toHaveClass(/js-series-data-loaded/); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @typedef {Object} PlotPixel | ||||
|  * @property {number} r The value of the red channel (0-255) | ||||
|  * @property {number} g The value of the green channel (0-255) | ||||
|  * @property {number} b The value of the blue channel (0-255) | ||||
|  * @property {number} a The value of the alpha channel (0-255) | ||||
|  * @property {string} strValue The rgba string value of the pixel | ||||
|  */ | ||||
|  | ||||
| /** | ||||
|  * Wait for all plots to render and then retrieve and return an array | ||||
|  * of canvas plot pixel data (RGBA values). | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  * @param {string} canvasSelector The selector for the canvas element | ||||
|  * @return {Promise<PlotPixel[]>} | ||||
|  */ | ||||
| async function getCanvasPixels(page, canvasSelector) { | ||||
|   const getTelemValuePromise = new Promise((resolve) => | ||||
|     page.exposeFunction('getCanvasValue', resolve) | ||||
|   ); | ||||
|   const canvasHandle = await page.evaluateHandle( | ||||
|     (canvas) => document.querySelector(canvas), | ||||
|     canvasSelector | ||||
|   ); | ||||
|   const canvasContextHandle = await page.evaluateHandle( | ||||
|     (canvas) => canvas.getContext('2d'), | ||||
|     canvasHandle | ||||
|   ); | ||||
|  | ||||
|   await waitForPlotsToRender(page); | ||||
|   await page.evaluate( | ||||
|     ([canvas, ctx]) => { | ||||
|       // The document canvas is where the plot points and lines are drawn. | ||||
|       // The only way to access the canvas is using document (using page.evaluate) | ||||
|       /** @type {ImageData} */ | ||||
|       const data = ctx.getImageData(0, 0, canvas.width, canvas.height).data; | ||||
|       /** @type {number[]} */ | ||||
|       const imageDataValues = Object.values(data); | ||||
|       /** @type {PlotPixel[]} */ | ||||
|       const plotPixels = []; | ||||
|       // Each pixel consists of four values within the ImageData.data array. The for loop iterates by multiples of four. | ||||
|       // The values associated with each pixel are R (red), G (green), B (blue), and A (alpha), in that order. | ||||
|       for (let i = 0; i < imageDataValues.length; ) { | ||||
|         if (imageDataValues[i] > 0) { | ||||
|           plotPixels.push({ | ||||
|             r: imageDataValues[i], | ||||
|             g: imageDataValues[i + 1], | ||||
|             b: imageDataValues[i + 2], | ||||
|             a: imageDataValues[i + 3], | ||||
|             strValue: `rgb(${imageDataValues[i]}, ${imageDataValues[i + 1]}, ${ | ||||
|               imageDataValues[i + 2] | ||||
|             }, ${imageDataValues[i + 3]})` | ||||
|           }); | ||||
|         } | ||||
|  | ||||
|         i = i + 4; | ||||
|       } | ||||
|  | ||||
|       window.getCanvasValue(plotPixels); | ||||
|     }, | ||||
|     [canvasHandle, canvasContextHandle] | ||||
|   ); | ||||
|  | ||||
|   return getTelemValuePromise; | ||||
| } | ||||
|  | ||||
| // eslint-disable-next-line no-undef | ||||
| module.exports = { | ||||
|     createDomainObjectWithDefaults, | ||||
|     createNotification, | ||||
|     expandTreePaneItemByName, | ||||
|     expandEntireTree, | ||||
|     createPlanFromJSON, | ||||
|     openObjectTreeContextMenu, | ||||
|     getHashUrlToDomainObject, | ||||
|     getFocusedObjectUuid, | ||||
|     setFixedTimeMode, | ||||
|     setRealTimeMode, | ||||
|     setStartOffset, | ||||
|     setEndOffset, | ||||
|     selectInspectorTab | ||||
|   createDomainObjectWithDefaults, | ||||
|   createNotification, | ||||
|   createPlanFromJSON, | ||||
|   expandEntireTree, | ||||
|   expandTreePaneItemByName, | ||||
|   getCanvasPixels, | ||||
|   getHashUrlToDomainObject, | ||||
|   getFocusedObjectUuid, | ||||
|   openObjectTreeContextMenu, | ||||
|   setFixedTimeMode, | ||||
|   setRealTimeMode, | ||||
|   setStartOffset, | ||||
|   setEndOffset, | ||||
|   selectInspectorTab, | ||||
|   waitForPlotsToRender | ||||
| }; | ||||
|   | ||||
| @@ -43,9 +43,9 @@ const sinon = require('sinon'); | ||||
|  * @returns {String} formatted string with message type, text, url, and line and column numbers | ||||
|  */ | ||||
| function _consoleMessageToString(msg) { | ||||
|     const { url, lineNumber, columnNumber } = msg.location(); | ||||
|   const { url, lineNumber, columnNumber } = msg.location(); | ||||
|  | ||||
|     return `[${msg.type()}] ${msg.text()} at (${url} ${lineNumber}:${columnNumber})`; | ||||
|   return `[${msg.type()}] ${msg.text()} at (${url} ${lineNumber}:${columnNumber})`; | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -56,12 +56,9 @@ function _consoleMessageToString(msg) { | ||||
|  * @return {Promise<Animation[]>} | ||||
|  */ | ||||
| function waitForAnimations(locator) { | ||||
|     return locator | ||||
|         .evaluate((element) => | ||||
|             Promise.all( | ||||
|                 element | ||||
|                     .getAnimations({ subtree: true }) | ||||
|                     .map((animation) => animation.finished))); | ||||
|   return locator.evaluate((element) => | ||||
|     Promise.all(element.getAnimations({ subtree: true }).map((animation) => animation.finished)) | ||||
|   ); | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -72,103 +69,113 @@ function waitForAnimations(locator) { | ||||
| const istanbulCLIOutput = path.join(process.cwd(), '.nyc_output'); | ||||
|  | ||||
| exports.test = base.test.extend({ | ||||
|     /** | ||||
|      * This allows the test to manipulate the browser clock. This is useful for Visual and Snapshot tests which need | ||||
|      * the Time Indicator Clock to be in a specific state. | ||||
|      * Usage: | ||||
|      * ``` | ||||
|      * test.use({ | ||||
|      *   clockOptions: { | ||||
|      *       now: 0, | ||||
|      *       shouldAdvanceTime: true | ||||
|      * ``` | ||||
|      * If clockOptions are provided, will override the default clock with fake timers provided by SinonJS. | ||||
|      * | ||||
|      * Default: `undefined` | ||||
|      * | ||||
|      * @see {@link https://github.com/microsoft/playwright/issues/6347 Github RFE} | ||||
|      * @see {@link https://github.com/sinonjs/fake-timers/#var-clock--faketimersinstallconfig SinonJS FakeTimers Config} | ||||
|      */ | ||||
|     clockOptions: [undefined, { option: true }], | ||||
|     overrideClock: [async ({ context, clockOptions }, use) => { | ||||
|         if (clockOptions !== undefined) { | ||||
|             await context.addInitScript({ | ||||
|                 path: path.join(__dirname, '../', './node_modules/sinon/pkg/sinon.js') | ||||
|             }); | ||||
|             await context.addInitScript((options) => { | ||||
|                 window.__clock = sinon.useFakeTimers(options); | ||||
|             }, clockOptions); | ||||
|         } | ||||
|  | ||||
|         await use(context); | ||||
|     }, { | ||||
|         auto: true, | ||||
|         scope: 'test' | ||||
|     }], | ||||
|     /** | ||||
|      * Extends the base context class to add codecoverage shim. | ||||
|      * @see {@link https://github.com/mxschmitt/playwright-test-coverage Github Project} | ||||
|      */ | ||||
|     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); | ||||
|             } | ||||
|   /** | ||||
|    * This allows the test to manipulate the browser clock. This is useful for Visual and Snapshot tests which need | ||||
|    * the Time Indicator Clock to be in a specific state. | ||||
|    * Usage: | ||||
|    * ``` | ||||
|    * test.use({ | ||||
|    *   clockOptions: { | ||||
|    *       now: 0, | ||||
|    *       shouldAdvanceTime: true | ||||
|    * ``` | ||||
|    * If clockOptions are provided, will override the default clock with fake timers provided by SinonJS. | ||||
|    * | ||||
|    * Default: `undefined` | ||||
|    * | ||||
|    * @see {@link https://github.com/microsoft/playwright/issues/6347 Github RFE} | ||||
|    * @see {@link https://github.com/sinonjs/fake-timers/#var-clock--faketimersinstallconfig SinonJS FakeTimers Config} | ||||
|    */ | ||||
|   clockOptions: [undefined, { option: true }], | ||||
|   overrideClock: [ | ||||
|     async ({ context, clockOptions }, use) => { | ||||
|       if (clockOptions !== undefined) { | ||||
|         await context.addInitScript({ | ||||
|           path: path.join(__dirname, '../', './node_modules/sinon/pkg/sinon.js') | ||||
|         }); | ||||
|         await context.addInitScript((options) => { | ||||
|           window.__clock = sinon.useFakeTimers(options); | ||||
|         }, clockOptions); | ||||
|       } | ||||
|  | ||||
|         await use(context); | ||||
|         for (const page of context.pages()) { | ||||
|             await page.evaluate(() => (window).collectIstanbulCoverage(JSON.stringify((window).__coverage__))); | ||||
|         } | ||||
|       await use(context); | ||||
|     }, | ||||
|     /** | ||||
|      * If true, will assert against any console.error calls that occur during the test. Assertions occur | ||||
|      * during test teardown (after the test has completed). | ||||
|      * | ||||
|      * Default: `true` | ||||
|      */ | ||||
|     failOnConsoleError: [true, { option: true }], | ||||
|     /** | ||||
|      * Extends the base page class to enable console log error detection. | ||||
|      * @see {@link https://github.com/microsoft/playwright/discussions/11690 Github Discussion} | ||||
|      */ | ||||
|     page: async ({ page, failOnConsoleError }, use) => { | ||||
|         // Capture any console errors during test execution | ||||
|         const messages = []; | ||||
|         page.on('console', (msg) => messages.push(msg)); | ||||
|  | ||||
|         await use(page); | ||||
|  | ||||
|         // Assert against console errors during teardown | ||||
|         if (failOnConsoleError) { | ||||
|             messages.forEach( | ||||
|                 msg => expect.soft(msg.type(), `Console error detected: ${_consoleMessageToString(msg)}`).not.toEqual('error') | ||||
|             ); | ||||
|         } | ||||
|     }, | ||||
|     /** | ||||
|      * Extends the base browser class to enable CDP connection definition in playwright.config.js. Once | ||||
|      * that RFE is implemented, this function can be removed. | ||||
|      * @see {@link https://github.com/microsoft/playwright/issues/8379 Github RFE} | ||||
|      */ | ||||
|     browser: async ({ playwright, browser }, use, workerInfo) => { | ||||
|         // Use browserless if configured | ||||
|         if (workerInfo.project.name.match(/browserless/)) { | ||||
|             const vBrowser = await playwright.chromium.connectOverCDP({ | ||||
|                 endpointURL: 'ws://localhost:3003' | ||||
|             }); | ||||
|             await use(vBrowser); | ||||
|         } else { | ||||
|             // Use Local Browser for testing. | ||||
|             await use(browser); | ||||
|         } | ||||
|     { | ||||
|       auto: true, | ||||
|       scope: 'test' | ||||
|     } | ||||
|   ], | ||||
|   /** | ||||
|    * Extends the base context class to add codecoverage shim. | ||||
|    * @see {@link https://github.com/mxschmitt/playwright-test-coverage Github Project} | ||||
|    */ | ||||
|   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__)) | ||||
|       ); | ||||
|     } | ||||
|   }, | ||||
|   /** | ||||
|    * If true, will assert against any console.error calls that occur during the test. Assertions occur | ||||
|    * during test teardown (after the test has completed). | ||||
|    * | ||||
|    * Default: `true` | ||||
|    */ | ||||
|   failOnConsoleError: [true, { option: true }], | ||||
|   /** | ||||
|    * Extends the base page class to enable console log error detection. | ||||
|    * @see {@link https://github.com/microsoft/playwright/discussions/11690 Github Discussion} | ||||
|    */ | ||||
|   page: async ({ page, failOnConsoleError }, use) => { | ||||
|     // Capture any console errors during test execution | ||||
|     const messages = []; | ||||
|     page.on('console', (msg) => messages.push(msg)); | ||||
|  | ||||
|     await use(page); | ||||
|  | ||||
|     // Assert against console errors during teardown | ||||
|     if (failOnConsoleError) { | ||||
|       messages.forEach((msg) => | ||||
|         expect | ||||
|           .soft(msg.type(), `Console error detected: ${_consoleMessageToString(msg)}`) | ||||
|           .not.toEqual('error') | ||||
|       ); | ||||
|     } | ||||
|   }, | ||||
|   /** | ||||
|    * Extends the base browser class to enable CDP connection definition in playwright.config.js. Once | ||||
|    * that RFE is implemented, this function can be removed. | ||||
|    * @see {@link https://github.com/microsoft/playwright/issues/8379 Github RFE} | ||||
|    */ | ||||
|   browser: async ({ playwright, browser }, use, workerInfo) => { | ||||
|     // Use browserless if configured | ||||
|     if (workerInfo.project.name.match(/browserless/)) { | ||||
|       const vBrowser = await playwright.chromium.connectOverCDP({ | ||||
|         endpointURL: 'ws://localhost:3003' | ||||
|       }); | ||||
|       await use(vBrowser); | ||||
|     } else { | ||||
|       // Use Local Browser for testing. | ||||
|       await use(browser); | ||||
|     } | ||||
|   } | ||||
| }); | ||||
|  | ||||
| exports.expect = expect; | ||||
|   | ||||
| @@ -23,6 +23,6 @@ | ||||
| // This should be used to install the Example Fault Provider, this will also install the FaultManagementPlugin (neither of which are installed by default). | ||||
|  | ||||
| document.addEventListener('DOMContentLoaded', () => { | ||||
|     const openmct = window.openmct; | ||||
|     openmct.install(openmct.plugins.example.ExampleFaultSource()); | ||||
|   const openmct = window.openmct; | ||||
|   openmct.install(openmct.plugins.example.ExampleFaultSource()); | ||||
| }); | ||||
|   | ||||
| @@ -23,8 +23,8 @@ | ||||
| // This should be used to install the Example Fault Provider, this will also install the FaultManagementPlugin (neither of which are installed by default). | ||||
|  | ||||
| document.addEventListener('DOMContentLoaded', () => { | ||||
|     const openmct = window.openmct; | ||||
|     const staticFaults = true; | ||||
|   const openmct = window.openmct; | ||||
|   const staticFaults = true; | ||||
|  | ||||
|     openmct.install(openmct.plugins.example.ExampleFaultSource(staticFaults)); | ||||
|   openmct.install(openmct.plugins.example.ExampleFaultSource(staticFaults)); | ||||
| }); | ||||
|   | ||||
| @@ -22,6 +22,6 @@ | ||||
|  | ||||
| // This should be used to install the Example User | ||||
| document.addEventListener('DOMContentLoaded', () => { | ||||
|     const openmct = window.openmct; | ||||
|     openmct.install(openmct.plugins.example.ExampleUser()); | ||||
|   const openmct = window.openmct; | ||||
|   openmct.install(openmct.plugins.example.ExampleUser()); | ||||
| }); | ||||
|   | ||||
| @@ -23,6 +23,6 @@ | ||||
| // This should be used to install the Example Fault Provider, this will also install the FaultManagementPlugin (neither of which are installed by default). | ||||
|  | ||||
| document.addEventListener('DOMContentLoaded', () => { | ||||
|     const openmct = window.openmct; | ||||
|     openmct.install(openmct.plugins.FaultManagement()); | ||||
|   const openmct = window.openmct; | ||||
|   openmct.install(openmct.plugins.FaultManagement()); | ||||
| }); | ||||
|   | ||||
| @@ -1,76 +1,71 @@ | ||||
| class DomainObjectViewProvider { | ||||
|     constructor(openmct) { | ||||
|         this.key = 'doViewProvider'; | ||||
|         this.name = 'Domain Object View Provider'; | ||||
|         this.openmct = openmct; | ||||
|     } | ||||
|   constructor(openmct) { | ||||
|     this.key = 'doViewProvider'; | ||||
|     this.name = 'Domain Object View Provider'; | ||||
|     this.openmct = openmct; | ||||
|   } | ||||
|  | ||||
|     canView(domainObject) { | ||||
|         return domainObject.type === 'imageFileInput' | ||||
|             || domainObject.type === 'jsonFileInput'; | ||||
|     } | ||||
|   canView(domainObject) { | ||||
|     return domainObject.type === 'imageFileInput' || domainObject.type === 'jsonFileInput'; | ||||
|   } | ||||
|  | ||||
|     view(domainObject, objectPath) { | ||||
|         let content; | ||||
|   view(domainObject, objectPath) { | ||||
|     let content; | ||||
|  | ||||
|         return { | ||||
|             show: function (element) { | ||||
|                 const body = domainObject.selectFile.body; | ||||
|                 const type = typeof body; | ||||
|     return { | ||||
|       show: function (element) { | ||||
|         const body = domainObject.selectFile.body; | ||||
|         const type = typeof body; | ||||
|  | ||||
|                 content = document.createElement('div'); | ||||
|                 content.id = 'file-input-type'; | ||||
|                 content.textContent = JSON.stringify(type); | ||||
|                 element.appendChild(content); | ||||
|             }, | ||||
|             destroy: function (element) { | ||||
|                 element.removeChild(content); | ||||
|                 content = undefined; | ||||
|             } | ||||
|         }; | ||||
|     } | ||||
|         content = document.createElement('div'); | ||||
|         content.id = 'file-input-type'; | ||||
|         content.textContent = JSON.stringify(type); | ||||
|         element.appendChild(content); | ||||
|       }, | ||||
|       destroy: function (element) { | ||||
|         element.removeChild(content); | ||||
|         content = undefined; | ||||
|       } | ||||
|     }; | ||||
|   } | ||||
| } | ||||
|  | ||||
| document.addEventListener('DOMContentLoaded', () => { | ||||
|     const openmct = window.openmct; | ||||
|   const openmct = window.openmct; | ||||
|  | ||||
|     openmct.types.addType('jsonFileInput', { | ||||
|         key: 'jsonFileInput', | ||||
|         name: "JSON File Input Object", | ||||
|         creatable: true, | ||||
|         form: [ | ||||
|             { | ||||
|                 name: 'Upload File', | ||||
|                 key: 'selectFile', | ||||
|                 control: 'file-input', | ||||
|                 required: true, | ||||
|                 text: 'Select File...', | ||||
|                 type: 'application/json', | ||||
|                 property: [ | ||||
|                     "selectFile" | ||||
|                 ] | ||||
|             } | ||||
|         ] | ||||
|     }); | ||||
|   openmct.types.addType('jsonFileInput', { | ||||
|     key: 'jsonFileInput', | ||||
|     name: 'JSON File Input Object', | ||||
|     creatable: true, | ||||
|     form: [ | ||||
|       { | ||||
|         name: 'Upload File', | ||||
|         key: 'selectFile', | ||||
|         control: 'file-input', | ||||
|         required: true, | ||||
|         text: 'Select File...', | ||||
|         type: 'application/json', | ||||
|         property: ['selectFile'] | ||||
|       } | ||||
|     ] | ||||
|   }); | ||||
|  | ||||
|     openmct.types.addType('imageFileInput', { | ||||
|         key: 'imageFileInput', | ||||
|         name: "Image File Input Object", | ||||
|         creatable: true, | ||||
|         form: [ | ||||
|             { | ||||
|                 name: 'Upload File', | ||||
|                 key: 'selectFile', | ||||
|                 control: 'file-input', | ||||
|                 required: true, | ||||
|                 text: 'Select File...', | ||||
|                 type: 'image/*', | ||||
|                 property: [ | ||||
|                     "selectFile" | ||||
|                 ] | ||||
|             } | ||||
|         ] | ||||
|     }); | ||||
|   openmct.types.addType('imageFileInput', { | ||||
|     key: 'imageFileInput', | ||||
|     name: 'Image File Input Object', | ||||
|     creatable: true, | ||||
|     form: [ | ||||
|       { | ||||
|         name: 'Upload File', | ||||
|         key: 'selectFile', | ||||
|         control: 'file-input', | ||||
|         required: true, | ||||
|         text: 'Select File...', | ||||
|         type: 'image/*', | ||||
|         property: ['selectFile'] | ||||
|       } | ||||
|     ] | ||||
|   }); | ||||
|  | ||||
|     openmct.objectViews.addProvider(new DomainObjectViewProvider(openmct)); | ||||
|   openmct.objectViews.addProvider(new DomainObjectViewProvider(openmct)); | ||||
| }); | ||||
|   | ||||
| @@ -27,6 +27,6 @@ const NOTEBOOK_NAME = 'Notebook'; | ||||
| const URL_WHITELIST = ['google.com']; | ||||
|  | ||||
| document.addEventListener('DOMContentLoaded', () => { | ||||
|     const openmct = window.openmct; | ||||
|     openmct.install(openmct.plugins.Notebook(NOTEBOOK_NAME, URL_WHITELIST)); | ||||
|   const openmct = window.openmct; | ||||
|   openmct.install(openmct.plugins.Notebook(NOTEBOOK_NAME, URL_WHITELIST)); | ||||
| }); | ||||
|   | ||||
| @@ -22,6 +22,6 @@ | ||||
|  | ||||
| // This should be used to install the Operator Status | ||||
| document.addEventListener('DOMContentLoaded', () => { | ||||
|     const openmct = window.openmct; | ||||
|     openmct.install(openmct.plugins.OperatorStatus()); | ||||
|   const openmct = window.openmct; | ||||
|   openmct.install(openmct.plugins.OperatorStatus()); | ||||
| }); | ||||
|   | ||||
| @@ -25,6 +25,6 @@ | ||||
| // await page.addInitScript({ path: path.join(__dirname, 'addInitRestrictedNotebook.js') }); | ||||
|  | ||||
| document.addEventListener('DOMContentLoaded', () => { | ||||
|     const openmct = window.openmct; | ||||
|     openmct.install(openmct.plugins.RestrictedNotebook('CUSTOM_NAME')); | ||||
|   const openmct = window.openmct; | ||||
|   openmct.install(openmct.plugins.RestrictedNotebook('CUSTOM_NAME')); | ||||
| }); | ||||
|   | ||||
| @@ -1,27 +1,27 @@ | ||||
| (function () { | ||||
|     document.addEventListener('DOMContentLoaded', () => { | ||||
|         const PERSISTENCE_KEY = 'persistence-tests'; | ||||
|         const openmct = window.openmct; | ||||
|   document.addEventListener('DOMContentLoaded', () => { | ||||
|     const PERSISTENCE_KEY = 'persistence-tests'; | ||||
|     const openmct = window.openmct; | ||||
|  | ||||
|         openmct.objects.addRoot({ | ||||
|             namespace: PERSISTENCE_KEY, | ||||
|             key: PERSISTENCE_KEY | ||||
|         }); | ||||
|  | ||||
|         openmct.objects.addProvider(PERSISTENCE_KEY, { | ||||
|             get(identifier) { | ||||
|                 if (identifier.key !== PERSISTENCE_KEY) { | ||||
|                     return undefined; | ||||
|                 } else { | ||||
|                     return Promise.resolve({ | ||||
|                         identifier, | ||||
|                         type: 'folder', | ||||
|                         name: 'Persistence Testing', | ||||
|                         location: 'ROOT', | ||||
|                         composition: [] | ||||
|                     }); | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|     openmct.objects.addRoot({ | ||||
|       namespace: PERSISTENCE_KEY, | ||||
|       key: PERSISTENCE_KEY | ||||
|     }); | ||||
| }()); | ||||
|  | ||||
|     openmct.objects.addProvider(PERSISTENCE_KEY, { | ||||
|       get(identifier) { | ||||
|         if (identifier.key !== PERSISTENCE_KEY) { | ||||
|           return undefined; | ||||
|         } else { | ||||
|           return Promise.resolve({ | ||||
|             identifier, | ||||
|             type: 'folder', | ||||
|             name: 'Persistence Testing', | ||||
|             location: 'ROOT', | ||||
|             composition: [] | ||||
|           }); | ||||
|         } | ||||
|       } | ||||
|     }); | ||||
|   }); | ||||
| })(); | ||||
|   | ||||
| @@ -19,264 +19,275 @@ | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* global __dirname */ | ||||
| const path = require('path'); | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function navigateToFaultManagementWithExample(page) { | ||||
|     // eslint-disable-next-line no-undef | ||||
|     await page.addInitScript({ path: path.join(__dirname, './', 'addInitExampleFaultProvider.js') }); | ||||
|   await page.addInitScript({ path: path.join(__dirname, './', 'addInitExampleFaultProvider.js') }); | ||||
|  | ||||
|     await navigateToFaultItemInTree(page); | ||||
|   await navigateToFaultItemInTree(page); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function navigateToFaultManagementWithStaticExample(page) { | ||||
|     // eslint-disable-next-line no-undef | ||||
|     await page.addInitScript({ path: path.join(__dirname, './', 'addInitExampleFaultProviderStatic.js') }); | ||||
|   await page.addInitScript({ | ||||
|     path: path.join(__dirname, './', 'addInitExampleFaultProviderStatic.js') | ||||
|   }); | ||||
|  | ||||
|     await navigateToFaultItemInTree(page); | ||||
|   await navigateToFaultItemInTree(page); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function navigateToFaultManagementWithoutExample(page) { | ||||
|     // eslint-disable-next-line no-undef | ||||
|     await page.addInitScript({ path: path.join(__dirname, './', 'addInitFaultManagementPlugin.js') }); | ||||
|   await page.addInitScript({ path: path.join(__dirname, './', 'addInitFaultManagementPlugin.js') }); | ||||
|  | ||||
|     await navigateToFaultItemInTree(page); | ||||
|   await navigateToFaultItemInTree(page); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function navigateToFaultItemInTree(page) { | ||||
|     await page.goto('./', { waitUntil: 'networkidle' }); | ||||
|   await page.goto('./', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|     const faultManagementTreeItem = page.getByRole('tree', { | ||||
|         name: "Main Tree" | ||||
|     }).getByRole('treeitem', { | ||||
|         name: "Fault Management" | ||||
|   const faultManagementTreeItem = page | ||||
|     .getByRole('tree', { | ||||
|       name: 'Main Tree' | ||||
|     }) | ||||
|     .getByRole('treeitem', { | ||||
|       name: 'Fault Management' | ||||
|     }); | ||||
|  | ||||
|     // Navigate to "Fault Management" from the tree | ||||
|     await faultManagementTreeItem.click(); | ||||
|   // Navigate to "Fault Management" from the tree | ||||
|   await faultManagementTreeItem.click(); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function acknowledgeFault(page, rowNumber) { | ||||
|     await openFaultRowMenu(page, rowNumber); | ||||
|     await page.locator('.c-menu >> text="Acknowledge"').click(); | ||||
|     // Click [aria-label="Save"] | ||||
|     await page.locator('[aria-label="Save"]').click(); | ||||
|  | ||||
|   await openFaultRowMenu(page, rowNumber); | ||||
|   await page.locator('.c-menu >> text="Acknowledge"').click(); | ||||
|   // Click [aria-label="Save"] | ||||
|   await page.locator('[aria-label="Save"]').click(); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function shelveMultipleFaults(page, ...nums) { | ||||
|     const selectRows = nums.map((num) => { | ||||
|         return selectFaultItem(page, num); | ||||
|     }); | ||||
|     await Promise.all(selectRows); | ||||
|   const selectRows = nums.map((num) => { | ||||
|     return selectFaultItem(page, num); | ||||
|   }); | ||||
|   await Promise.all(selectRows); | ||||
|  | ||||
|     await page.locator('button:has-text("Shelve")').click(); | ||||
|     await page.locator('[aria-label="Save"]').click(); | ||||
|   await page.locator('button:has-text("Shelve")').click(); | ||||
|   await page.locator('[aria-label="Save"]').click(); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function acknowledgeMultipleFaults(page, ...nums) { | ||||
|     const selectRows = nums.map((num) => { | ||||
|         return selectFaultItem(page, num); | ||||
|     }); | ||||
|     await Promise.all(selectRows); | ||||
|   const selectRows = nums.map((num) => { | ||||
|     return selectFaultItem(page, num); | ||||
|   }); | ||||
|   await Promise.all(selectRows); | ||||
|  | ||||
|     await page.locator('button:has-text("Acknowledge")').click(); | ||||
|     await page.locator('[aria-label="Save"]').click(); | ||||
|   await page.locator('button:has-text("Acknowledge")').click(); | ||||
|   await page.locator('[aria-label="Save"]').click(); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function shelveFault(page, rowNumber) { | ||||
|     await openFaultRowMenu(page, rowNumber); | ||||
|     await page.locator('.c-menu >> text="Shelve"').click(); | ||||
|     // Click [aria-label="Save"] | ||||
|     await page.locator('[aria-label="Save"]').click(); | ||||
|   await openFaultRowMenu(page, rowNumber); | ||||
|   await page.locator('.c-menu >> text="Shelve"').click(); | ||||
|   // Click [aria-label="Save"] | ||||
|   await page.locator('[aria-label="Save"]').click(); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function changeViewTo(page, view) { | ||||
|     await page.locator('.c-fault-mgmt__search-row select').first().selectOption(view); | ||||
|   await page.locator('.c-fault-mgmt__search-row select').first().selectOption(view); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function sortFaultsBy(page, sort) { | ||||
|     await page.locator('.c-fault-mgmt__list-header-sortButton select').selectOption(sort); | ||||
|   await page.locator('.c-fault-mgmt__list-header-sortButton select').selectOption(sort); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function enterSearchTerm(page, term) { | ||||
|     await page.locator('.c-fault-mgmt-search [aria-label="Search Input"]').fill(term); | ||||
|   await page.locator('.c-fault-mgmt-search [aria-label="Search Input"]').fill(term); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function clearSearch(page) { | ||||
|     await enterSearchTerm(page, ''); | ||||
|   await enterSearchTerm(page, ''); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function selectFaultItem(page, rowNumber) { | ||||
|     await page.locator(`.c-fault-mgmt-item > input >> nth=${rowNumber - 1}`).check(); | ||||
|   await page.locator(`.c-fault-mgmt-item > input >> nth=${rowNumber - 1}`).check(); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function getHighestSeverity(page) { | ||||
|     const criticalCount = await page.locator('[title=CRITICAL]').count(); | ||||
|     const warningCount = await page.locator('[title=WARNING]').count(); | ||||
|   const criticalCount = await page.locator('[title=CRITICAL]').count(); | ||||
|   const warningCount = await page.locator('[title=WARNING]').count(); | ||||
|  | ||||
|     if (criticalCount > 0) { | ||||
|         return 'CRITICAL'; | ||||
|     } else if (warningCount > 0) { | ||||
|         return 'WARNING'; | ||||
|     } | ||||
|   if (criticalCount > 0) { | ||||
|     return 'CRITICAL'; | ||||
|   } else if (warningCount > 0) { | ||||
|     return 'WARNING'; | ||||
|   } | ||||
|  | ||||
|     return 'WATCH'; | ||||
|   return 'WATCH'; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function getLowestSeverity(page) { | ||||
|     const warningCount = await page.locator('[title=WARNING]').count(); | ||||
|     const watchCount = await page.locator('[title=WATCH]').count(); | ||||
|   const warningCount = await page.locator('[title=WARNING]').count(); | ||||
|   const watchCount = await page.locator('[title=WATCH]').count(); | ||||
|  | ||||
|     if (watchCount > 0) { | ||||
|         return 'WATCH'; | ||||
|     } else if (warningCount > 0) { | ||||
|         return 'WARNING'; | ||||
|     } | ||||
|   if (watchCount > 0) { | ||||
|     return 'WATCH'; | ||||
|   } else if (warningCount > 0) { | ||||
|     return 'WARNING'; | ||||
|   } | ||||
|  | ||||
|     return 'CRITICAL'; | ||||
|   return 'CRITICAL'; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function getFaultResultCount(page) { | ||||
|     const count = await page.locator('.c-faults-list-view-item-body > .c-fault-mgmt__list').count(); | ||||
|   const count = await page.locator('.c-faults-list-view-item-body > .c-fault-mgmt__list').count(); | ||||
|  | ||||
|     return count; | ||||
|   return count; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| function getFault(page, rowNumber) { | ||||
|     const fault = page.locator(`.c-faults-list-view-item-body > .c-fault-mgmt__list >> nth=${rowNumber - 1}`); | ||||
|   const fault = page.locator( | ||||
|     `.c-faults-list-view-item-body > .c-fault-mgmt__list >> nth=${rowNumber - 1}` | ||||
|   ); | ||||
|  | ||||
|     return fault; | ||||
|   return fault; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| function getFaultByName(page, name) { | ||||
|     const fault = page.locator(`.c-fault-mgmt__list-faultname:has-text("${name}")`); | ||||
|   const fault = page.locator(`.c-fault-mgmt__list-faultname:has-text("${name}")`); | ||||
|  | ||||
|     return fault; | ||||
|   return fault; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function getFaultName(page, rowNumber) { | ||||
|     const faultName = await page.locator(`.c-fault-mgmt__list-faultname >> nth=${rowNumber - 1}`).textContent(); | ||||
|   const faultName = await page | ||||
|     .locator(`.c-fault-mgmt__list-faultname >> nth=${rowNumber - 1}`) | ||||
|     .textContent(); | ||||
|  | ||||
|     return faultName; | ||||
|   return faultName; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function getFaultSeverity(page, rowNumber) { | ||||
|     const faultSeverity = await page.locator(`.c-faults-list-view-item-body .c-fault-mgmt__list-severity >> nth=${rowNumber - 1}`).getAttribute('title'); | ||||
|   const faultSeverity = await page | ||||
|     .locator(`.c-faults-list-view-item-body .c-fault-mgmt__list-severity >> nth=${rowNumber - 1}`) | ||||
|     .getAttribute('title'); | ||||
|  | ||||
|     return faultSeverity; | ||||
|   return faultSeverity; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function getFaultNamespace(page, rowNumber) { | ||||
|     const faultNamespace = await page.locator(`.c-fault-mgmt__list-path >> nth=${rowNumber - 1}`).textContent(); | ||||
|   const faultNamespace = await page | ||||
|     .locator(`.c-fault-mgmt__list-path >> nth=${rowNumber - 1}`) | ||||
|     .textContent(); | ||||
|  | ||||
|     return faultNamespace; | ||||
|   return faultNamespace; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function getFaultTriggerTime(page, rowNumber) { | ||||
|     const faultTriggerTime = await page.locator(`.c-fault-mgmt__list-trigTime >> nth=${rowNumber - 1} >> .c-fault-mgmt-item__value`).textContent(); | ||||
|   const faultTriggerTime = await page | ||||
|     .locator(`.c-fault-mgmt__list-trigTime >> nth=${rowNumber - 1} >> .c-fault-mgmt-item__value`) | ||||
|     .textContent(); | ||||
|  | ||||
|     return faultTriggerTime.toString().trim(); | ||||
|   return faultTriggerTime.toString().trim(); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function openFaultRowMenu(page, rowNumber) { | ||||
|     // select | ||||
|     await page.locator(`.c-fault-mgmt-item > .c-fault-mgmt__list-action-button >> nth=${rowNumber - 1}`).click(); | ||||
|  | ||||
|   // select | ||||
|   await page | ||||
|     .locator(`.c-fault-mgmt-item > .c-fault-mgmt__list-action-button >> nth=${rowNumber - 1}`) | ||||
|     .click(); | ||||
| } | ||||
|  | ||||
| // eslint-disable-next-line no-undef | ||||
| module.exports = { | ||||
|     navigateToFaultManagementWithExample, | ||||
|     navigateToFaultManagementWithStaticExample, | ||||
|     navigateToFaultManagementWithoutExample, | ||||
|     navigateToFaultItemInTree, | ||||
|     acknowledgeFault, | ||||
|     shelveMultipleFaults, | ||||
|     acknowledgeMultipleFaults, | ||||
|     shelveFault, | ||||
|     changeViewTo, | ||||
|     sortFaultsBy, | ||||
|     enterSearchTerm, | ||||
|     clearSearch, | ||||
|     selectFaultItem, | ||||
|     getHighestSeverity, | ||||
|     getLowestSeverity, | ||||
|     getFaultResultCount, | ||||
|     getFault, | ||||
|     getFaultByName, | ||||
|     getFaultName, | ||||
|     getFaultSeverity, | ||||
|     getFaultNamespace, | ||||
|     getFaultTriggerTime, | ||||
|     openFaultRowMenu | ||||
|   navigateToFaultManagementWithExample, | ||||
|   navigateToFaultManagementWithStaticExample, | ||||
|   navigateToFaultManagementWithoutExample, | ||||
|   navigateToFaultItemInTree, | ||||
|   acknowledgeFault, | ||||
|   shelveMultipleFaults, | ||||
|   acknowledgeMultipleFaults, | ||||
|   shelveFault, | ||||
|   changeViewTo, | ||||
|   sortFaultsBy, | ||||
|   enterSearchTerm, | ||||
|   clearSearch, | ||||
|   selectFaultItem, | ||||
|   getHighestSeverity, | ||||
|   getLowestSeverity, | ||||
|   getFaultResultCount, | ||||
|   getFault, | ||||
|   getFaultByName, | ||||
|   getFaultName, | ||||
|   getFaultSeverity, | ||||
|   getFaultNamespace, | ||||
|   getFaultTriggerTime, | ||||
|   openFaultRowMenu | ||||
| }; | ||||
|   | ||||
| @@ -28,29 +28,29 @@ const NOTEBOOK_DROP_AREA = '.c-notebook__drag-area'; | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function enterTextEntry(page, text) { | ||||
|     // Click the 'Add Notebook Entry' area | ||||
|     await page.locator(NOTEBOOK_DROP_AREA).click(); | ||||
|   // Click the 'Add Notebook Entry' area | ||||
|   await page.locator(NOTEBOOK_DROP_AREA).click(); | ||||
|  | ||||
|     // enter text | ||||
|     await page.locator('[aria-label="Notebook Entry"].is-selected div.c-ne__text').fill(text); | ||||
|     await commitEntry(page); | ||||
|   // enter text | ||||
|   await page.locator('[aria-label="Notebook Entry"].is-selected div.c-ne__text').fill(text); | ||||
|   await commitEntry(page); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function dragAndDropEmbed(page, notebookObject) { | ||||
|     // Create example telemetry object | ||||
|     const swg = await createDomainObjectWithDefaults(page, { | ||||
|         type: "Sine Wave Generator" | ||||
|     }); | ||||
|     // Navigate to notebook | ||||
|     await page.goto(notebookObject.url); | ||||
|     // Expand the tree to reveal the notebook | ||||
|     await page.click('button[title="Show selected item in tree"]'); | ||||
|     // Drag and drop the SWG into the notebook | ||||
|     await page.dragAndDrop(`text=${swg.name}`, NOTEBOOK_DROP_AREA); | ||||
|     await commitEntry(page); | ||||
|   // Create example telemetry object | ||||
|   const swg = await createDomainObjectWithDefaults(page, { | ||||
|     type: 'Sine Wave Generator' | ||||
|   }); | ||||
|   // Navigate to notebook | ||||
|   await page.goto(notebookObject.url); | ||||
|   // Expand the tree to reveal the notebook | ||||
|   await page.click('button[title="Show selected item in tree"]'); | ||||
|   // Drag and drop the SWG into the notebook | ||||
|   await page.dragAndDrop(`text=${swg.name}`, NOTEBOOK_DROP_AREA); | ||||
|   await commitEntry(page); | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -58,12 +58,12 @@ async function dragAndDropEmbed(page, notebookObject) { | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function commitEntry(page) { | ||||
|     //Click the Commit Entry button | ||||
|     await page.locator('.c-ne__save-button > button').click(); | ||||
|   //Click the Commit Entry button | ||||
|   await page.locator('.c-ne__save-button > button').click(); | ||||
| } | ||||
|  | ||||
| // eslint-disable-next-line no-undef | ||||
| module.exports = { | ||||
|     enterTextEntry, | ||||
|     dragAndDropEmbed | ||||
|   enterTextEntry, | ||||
|   dragAndDropEmbed | ||||
| }; | ||||
|   | ||||
| @@ -32,46 +32,53 @@ import { expect } from '../pluginFixtures'; | ||||
|  * @param {string} objectUrl The URL of the object to assert against (plan or gantt chart) | ||||
|  */ | ||||
| export async function assertPlanActivities(page, plan, objectUrl) { | ||||
|     const groups = Object.keys(plan); | ||||
|     for (const group of groups) { | ||||
|         for (let i = 0; i < plan[group].length; i++) { | ||||
|             // Set the startBound to the start time of the first activity in the group | ||||
|             const startBound = plan[group][0].start; | ||||
|             // Set the endBound to the end time of the current activity | ||||
|             let endBound = plan[group][i].end; | ||||
|             if (endBound === startBound) { | ||||
|                 // Prevent oddities with setting start and end bound equal | ||||
|                 // via URL params | ||||
|                 endBound += 1; | ||||
|             } | ||||
|   const groups = Object.keys(plan); | ||||
|   for (const group of groups) { | ||||
|     for (let i = 0; i < plan[group].length; i++) { | ||||
|       // Set the startBound to the start time of the first activity in the group | ||||
|       const startBound = plan[group][0].start; | ||||
|       // Set the endBound to the end time of the current activity | ||||
|       let endBound = plan[group][i].end; | ||||
|       if (endBound === startBound) { | ||||
|         // Prevent oddities with setting start and end bound equal | ||||
|         // via URL params | ||||
|         endBound += 1; | ||||
|       } | ||||
|  | ||||
|             // Switch to fixed time mode with all plan events within the bounds | ||||
|             await page.goto(`${objectUrl}?tc.mode=fixed&tc.startBound=${startBound}&tc.endBound=${endBound}&tc.timeSystem=utc&view=plan.view`); | ||||
|       // Switch to fixed time mode with all plan events within the bounds | ||||
|       await page.goto( | ||||
|         `${objectUrl}?tc.mode=fixed&tc.startBound=${startBound}&tc.endBound=${endBound}&tc.timeSystem=utc&view=plan.view` | ||||
|       ); | ||||
|  | ||||
|             // Assert that the number of activities in the plan view matches the number of | ||||
|             // activities in the plan data within the specified time bounds | ||||
|             const eventCount = await page.locator('.activity-bounds').count(); | ||||
|             expect(eventCount).toEqual(Object.values(plan) | ||||
|                 .flat() | ||||
|                 .filter(event => | ||||
|                     activitiesWithinTimeBounds(event.start, event.end, startBound, endBound)).length); | ||||
|         } | ||||
|       // Assert that the number of activities in the plan view matches the number of | ||||
|       // activities in the plan data within the specified time bounds | ||||
|       const eventCount = await page.locator('.activity-bounds').count(); | ||||
|       expect(eventCount).toEqual( | ||||
|         Object.values(plan) | ||||
|           .flat() | ||||
|           .filter((event) => | ||||
|             activitiesWithinTimeBounds(event.start, event.end, startBound, endBound) | ||||
|           ).length | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Returns true if the activities time bounds overlap, false otherwise. | ||||
| * @param {number} start1 the start time of the first activity | ||||
| * @param {number} end1 the end time of the first activity | ||||
| * @param {number} start2 the start time of the second activity | ||||
| * @param {number} end2 the end time of the second activity | ||||
| * @returns {boolean} true if the activities overlap, false otherwise | ||||
| */ | ||||
|  * @param {number} start1 the start time of the first activity | ||||
|  * @param {number} end1 the end time of the first activity | ||||
|  * @param {number} start2 the start time of the second activity | ||||
|  * @param {number} end2 the end time of the second activity | ||||
|  * @returns {boolean} true if the activities overlap, false otherwise | ||||
|  */ | ||||
| function activitiesWithinTimeBounds(start1, end1, start2, end2) { | ||||
|     return (start1 >= start2 && start1 <= end2) | ||||
|          || (end1 >= start2 && end1 <= end2) | ||||
|          || (start2 >= start1 && start2 <= end1) | ||||
|          || (end2 >= start1 && end2 <= end1); | ||||
|   return ( | ||||
|     (start1 >= start2 && start1 <= end2) || | ||||
|     (end1 >= start2 && end1 <= end2) || | ||||
|     (start2 >= start1 && start2 <= end1) || | ||||
|     (end2 >= start1 && end2 <= end1) | ||||
|   ); | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -82,11 +89,13 @@ function activitiesWithinTimeBounds(start1, end1, start2, end2) { | ||||
|  * @param {string} planObjectUrl | ||||
|  */ | ||||
| export async function setBoundsToSpanAllActivities(page, planJson, planObjectUrl) { | ||||
|     const activities = Object.values(planJson).flat(); | ||||
|     // Get the earliest start value | ||||
|     const start = Math.min(...activities.map(activity => activity.start)); | ||||
|     // Get the latest end value | ||||
|     const end = Math.max(...activities.map(activity => activity.end)); | ||||
|     // Set the start and end bounds to the earliest start and latest end | ||||
|     await page.goto(`${planObjectUrl}?tc.mode=fixed&tc.startBound=${start}&tc.endBound=${end}&tc.timeSystem=utc&view=plan.view`); | ||||
|   const activities = Object.values(planJson).flat(); | ||||
|   // Get the earliest start value | ||||
|   const start = Math.min(...activities.map((activity) => activity.start)); | ||||
|   // Get the latest end value | ||||
|   const end = Math.max(...activities.map((activity) => activity.end)); | ||||
|   // Set the start and end bounds to the earliest start and latest end | ||||
|   await page.goto( | ||||
|     `${planObjectUrl}?tc.mode=fixed&tc.startBound=${start}&tc.endBound=${end}&tc.timeSystem=utc&view=plan.view` | ||||
|   ); | ||||
| } | ||||
|   | ||||
| @@ -25,6 +25,6 @@ | ||||
| // await page.addInitScript({ path: path.join(__dirname, 'useSnowTheme.js') }); | ||||
|  | ||||
| document.addEventListener('DOMContentLoaded', () => { | ||||
|     const openmct = window.openmct; | ||||
|     openmct.install(openmct.plugins.Snow()); | ||||
|   const openmct = window.openmct; | ||||
|   openmct.install(openmct.plugins.Snow()); | ||||
| }); | ||||
|   | ||||
| @@ -9,74 +9,77 @@ const NUM_WORKERS = 2; | ||||
|  | ||||
| /** @type {import('@playwright/test').PlaywrightTestConfig} */ | ||||
| const config = { | ||||
|     retries: 2, //Retries 2 times for a total of 3 runs. When running sharded and with max-failures=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:coverage', | ||||
|         url: 'http://localhost:8080/#', | ||||
|         timeout: 200 * 1000, | ||||
|         reuseExistingServer: false | ||||
|   retries: 2, //Retries 2 times for a total of 3 runs. When running sharded and with max-failures=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:coverage', | ||||
|     url: 'http://localhost:8080/#', | ||||
|     timeout: 200 * 1000, | ||||
|     reuseExistingServer: false | ||||
|   }, | ||||
|   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: 'off' | ||||
|   }, | ||||
|   projects: [ | ||||
|     { | ||||
|       name: 'chrome', | ||||
|       testMatch: '**/*.e2e.spec.js', // only run e2e tests | ||||
|       use: { | ||||
|         browserName: 'chromium' | ||||
|       } | ||||
|     }, | ||||
|     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: 'off' | ||||
|     }, | ||||
|     projects: [ | ||||
|         { | ||||
|             name: 'chrome', | ||||
|             testMatch: '**/*.e2e.spec.js', // only run e2e tests | ||||
|             use: { | ||||
|                 browserName: 'chromium' | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|             name: 'MMOC', | ||||
|             testMatch: '**/*.e2e.spec.js', // only run e2e tests | ||||
|             grepInvert: /@snapshot/, | ||||
|             use: { | ||||
|                 browserName: 'chromium', | ||||
|                 viewport: { | ||||
|                     width: 2560, | ||||
|                     height: 1440 | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|             name: 'firefox', | ||||
|             testMatch: '**/*.e2e.spec.js', // only run e2e tests | ||||
|             grepInvert: /@snapshot/, | ||||
|             use: { | ||||
|                 browserName: 'firefox' | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|             name: 'chrome-beta', //Only Chrome Beta is available on ubuntu -- not chrome canary | ||||
|             testMatch: '**/*.e2e.spec.js', // only run e2e tests | ||||
|             grepInvert: /@snapshot/, | ||||
|             use: { | ||||
|                 browserName: 'chromium', | ||||
|                 channel: 'chrome-beta' | ||||
|             } | ||||
|     { | ||||
|       name: 'MMOC', | ||||
|       testMatch: '**/*.e2e.spec.js', // only run e2e tests | ||||
|       grepInvert: /@snapshot/, | ||||
|       use: { | ||||
|         browserName: 'chromium', | ||||
|         viewport: { | ||||
|           width: 2560, | ||||
|           height: 1440 | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       name: 'firefox', | ||||
|       testMatch: '**/*.e2e.spec.js', // only run e2e tests | ||||
|       grepInvert: /@snapshot/, | ||||
|       use: { | ||||
|         browserName: 'firefox' | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       name: 'chrome-beta', //Only Chrome Beta is available on ubuntu -- not chrome canary | ||||
|       testMatch: '**/*.e2e.spec.js', // only run e2e tests | ||||
|       grepInvert: /@snapshot/, | ||||
|       use: { | ||||
|         browserName: 'chromium', | ||||
|         channel: 'chrome-beta' | ||||
|       } | ||||
|     } | ||||
|   ], | ||||
|   reporter: [ | ||||
|     ['list'], | ||||
|     [ | ||||
|       'html', | ||||
|       { | ||||
|         open: 'never', | ||||
|         outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840 | ||||
|       } | ||||
|     ], | ||||
|     reporter: [ | ||||
|         ['list'], | ||||
|         ['html', { | ||||
|             open: 'never', | ||||
|             outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840 | ||||
|         }], | ||||
|         ['junit', { outputFile: '../test-results/results.xml' }], | ||||
|         ['github'], | ||||
|         ['@deploysentinel/playwright'] | ||||
|     ] | ||||
|     ['junit', { outputFile: '../test-results/results.xml' }], | ||||
|     ['github'], | ||||
|     ['@deploysentinel/playwright'] | ||||
|   ] | ||||
| }; | ||||
|  | ||||
| module.exports = config; | ||||
|   | ||||
| @@ -7,98 +7,101 @@ const { devices } = require('@playwright/test'); | ||||
|  | ||||
| /** @type {import('@playwright/test').PlaywrightTestConfig} */ | ||||
| const config = { | ||||
|     retries: 0, | ||||
|     testDir: 'tests', | ||||
|     testIgnore: '**/*.perf.spec.js', | ||||
|     timeout: 30 * 1000, | ||||
|     webServer: { | ||||
|         command: 'npm run start:coverage', | ||||
|         url: 'http://localhost:8080/#', | ||||
|         timeout: 120 * 1000, | ||||
|         reuseExistingServer: true | ||||
|   retries: 0, | ||||
|   testDir: 'tests', | ||||
|   testIgnore: '**/*.perf.spec.js', | ||||
|   timeout: 30 * 1000, | ||||
|   webServer: { | ||||
|     command: 'npm run start:coverage', | ||||
|     url: 'http://localhost:8080/#', | ||||
|     timeout: 120 * 1000, | ||||
|     reuseExistingServer: true | ||||
|   }, | ||||
|   workers: 1, | ||||
|   use: { | ||||
|     browserName: 'chromium', | ||||
|     baseURL: 'http://localhost:8080/', | ||||
|     headless: false, | ||||
|     ignoreHTTPSErrors: true, | ||||
|     screenshot: 'only-on-failure', | ||||
|     trace: 'retain-on-failure', | ||||
|     video: 'off' | ||||
|   }, | ||||
|   projects: [ | ||||
|     { | ||||
|       name: 'chrome', | ||||
|       use: { | ||||
|         browserName: 'chromium' | ||||
|       } | ||||
|     }, | ||||
|     workers: 1, | ||||
|     use: { | ||||
|         browserName: "chromium", | ||||
|         baseURL: 'http://localhost:8080/', | ||||
|         headless: false, | ||||
|         ignoreHTTPSErrors: true, | ||||
|         screenshot: 'only-on-failure', | ||||
|         trace: 'retain-on-failure', | ||||
|         video: 'off' | ||||
|     }, | ||||
|     projects: [ | ||||
|         { | ||||
|             name: 'chrome', | ||||
|             use: { | ||||
|                 browserName: 'chromium' | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|             name: 'MMOC', | ||||
|             testMatch: '**/*.e2e.spec.js', // only run e2e tests | ||||
|             grepInvert: /@snapshot/, | ||||
|             use: { | ||||
|                 browserName: 'chromium', | ||||
|                 viewport: { | ||||
|                     width: 2560, | ||||
|                     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 | ||||
|             } | ||||
|     { | ||||
|       name: 'MMOC', | ||||
|       testMatch: '**/*.e2e.spec.js', // only run e2e tests | ||||
|       grepInvert: /@snapshot/, | ||||
|       use: { | ||||
|         browserName: 'chromium', | ||||
|         viewport: { | ||||
|           width: 2560, | ||||
|           height: 1440 | ||||
|         } | ||||
|     ], | ||||
|     reporter: [ | ||||
|         ['list'], | ||||
|         ['html', { | ||||
|             open: 'on-failure', | ||||
|             outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840 | ||||
|         }] | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       name: 'safari', | ||||
|       testMatch: '**/*.e2e.spec.js', // only run e2e tests | ||||
|       grep: /@ipad/, // only run ipad tests due to this bug https://github.com/microsoft/playwright/issues/8340 | ||||
|       grepInvert: /@snapshot/, | ||||
|       use: { | ||||
|         browserName: 'webkit' | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       name: 'firefox', | ||||
|       testMatch: '**/*.e2e.spec.js', // only run e2e tests | ||||
|       grepInvert: /@snapshot/, | ||||
|       use: { | ||||
|         browserName: 'firefox' | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       name: 'canary', | ||||
|       testMatch: '**/*.e2e.spec.js', // only run e2e tests | ||||
|       grepInvert: /@snapshot/, | ||||
|       use: { | ||||
|         browserName: 'chromium', | ||||
|         channel: 'chrome-canary' //Note this is not available in ubuntu/CircleCI | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       name: 'chrome-beta', | ||||
|       testMatch: '**/*.e2e.spec.js', // only run e2e tests | ||||
|       grepInvert: /@snapshot/, | ||||
|       use: { | ||||
|         browserName: 'chromium', | ||||
|         channel: 'chrome-beta' | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       name: 'ipad', | ||||
|       testMatch: '**/*.e2e.spec.js', // only run e2e tests | ||||
|       grep: /@ipad/, | ||||
|       grepInvert: /@snapshot/, | ||||
|       use: { | ||||
|         browserName: 'webkit', | ||||
|         ...devices['iPad (gen 7) landscape'] // Complete List https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json | ||||
|       } | ||||
|     } | ||||
|   ], | ||||
|   reporter: [ | ||||
|     ['list'], | ||||
|     [ | ||||
|       'html', | ||||
|       { | ||||
|         open: 'on-failure', | ||||
|         outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840 | ||||
|       } | ||||
|     ] | ||||
|   ] | ||||
| }; | ||||
|  | ||||
| module.exports = config; | ||||
|   | ||||
| @@ -6,38 +6,38 @@ const CI = process.env.CI === 'true'; | ||||
|  | ||||
| /** @type {import('@playwright/test').PlaywrightTestConfig} */ | ||||
| const config = { | ||||
|     retries: 1, //Only for debugging purposes for trace: 'on-first-retry' | ||||
|     testDir: 'tests/performance/', | ||||
|     timeout: 60 * 1000, | ||||
|     workers: 1, //Only run in serial with 1 worker | ||||
|     webServer: { | ||||
|         command: 'npm run start', //coverage not generated | ||||
|         url: 'http://localhost:8080/#', | ||||
|         timeout: 200 * 1000, | ||||
|         reuseExistingServer: !CI | ||||
|     }, | ||||
|     use: { | ||||
|         browserName: "chromium", | ||||
|         baseURL: 'http://localhost:8080/', | ||||
|         headless: CI, //Only if running locally | ||||
|         ignoreHTTPSErrors: true, | ||||
|         screenshot: 'off', | ||||
|         trace: 'on-first-retry', | ||||
|         video: 'off' | ||||
|     }, | ||||
|     projects: [ | ||||
|         { | ||||
|             name: 'chrome', | ||||
|             use: { | ||||
|                 browserName: 'chromium' | ||||
|             } | ||||
|         } | ||||
|     ], | ||||
|     reporter: [ | ||||
|         ['list'], | ||||
|         ['junit', { outputFile: '../test-results/results.xml' }], | ||||
|         ['json', { outputFile: '../test-results/results.json' }] | ||||
|     ] | ||||
|   retries: 1, //Only for debugging purposes for trace: 'on-first-retry' | ||||
|   testDir: 'tests/performance/', | ||||
|   timeout: 60 * 1000, | ||||
|   workers: 1, //Only run in serial with 1 worker | ||||
|   webServer: { | ||||
|     command: 'npm run start', //coverage not generated | ||||
|     url: 'http://localhost:8080/#', | ||||
|     timeout: 200 * 1000, | ||||
|     reuseExistingServer: !CI | ||||
|   }, | ||||
|   use: { | ||||
|     browserName: 'chromium', | ||||
|     baseURL: 'http://localhost:8080/', | ||||
|     headless: CI, //Only if running locally | ||||
|     ignoreHTTPSErrors: true, | ||||
|     screenshot: 'off', | ||||
|     trace: 'on-first-retry', | ||||
|     video: 'off' | ||||
|   }, | ||||
|   projects: [ | ||||
|     { | ||||
|       name: 'chrome', | ||||
|       use: { | ||||
|         browserName: 'chromium' | ||||
|       } | ||||
|     } | ||||
|   ], | ||||
|   reporter: [ | ||||
|     ['list'], | ||||
|     ['junit', { outputFile: '../test-results/results.xml' }], | ||||
|     ['json', { outputFile: '../test-results/results.json' }] | ||||
|   ] | ||||
| }; | ||||
|  | ||||
| module.exports = config; | ||||
|   | ||||
| @@ -4,48 +4,51 @@ | ||||
|  | ||||
| /** @type {import('@playwright/test').PlaywrightTestConfig<{ theme: string }>} */ | ||||
| const config = { | ||||
|     retries: 0, // Visual tests should never retry due to snapshot comparison errors. Leaving as a shim | ||||
|     testDir: 'tests/visual', | ||||
|     testMatch: '**/*.visual.spec.js', // only run visual tests | ||||
|     timeout: 60 * 1000, | ||||
|     workers: 1, //Lower stress on Circle CI Agent for Visual tests https://github.com/percy/cli/discussions/1067 | ||||
|     webServer: { | ||||
|         command: 'npm run start:coverage', | ||||
|         url: 'http://localhost:8080/#', | ||||
|         timeout: 200 * 1000, | ||||
|         reuseExistingServer: !process.env.CI | ||||
|   retries: 0, // Visual tests should never retry due to snapshot comparison errors. Leaving as a shim | ||||
|   testDir: 'tests/visual', | ||||
|   testMatch: '**/*.visual.spec.js', // only run visual tests | ||||
|   timeout: 60 * 1000, | ||||
|   workers: 1, //Lower stress on Circle CI Agent for Visual tests https://github.com/percy/cli/discussions/1067 | ||||
|   webServer: { | ||||
|     command: 'npm run start:coverage', | ||||
|     url: 'http://localhost:8080/#', | ||||
|     timeout: 200 * 1000, | ||||
|     reuseExistingServer: !process.env.CI | ||||
|   }, | ||||
|   use: { | ||||
|     baseURL: 'http://localhost:8080/', | ||||
|     headless: true, // this needs to remain headless to avoid visual changes due to GPU rendering in headed browsers | ||||
|     ignoreHTTPSErrors: true, | ||||
|     screenshot: 'only-on-failure', | ||||
|     trace: 'on-first-retry', | ||||
|     video: 'off' | ||||
|   }, | ||||
|   projects: [ | ||||
|     { | ||||
|       name: 'chrome', | ||||
|       use: { | ||||
|         browserName: 'chromium' | ||||
|       } | ||||
|     }, | ||||
|     use: { | ||||
|         baseURL: 'http://localhost:8080/', | ||||
|         headless: true, // this needs to remain headless to avoid visual changes due to GPU rendering in headed browsers | ||||
|         ignoreHTTPSErrors: true, | ||||
|         screenshot: 'only-on-failure', | ||||
|         trace: 'on-first-retry', | ||||
|         video: 'off' | ||||
|     }, | ||||
|     projects: [ | ||||
|         { | ||||
|             name: 'chrome', | ||||
|             use: { | ||||
|                 browserName: 'chromium' | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|             name: 'chrome-snow-theme', //Runs the same visual tests but with snow-theme enabled | ||||
|             use: { | ||||
|                 browserName: 'chromium', | ||||
|                 theme: 'snow' | ||||
|             } | ||||
|         } | ||||
|     ], | ||||
|     reporter: [ | ||||
|         ['list'], | ||||
|         ['junit', { outputFile: '../test-results/results.xml' }], | ||||
|         ['html', { | ||||
|             open: 'on-failure', | ||||
|             outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840 | ||||
|         }] | ||||
|     { | ||||
|       name: 'chrome-snow-theme', //Runs the same visual tests but with snow-theme enabled | ||||
|       use: { | ||||
|         browserName: 'chromium', | ||||
|         theme: 'snow' | ||||
|       } | ||||
|     } | ||||
|   ], | ||||
|   reporter: [ | ||||
|     ['list'], | ||||
|     ['junit', { outputFile: '../test-results/results.xml' }], | ||||
|     [ | ||||
|       'html', | ||||
|       { | ||||
|         open: 'on-failure', | ||||
|         outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840 | ||||
|       } | ||||
|     ] | ||||
|   ] | ||||
| }; | ||||
|  | ||||
| module.exports = config; | ||||
|   | ||||
| @@ -120,34 +120,31 @@ const theme = 'espresso'; | ||||
|  * | ||||
|  * @type {string} | ||||
|  */ | ||||
| const myItemsFolderName = "My Items"; | ||||
| const myItemsFolderName = 'My Items'; | ||||
|  | ||||
| exports.test = test.extend({ | ||||
|     // This should follow in the Project's configuration. Can be set to 'snow' in playwright config.js | ||||
|     theme: [theme, { option: true }], | ||||
|     // eslint-disable-next-line no-shadow | ||||
|     page: async ({ page, theme }, use, testInfo) => { | ||||
|         // eslint-disable-next-line playwright/no-conditional-in-test | ||||
|         if (theme === 'snow') { | ||||
|             //inject snow theme | ||||
|             await page.addInitScript({ path: path.join(__dirname, './helper', './useSnowTheme.js') }); | ||||
|         } | ||||
|  | ||||
|         // Attach info about the currently running test and its project. | ||||
|         // This will be used by appActions to fill in the created | ||||
|         // domain object's notes. | ||||
|         page.testNotes = [ | ||||
|             `${testInfo.titlePath.join('\n')}`, | ||||
|             `${testInfo.project.name}` | ||||
|         ].join('\n'); | ||||
|  | ||||
|         await use(page); | ||||
|     }, | ||||
|     myItemsFolderName: [myItemsFolderName, { option: true }], | ||||
|     // eslint-disable-next-line no-shadow | ||||
|     openmctConfig: async ({ myItemsFolderName }, use) => { | ||||
|         await use({ myItemsFolderName }); | ||||
|   // This should follow in the Project's configuration. Can be set to 'snow' in playwright config.js | ||||
|   theme: [theme, { option: true }], | ||||
|   // eslint-disable-next-line no-shadow | ||||
|   page: async ({ page, theme }, use, testInfo) => { | ||||
|     // eslint-disable-next-line playwright/no-conditional-in-test | ||||
|     if (theme === 'snow') { | ||||
|       //inject snow theme | ||||
|       await page.addInitScript({ path: path.join(__dirname, './helper', './useSnowTheme.js') }); | ||||
|     } | ||||
|  | ||||
|     // Attach info about the currently running test and its project. | ||||
|     // This will be used by appActions to fill in the created | ||||
|     // domain object's notes. | ||||
|     page.testNotes = [`${testInfo.titlePath.join('\n')}`, `${testInfo.project.name}`].join('\n'); | ||||
|  | ||||
|     await use(page); | ||||
|   }, | ||||
|   myItemsFolderName: [myItemsFolderName, { option: true }], | ||||
|   // eslint-disable-next-line no-shadow | ||||
|   openmctConfig: async ({ myItemsFolderName }, use) => { | ||||
|     await use({ myItemsFolderName }); | ||||
|   } | ||||
| }); | ||||
| exports.expect = expect; | ||||
|  | ||||
| @@ -157,10 +154,10 @@ exports.expect = expect; | ||||
|  * @return {Promise<String>} the stringified stream | ||||
|  */ | ||||
| exports.streamToString = async function (readable) { | ||||
|     let result = ''; | ||||
|     for await (const chunk of readable) { | ||||
|         result += chunk; | ||||
|     } | ||||
|   let result = ''; | ||||
|   for await (const chunk of readable) { | ||||
|     result += chunk; | ||||
|   } | ||||
|  | ||||
|     return result; | ||||
|   return result; | ||||
| }; | ||||
|   | ||||
| @@ -274,10 +274,7 @@ | ||||
|             "id": "ac0d7eb1-b485-458f-bd2a-a63aa87a3a8a" | ||||
|           } | ||||
|         ], | ||||
|         "layoutGrid": [ | ||||
|           10, | ||||
|           10 | ||||
|         ], | ||||
|         "layoutGrid": [10, 10], | ||||
|         "objectStyles": { | ||||
|           "ed63cc29-80e2-4e2b-a472-3d6d4adbf310": { | ||||
|             "staticStyle": { | ||||
| @@ -1455,9 +1452,7 @@ | ||||
|                   "id": "64e49fe7-5b36-43db-8347-4550b910de4c", | ||||
|                   "telemetry": "any", | ||||
|                   "operation": "greaterThan", | ||||
|                   "input": [ | ||||
|                     "120" | ||||
|                   ], | ||||
|                   "input": ["120"], | ||||
|                   "metadata": "sin" | ||||
|                 } | ||||
|               ] | ||||
| @@ -1475,10 +1470,7 @@ | ||||
|                   "id": "59f1c4bf-5d36-450c-9668-6546955fc066", | ||||
|                   "telemetry": "any", | ||||
|                   "operation": "between", | ||||
|                   "input": [ | ||||
|                     "120", | ||||
|                     "-20" | ||||
|                   ], | ||||
|                   "input": ["120", "-20"], | ||||
|                   "metadata": "sin" | ||||
|                 } | ||||
|               ] | ||||
| @@ -1496,9 +1488,7 @@ | ||||
|                   "id": "6707be12-6a6e-4535-bb97-ab5c86f99934", | ||||
|                   "telemetry": "any", | ||||
|                   "operation": "lessThan", | ||||
|                   "input": [ | ||||
|                     "-20" | ||||
|                   ], | ||||
|                   "input": ["-20"], | ||||
|                   "metadata": "sin" | ||||
|                 } | ||||
|               ] | ||||
| @@ -1550,9 +1540,7 @@ | ||||
|                   "id": "64e49fe7-5b36-43db-8347-4550b910de4c", | ||||
|                   "telemetry": "any", | ||||
|                   "operation": "greaterThan", | ||||
|                   "input": [ | ||||
|                     "120" | ||||
|                   ], | ||||
|                   "input": ["120"], | ||||
|                   "metadata": "sin" | ||||
|                 } | ||||
|               ] | ||||
| @@ -1570,10 +1558,7 @@ | ||||
|                   "id": "59f1c4bf-5d36-450c-9668-6546955fc066", | ||||
|                   "telemetry": "any", | ||||
|                   "operation": "between", | ||||
|                   "input": [ | ||||
|                     "120", | ||||
|                     "-20" | ||||
|                   ], | ||||
|                   "input": ["120", "-20"], | ||||
|                   "metadata": "sin" | ||||
|                 } | ||||
|               ] | ||||
| @@ -1591,9 +1576,7 @@ | ||||
|                   "id": "6707be12-6a6e-4535-bb97-ab5c86f99934", | ||||
|                   "telemetry": "any", | ||||
|                   "operation": "lessThan", | ||||
|                   "input": [ | ||||
|                     "-20" | ||||
|                   ], | ||||
|                   "input": ["-20"], | ||||
|                   "metadata": "sin" | ||||
|                 } | ||||
|               ] | ||||
| @@ -1645,9 +1628,7 @@ | ||||
|                   "id": "64e49fe7-5b36-43db-8347-4550b910de4c", | ||||
|                   "telemetry": "any", | ||||
|                   "operation": "greaterThan", | ||||
|                   "input": [ | ||||
|                     "150" | ||||
|                   ], | ||||
|                   "input": ["150"], | ||||
|                   "metadata": "sin" | ||||
|                 } | ||||
|               ] | ||||
| @@ -1665,10 +1646,7 @@ | ||||
|                   "id": "59f1c4bf-5d36-450c-9668-6546955fc066", | ||||
|                   "telemetry": "any", | ||||
|                   "operation": "between", | ||||
|                   "input": [ | ||||
|                     "50", | ||||
|                     "-50" | ||||
|                   ], | ||||
|                   "input": ["50", "-50"], | ||||
|                   "metadata": "sin" | ||||
|                 } | ||||
|               ] | ||||
| @@ -1720,9 +1698,7 @@ | ||||
|                   "id": "64e49fe7-5b36-43db-8347-4550b910de4c", | ||||
|                   "telemetry": "any", | ||||
|                   "operation": "greaterThan", | ||||
|                   "input": [ | ||||
|                     "150" | ||||
|                   ], | ||||
|                   "input": ["150"], | ||||
|                   "metadata": "sin" | ||||
|                 } | ||||
|               ] | ||||
| @@ -1740,10 +1716,7 @@ | ||||
|                   "id": "59f1c4bf-5d36-450c-9668-6546955fc066", | ||||
|                   "telemetry": "any", | ||||
|                   "operation": "between", | ||||
|                   "input": [ | ||||
|                     "50", | ||||
|                     "-50" | ||||
|                   ], | ||||
|                   "input": ["50", "-50"], | ||||
|                   "metadata": "sin" | ||||
|                 } | ||||
|               ] | ||||
| @@ -2204,4 +2177,4 @@ | ||||
|     } | ||||
|   }, | ||||
|   "rootId": "45b24009-dfed-4023-a30b-d31f5e3a2d87" | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -1 +1,90 @@ | ||||
| {"openmct":{"b3cee102-86dd-4c0a-8eec-4d5d276f8691":{"identifier":{"key":"b3cee102-86dd-4c0a-8eec-4d5d276f8691","namespace":""},"name":"Performance Display Layout","type":"layout","composition":[{"key":"9666e7b4-be0c-47a5-94b8-99accad7155e","namespace":""}],"configuration":{"items":[{"width":32,"height":18,"x":12,"y":9,"identifier":{"key":"9666e7b4-be0c-47a5-94b8-99accad7155e","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"23ca351d-a67d-46aa-a762-290eb742d2f1"}],"layoutGrid":[10,10]},"modified":1654299875432,"location":"mine","persisted":1654299878751},"9666e7b4-be0c-47a5-94b8-99accad7155e":{"identifier":{"key":"9666e7b4-be0c-47a5-94b8-99accad7155e","namespace":""},"name":"Performance Example Imagery","type":"example.imagery","configuration":{"imageLocation":"","imageLoadDelayInMilliSeconds":20000,"imageSamples":[],"layers":[{"source":"dist/imagery/example-imagery-layer-16x9.png","name":"16:9","visible":false},{"source":"dist/imagery/example-imagery-layer-safe.png","name":"Safe","visible":false},{"source":"dist/imagery/example-imagery-layer-scale.png","name":"Scale","visible":false}]},"telemetry":{"values":[{"name":"Name","key":"name"},{"name":"Time","key":"utc","format":"utc","hints":{"domain":2}},{"name":"Local Time","key":"local","format":"local-format","hints":{"domain":1}},{"name":"Image","key":"url","format":"image","hints":{"image":1},"layers":[{"source":"dist/imagery/example-imagery-layer-16x9.png","name":"16:9"},{"source":"dist/imagery/example-imagery-layer-safe.png","name":"Safe"},{"source":"dist/imagery/example-imagery-layer-scale.png","name":"Scale"}]},{"name":"Image Download Name","key":"imageDownloadName","format":"imageDownloadName","hints":{"imageDownloadName":1}}]},"modified":1654299840077,"location":"b3cee102-86dd-4c0a-8eec-4d5d276f8691","persisted":1654299840078}},"rootId":"b3cee102-86dd-4c0a-8eec-4d5d276f8691"} | ||||
| { | ||||
|   "openmct": { | ||||
|     "b3cee102-86dd-4c0a-8eec-4d5d276f8691": { | ||||
|       "identifier": { "key": "b3cee102-86dd-4c0a-8eec-4d5d276f8691", "namespace": "" }, | ||||
|       "name": "Performance Display Layout", | ||||
|       "type": "layout", | ||||
|       "composition": [{ "key": "9666e7b4-be0c-47a5-94b8-99accad7155e", "namespace": "" }], | ||||
|       "configuration": { | ||||
|         "items": [ | ||||
|           { | ||||
|             "width": 32, | ||||
|             "height": 18, | ||||
|             "x": 12, | ||||
|             "y": 9, | ||||
|             "identifier": { "key": "9666e7b4-be0c-47a5-94b8-99accad7155e", "namespace": "" }, | ||||
|             "hasFrame": true, | ||||
|             "fontSize": "default", | ||||
|             "font": "default", | ||||
|             "type": "subobject-view", | ||||
|             "id": "23ca351d-a67d-46aa-a762-290eb742d2f1" | ||||
|           } | ||||
|         ], | ||||
|         "layoutGrid": [10, 10] | ||||
|       }, | ||||
|       "modified": 1654299875432, | ||||
|       "location": "mine", | ||||
|       "persisted": 1654299878751 | ||||
|     }, | ||||
|     "9666e7b4-be0c-47a5-94b8-99accad7155e": { | ||||
|       "identifier": { "key": "9666e7b4-be0c-47a5-94b8-99accad7155e", "namespace": "" }, | ||||
|       "name": "Performance Example Imagery", | ||||
|       "type": "example.imagery", | ||||
|       "configuration": { | ||||
|         "imageLocation": "", | ||||
|         "imageLoadDelayInMilliSeconds": 20000, | ||||
|         "imageSamples": [], | ||||
|         "layers": [ | ||||
|           { | ||||
|             "source": "dist/imagery/example-imagery-layer-16x9.png", | ||||
|             "name": "16:9", | ||||
|             "visible": false | ||||
|           }, | ||||
|           { | ||||
|             "source": "dist/imagery/example-imagery-layer-safe.png", | ||||
|             "name": "Safe", | ||||
|             "visible": false | ||||
|           }, | ||||
|           { | ||||
|             "source": "dist/imagery/example-imagery-layer-scale.png", | ||||
|             "name": "Scale", | ||||
|             "visible": false | ||||
|           } | ||||
|         ] | ||||
|       }, | ||||
|       "telemetry": { | ||||
|         "values": [ | ||||
|           { "name": "Name", "key": "name" }, | ||||
|           { "name": "Time", "key": "utc", "format": "utc", "hints": { "domain": 2 } }, | ||||
|           { | ||||
|             "name": "Local Time", | ||||
|             "key": "local", | ||||
|             "format": "local-format", | ||||
|             "hints": { "domain": 1 } | ||||
|           }, | ||||
|           { | ||||
|             "name": "Image", | ||||
|             "key": "url", | ||||
|             "format": "image", | ||||
|             "hints": { "image": 1 }, | ||||
|             "layers": [ | ||||
|               { "source": "dist/imagery/example-imagery-layer-16x9.png", "name": "16:9" }, | ||||
|               { "source": "dist/imagery/example-imagery-layer-safe.png", "name": "Safe" }, | ||||
|               { "source": "dist/imagery/example-imagery-layer-scale.png", "name": "Scale" } | ||||
|             ] | ||||
|           }, | ||||
|           { | ||||
|             "name": "Image Download Name", | ||||
|             "key": "imageDownloadName", | ||||
|             "format": "imageDownloadName", | ||||
|             "hints": { "imageDownloadName": 1 } | ||||
|           } | ||||
|         ] | ||||
|       }, | ||||
|       "modified": 1654299840077, | ||||
|       "location": "b3cee102-86dd-4c0a-8eec-4d5d276f8691", | ||||
|       "persisted": 1654299840078 | ||||
|     } | ||||
|   }, | ||||
|   "rootId": "b3cee102-86dd-4c0a-8eec-4d5d276f8691" | ||||
| } | ||||
|   | ||||
| @@ -1 +1,96 @@ | ||||
| {"openmct":{"6d2fa9fd-f2aa-461a-a1e1-164ac44bec9d":{"identifier":{"key":"6d2fa9fd-f2aa-461a-a1e1-164ac44bec9d","namespace":""},"name":"Performance Notebook","type":"notebook","configuration":{"defaultSort":"oldest","entries":{"3e31c412-33ba-4757-8ade-e9821f6ba321":{"8c8f6035-631c-45af-8c24-786c60295335":[{"id":"entry-1652815305457","createdOn":1652815305457,"createdBy":"","text":"Existing Entry 1","embeds":[]},{"id":"entry-1652815313465","createdOn":1652815313465,"createdBy":"","text":"Existing Entry 2","embeds":[]},{"id":"entry-1652815399955","createdOn":1652815399955,"createdBy":"","text":"Existing Entry 3","embeds":[]}]}},"imageMigrationVer":"v1","pageTitle":"Page","sections":[{"id":"3e31c412-33ba-4757-8ade-e9821f6ba321","isDefault":false,"isSelected":false,"name":"Section1","pages":[{"id":"8c8f6035-631c-45af-8c24-786c60295335","isDefault":false,"isSelected":false,"name":"Page1","pageTitle":"Page"},{"id":"36555942-c9aa-439c-bbdb-0aaf50db50f5","isDefault":false,"isSelected":false,"name":"Page2","pageTitle":"Page"}],"sectionTitle":"Section"},{"id":"dab0bd1d-2c5a-405c-987f-107123d6189a","isDefault":false,"isSelected":true,"name":"Section2","pages":[{"id":"f625a86a-cb99-4898-8082-80543c8de534","isDefault":false,"isSelected":false,"name":"Page1","pageTitle":"Page"},{"id":"e77ef810-f785-42a7-942e-07e999b79c59","isDefault":false,"isSelected":true,"name":"Page2","pageTitle":"Page"}],"sectionTitle":"Section"}],"sectionTitle":"Section","type":"General","showTime":"0"},"modified":1652815915219,"location":"mine","persisted":1652815915222}},"rootId":"6d2fa9fd-f2aa-461a-a1e1-164ac44bec9d"} | ||||
| { | ||||
|   "openmct": { | ||||
|     "6d2fa9fd-f2aa-461a-a1e1-164ac44bec9d": { | ||||
|       "identifier": { "key": "6d2fa9fd-f2aa-461a-a1e1-164ac44bec9d", "namespace": "" }, | ||||
|       "name": "Performance Notebook", | ||||
|       "type": "notebook", | ||||
|       "configuration": { | ||||
|         "defaultSort": "oldest", | ||||
|         "entries": { | ||||
|           "3e31c412-33ba-4757-8ade-e9821f6ba321": { | ||||
|             "8c8f6035-631c-45af-8c24-786c60295335": [ | ||||
|               { | ||||
|                 "id": "entry-1652815305457", | ||||
|                 "createdOn": 1652815305457, | ||||
|                 "createdBy": "", | ||||
|                 "text": "Existing Entry 1", | ||||
|                 "embeds": [] | ||||
|               }, | ||||
|               { | ||||
|                 "id": "entry-1652815313465", | ||||
|                 "createdOn": 1652815313465, | ||||
|                 "createdBy": "", | ||||
|                 "text": "Existing Entry 2", | ||||
|                 "embeds": [] | ||||
|               }, | ||||
|               { | ||||
|                 "id": "entry-1652815399955", | ||||
|                 "createdOn": 1652815399955, | ||||
|                 "createdBy": "", | ||||
|                 "text": "Existing Entry 3", | ||||
|                 "embeds": [] | ||||
|               } | ||||
|             ] | ||||
|           } | ||||
|         }, | ||||
|         "imageMigrationVer": "v1", | ||||
|         "pageTitle": "Page", | ||||
|         "sections": [ | ||||
|           { | ||||
|             "id": "3e31c412-33ba-4757-8ade-e9821f6ba321", | ||||
|             "isDefault": false, | ||||
|             "isSelected": false, | ||||
|             "name": "Section1", | ||||
|             "pages": [ | ||||
|               { | ||||
|                 "id": "8c8f6035-631c-45af-8c24-786c60295335", | ||||
|                 "isDefault": false, | ||||
|                 "isSelected": false, | ||||
|                 "name": "Page1", | ||||
|                 "pageTitle": "Page" | ||||
|               }, | ||||
|               { | ||||
|                 "id": "36555942-c9aa-439c-bbdb-0aaf50db50f5", | ||||
|                 "isDefault": false, | ||||
|                 "isSelected": false, | ||||
|                 "name": "Page2", | ||||
|                 "pageTitle": "Page" | ||||
|               } | ||||
|             ], | ||||
|             "sectionTitle": "Section" | ||||
|           }, | ||||
|           { | ||||
|             "id": "dab0bd1d-2c5a-405c-987f-107123d6189a", | ||||
|             "isDefault": false, | ||||
|             "isSelected": true, | ||||
|             "name": "Section2", | ||||
|             "pages": [ | ||||
|               { | ||||
|                 "id": "f625a86a-cb99-4898-8082-80543c8de534", | ||||
|                 "isDefault": false, | ||||
|                 "isSelected": false, | ||||
|                 "name": "Page1", | ||||
|                 "pageTitle": "Page" | ||||
|               }, | ||||
|               { | ||||
|                 "id": "e77ef810-f785-42a7-942e-07e999b79c59", | ||||
|                 "isDefault": false, | ||||
|                 "isSelected": true, | ||||
|                 "name": "Page2", | ||||
|                 "pageTitle": "Page" | ||||
|               } | ||||
|             ], | ||||
|             "sectionTitle": "Section" | ||||
|           } | ||||
|         ], | ||||
|         "sectionTitle": "Section", | ||||
|         "type": "General", | ||||
|         "showTime": "0" | ||||
|       }, | ||||
|       "modified": 1652815915219, | ||||
|       "location": "mine", | ||||
|       "persisted": 1652815915222 | ||||
|     } | ||||
|   }, | ||||
|   "rootId": "6d2fa9fd-f2aa-461a-a1e1-164ac44bec9d" | ||||
| } | ||||
|   | ||||
| @@ -19,4 +19,4 @@ | ||||
|       ] | ||||
|     } | ||||
|   ] | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -1077,4 +1077,4 @@ | ||||
|       "textColor": "#ffffff" | ||||
|     } | ||||
|   ] | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -1,44 +1,44 @@ | ||||
| { | ||||
|   "Group 1": [ | ||||
|       { | ||||
|           "name": "Past event 1", | ||||
|           "start": 1660320408000, | ||||
|           "end": 1660343797000, | ||||
|           "type": "Group 1", | ||||
|           "color": "orange", | ||||
|           "textColor": "white" | ||||
|       }, | ||||
|       { | ||||
|           "name": "Past event 2", | ||||
|           "start": 1660406808000, | ||||
|           "end": 1660429160000, | ||||
|           "type": "Group 1", | ||||
|           "color": "orange", | ||||
|           "textColor": "white" | ||||
|       }, | ||||
|       { | ||||
|           "name": "Past event 3", | ||||
|           "start": 1660493208000, | ||||
|           "end": 1660503981000, | ||||
|           "type": "Group 1", | ||||
|           "color": "orange", | ||||
|           "textColor": "white" | ||||
|       }, | ||||
|       { | ||||
|           "name": "Past event 4", | ||||
|           "start": 1660579608000, | ||||
|           "end": 1660624108000, | ||||
|           "type": "Group 1", | ||||
|           "color": "orange", | ||||
|           "textColor": "white" | ||||
|       }, | ||||
|       { | ||||
|           "name": "Past event 5", | ||||
|           "start": 1660666008000, | ||||
|           "end": 1660681529000, | ||||
|           "type": "Group 1", | ||||
|           "color": "orange", | ||||
|           "textColor": "white" | ||||
|       } | ||||
|     { | ||||
|       "name": "Past event 1", | ||||
|       "start": 1660320408000, | ||||
|       "end": 1660343797000, | ||||
|       "type": "Group 1", | ||||
|       "color": "orange", | ||||
|       "textColor": "white" | ||||
|     }, | ||||
|     { | ||||
|       "name": "Past event 2", | ||||
|       "start": 1660406808000, | ||||
|       "end": 1660429160000, | ||||
|       "type": "Group 1", | ||||
|       "color": "orange", | ||||
|       "textColor": "white" | ||||
|     }, | ||||
|     { | ||||
|       "name": "Past event 3", | ||||
|       "start": 1660493208000, | ||||
|       "end": 1660503981000, | ||||
|       "type": "Group 1", | ||||
|       "color": "orange", | ||||
|       "textColor": "white" | ||||
|     }, | ||||
|     { | ||||
|       "name": "Past event 4", | ||||
|       "start": 1660579608000, | ||||
|       "end": 1660624108000, | ||||
|       "type": "Group 1", | ||||
|       "color": "orange", | ||||
|       "textColor": "white" | ||||
|     }, | ||||
|     { | ||||
|       "name": "Past event 5", | ||||
|       "start": 1660666008000, | ||||
|       "end": 1660681529000, | ||||
|       "type": "Group 1", | ||||
|       "color": "orange", | ||||
|       "textColor": "white" | ||||
|     } | ||||
|   ] | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -1,38 +1,38 @@ | ||||
| { | ||||
|   "Group 1": [ | ||||
|       { | ||||
|           "name": "Group 1 event 1", | ||||
|           "start": 1650320408000, | ||||
|           "end": 1660343797000, | ||||
|           "type": "Group 1", | ||||
|           "color": "orange", | ||||
|           "textColor": "white" | ||||
|       }, | ||||
|       { | ||||
|           "name": "Group 1 event 2", | ||||
|           "start": 1660005808000, | ||||
|           "end": 1660429160000, | ||||
|           "type": "Group 1", | ||||
|           "color": "yellow", | ||||
|           "textColor": "white" | ||||
|       } | ||||
|     { | ||||
|       "name": "Group 1 event 1", | ||||
|       "start": 1650320408000, | ||||
|       "end": 1660343797000, | ||||
|       "type": "Group 1", | ||||
|       "color": "orange", | ||||
|       "textColor": "white" | ||||
|     }, | ||||
|     { | ||||
|       "name": "Group 1 event 2", | ||||
|       "start": 1660005808000, | ||||
|       "end": 1660429160000, | ||||
|       "type": "Group 1", | ||||
|       "color": "yellow", | ||||
|       "textColor": "white" | ||||
|     } | ||||
|   ], | ||||
|   "Group 2": [ | ||||
|       { | ||||
|           "name": "Group 2 event 1", | ||||
|           "start": 1660320408000, | ||||
|           "end": 1660420408000, | ||||
|           "type": "Group 2", | ||||
|           "color": "green", | ||||
|           "textColor": "white" | ||||
|       }, | ||||
|       { | ||||
|           "name": "Group 2 event 2", | ||||
|           "start": 1660406808000, | ||||
|           "end": 1690429160000, | ||||
|           "type": "Group 2", | ||||
|           "color": "blue", | ||||
|           "textColor": "white" | ||||
|       } | ||||
|     { | ||||
|       "name": "Group 2 event 1", | ||||
|       "start": 1660320408000, | ||||
|       "end": 1660420408000, | ||||
|       "type": "Group 2", | ||||
|       "color": "green", | ||||
|       "textColor": "white" | ||||
|     }, | ||||
|     { | ||||
|       "name": "Group 2 event 2", | ||||
|       "start": 1660406808000, | ||||
|       "end": 1690429160000, | ||||
|       "type": "Group 2", | ||||
|       "color": "blue", | ||||
|       "textColor": "white" | ||||
|     } | ||||
|   ] | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -19,4 +19,4 @@ | ||||
|       ] | ||||
|     } | ||||
|   ] | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -21,145 +21,149 @@ | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| const { test, expect } = require('../../pluginFixtures.js'); | ||||
| const { createDomainObjectWithDefaults, createNotification, expandEntireTree } = require('../../appActions.js'); | ||||
| const { | ||||
|   createDomainObjectWithDefaults, | ||||
|   createNotification, | ||||
|   expandEntireTree | ||||
| } = require('../../appActions.js'); | ||||
|  | ||||
| test.describe('AppActions', () => { | ||||
|     test('createDomainObjectsWithDefaults', async ({ page }) => { | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|   test('createDomainObjectsWithDefaults', async ({ page }) => { | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|  | ||||
|         const e2eFolder = await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Folder', | ||||
|             name: 'e2e folder' | ||||
|         }); | ||||
|  | ||||
|         await test.step('Create multiple flat objects in a row', async () => { | ||||
|             const timer1 = await createDomainObjectWithDefaults(page, { | ||||
|                 type: 'Timer', | ||||
|                 name: 'Timer Foo', | ||||
|                 parent: e2eFolder.uuid | ||||
|             }); | ||||
|             const timer2 = await createDomainObjectWithDefaults(page, { | ||||
|                 type: 'Timer', | ||||
|                 name: 'Timer Bar', | ||||
|                 parent: e2eFolder.uuid | ||||
|             }); | ||||
|             const timer3 = await createDomainObjectWithDefaults(page, { | ||||
|                 type: 'Timer', | ||||
|                 name: 'Timer Baz', | ||||
|                 parent: e2eFolder.uuid | ||||
|             }); | ||||
|  | ||||
|             await page.goto(timer1.url); | ||||
|             await expect(page.locator('.l-browse-bar__object-name')).toHaveText(timer1.name); | ||||
|             await page.goto(timer2.url); | ||||
|             await expect(page.locator('.l-browse-bar__object-name')).toHaveText(timer2.name); | ||||
|             await page.goto(timer3.url); | ||||
|             await expect(page.locator('.l-browse-bar__object-name')).toHaveText(timer3.name); | ||||
|         }); | ||||
|  | ||||
|         await test.step('Create multiple nested objects in a row', async () => { | ||||
|             const folder1 = await createDomainObjectWithDefaults(page, { | ||||
|                 type: 'Folder', | ||||
|                 name: 'Folder Foo', | ||||
|                 parent: e2eFolder.uuid | ||||
|             }); | ||||
|             const folder2 = await createDomainObjectWithDefaults(page, { | ||||
|                 type: 'Folder', | ||||
|                 name: 'Folder Bar', | ||||
|                 parent: folder1.uuid | ||||
|             }); | ||||
|             const folder3 = await createDomainObjectWithDefaults(page, { | ||||
|                 type: 'Folder', | ||||
|                 name: 'Folder Baz', | ||||
|                 parent: folder2.uuid | ||||
|             }); | ||||
|             await page.goto(folder1.url); | ||||
|             await expect(page.locator('.l-browse-bar__object-name')).toHaveText(folder1.name); | ||||
|             await page.goto(folder2.url); | ||||
|             await expect(page.locator('.l-browse-bar__object-name')).toHaveText(folder2.name); | ||||
|             await page.goto(folder3.url); | ||||
|             await expect(page.locator('.l-browse-bar__object-name')).toHaveText(folder3.name); | ||||
|  | ||||
|             expect(folder1.url).toBe(`${e2eFolder.url}/${folder1.uuid}`); | ||||
|             expect(folder2.url).toBe(`${e2eFolder.url}/${folder1.uuid}/${folder2.uuid}`); | ||||
|             expect(folder3.url).toBe(`${e2eFolder.url}/${folder1.uuid}/${folder2.uuid}/${folder3.uuid}`); | ||||
|         }); | ||||
|     const e2eFolder = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Folder', | ||||
|       name: 'e2e folder' | ||||
|     }); | ||||
|     test("createNotification", async ({ page }) => { | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|         await createNotification(page, { | ||||
|             message: 'Test info notification', | ||||
|             severity: 'info' | ||||
|         }); | ||||
|         await expect(page.locator('.c-message-banner__message')).toHaveText('Test info notification'); | ||||
|         await expect(page.locator('.c-message-banner')).toHaveClass(/info/); | ||||
|         await page.locator('[aria-label="Dismiss"]').click(); | ||||
|         await createNotification(page, { | ||||
|             message: 'Test alert notification', | ||||
|             severity: 'alert' | ||||
|         }); | ||||
|         await expect(page.locator('.c-message-banner__message')).toHaveText('Test alert notification'); | ||||
|         await expect(page.locator('.c-message-banner')).toHaveClass(/alert/); | ||||
|         await page.locator('[aria-label="Dismiss"]').click(); | ||||
|         await createNotification(page, { | ||||
|             message: 'Test error notification', | ||||
|             severity: 'error' | ||||
|         }); | ||||
|         await expect(page.locator('.c-message-banner__message')).toHaveText('Test error notification'); | ||||
|         await expect(page.locator('.c-message-banner')).toHaveClass(/error/); | ||||
|         await page.locator('[aria-label="Dismiss"]').click(); | ||||
|  | ||||
|     await test.step('Create multiple flat objects in a row', async () => { | ||||
|       const timer1 = await createDomainObjectWithDefaults(page, { | ||||
|         type: 'Timer', | ||||
|         name: 'Timer Foo', | ||||
|         parent: e2eFolder.uuid | ||||
|       }); | ||||
|       const timer2 = await createDomainObjectWithDefaults(page, { | ||||
|         type: 'Timer', | ||||
|         name: 'Timer Bar', | ||||
|         parent: e2eFolder.uuid | ||||
|       }); | ||||
|       const timer3 = await createDomainObjectWithDefaults(page, { | ||||
|         type: 'Timer', | ||||
|         name: 'Timer Baz', | ||||
|         parent: e2eFolder.uuid | ||||
|       }); | ||||
|  | ||||
|       await page.goto(timer1.url); | ||||
|       await expect(page.locator('.l-browse-bar__object-name')).toHaveText(timer1.name); | ||||
|       await page.goto(timer2.url); | ||||
|       await expect(page.locator('.l-browse-bar__object-name')).toHaveText(timer2.name); | ||||
|       await page.goto(timer3.url); | ||||
|       await expect(page.locator('.l-browse-bar__object-name')).toHaveText(timer3.name); | ||||
|     }); | ||||
|     test('expandEntireTree', async ({ page }) => { | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|  | ||||
|         const rootFolder = await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Folder' | ||||
|         }); | ||||
|         const folder1 = await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Folder', | ||||
|             parent: rootFolder.uuid | ||||
|         }); | ||||
|     await test.step('Create multiple nested objects in a row', async () => { | ||||
|       const folder1 = await createDomainObjectWithDefaults(page, { | ||||
|         type: 'Folder', | ||||
|         name: 'Folder Foo', | ||||
|         parent: e2eFolder.uuid | ||||
|       }); | ||||
|       const folder2 = await createDomainObjectWithDefaults(page, { | ||||
|         type: 'Folder', | ||||
|         name: 'Folder Bar', | ||||
|         parent: folder1.uuid | ||||
|       }); | ||||
|       const folder3 = await createDomainObjectWithDefaults(page, { | ||||
|         type: 'Folder', | ||||
|         name: 'Folder Baz', | ||||
|         parent: folder2.uuid | ||||
|       }); | ||||
|       await page.goto(folder1.url); | ||||
|       await expect(page.locator('.l-browse-bar__object-name')).toHaveText(folder1.name); | ||||
|       await page.goto(folder2.url); | ||||
|       await expect(page.locator('.l-browse-bar__object-name')).toHaveText(folder2.name); | ||||
|       await page.goto(folder3.url); | ||||
|       await expect(page.locator('.l-browse-bar__object-name')).toHaveText(folder3.name); | ||||
|  | ||||
|         await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Clock', | ||||
|             parent: folder1.uuid | ||||
|         }); | ||||
|         const folder2 = await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Folder', | ||||
|             parent: folder1.uuid | ||||
|         }); | ||||
|         await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Folder', | ||||
|             parent: folder1.uuid | ||||
|         }); | ||||
|         await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Display Layout', | ||||
|             parent: folder2.uuid | ||||
|         }); | ||||
|         await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Folder', | ||||
|             parent: folder2.uuid | ||||
|         }); | ||||
|  | ||||
|         await page.goto('./#/browse/mine'); | ||||
|         await expandEntireTree(page); | ||||
|         const treePane = page.getByRole('tree', { | ||||
|             name: "Main Tree" | ||||
|         }); | ||||
|         const treePaneCollapsedItems = treePane.getByRole('treeitem', { expanded: false }); | ||||
|         expect(await treePaneCollapsedItems.count()).toBe(0); | ||||
|  | ||||
|         await page.goto('./#/browse/mine'); | ||||
|         //Click the Create button | ||||
|         await page.click('button:has-text("Create")'); | ||||
|  | ||||
|         // Click the object specified by 'type' | ||||
|         await page.click(`li[role='menuitem']:text("Clock")`); | ||||
|         await expandEntireTree(page, "Create Modal Tree"); | ||||
|         const locatorTree = page.getByRole("tree", { | ||||
|             name: "Create Modal Tree" | ||||
|         }); | ||||
|         const locatorTreeCollapsedItems = locatorTree.locator('role=treeitem[expanded=false]'); | ||||
|         expect(await locatorTreeCollapsedItems.count()).toBe(0); | ||||
|       expect(folder1.url).toBe(`${e2eFolder.url}/${folder1.uuid}`); | ||||
|       expect(folder2.url).toBe(`${e2eFolder.url}/${folder1.uuid}/${folder2.uuid}`); | ||||
|       expect(folder3.url).toBe(`${e2eFolder.url}/${folder1.uuid}/${folder2.uuid}/${folder3.uuid}`); | ||||
|     }); | ||||
|   }); | ||||
|   test('createNotification', async ({ page }) => { | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|     await createNotification(page, { | ||||
|       message: 'Test info notification', | ||||
|       severity: 'info' | ||||
|     }); | ||||
|     await expect(page.locator('.c-message-banner__message')).toHaveText('Test info notification'); | ||||
|     await expect(page.locator('.c-message-banner')).toHaveClass(/info/); | ||||
|     await page.locator('[aria-label="Dismiss"]').click(); | ||||
|     await createNotification(page, { | ||||
|       message: 'Test alert notification', | ||||
|       severity: 'alert' | ||||
|     }); | ||||
|     await expect(page.locator('.c-message-banner__message')).toHaveText('Test alert notification'); | ||||
|     await expect(page.locator('.c-message-banner')).toHaveClass(/alert/); | ||||
|     await page.locator('[aria-label="Dismiss"]').click(); | ||||
|     await createNotification(page, { | ||||
|       message: 'Test error notification', | ||||
|       severity: 'error' | ||||
|     }); | ||||
|     await expect(page.locator('.c-message-banner__message')).toHaveText('Test error notification'); | ||||
|     await expect(page.locator('.c-message-banner')).toHaveClass(/error/); | ||||
|     await page.locator('[aria-label="Dismiss"]').click(); | ||||
|   }); | ||||
|   test('expandEntireTree', async ({ page }) => { | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|  | ||||
|     const rootFolder = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Folder' | ||||
|     }); | ||||
|     const folder1 = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Folder', | ||||
|       parent: rootFolder.uuid | ||||
|     }); | ||||
|  | ||||
|     await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Clock', | ||||
|       parent: folder1.uuid | ||||
|     }); | ||||
|     const folder2 = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Folder', | ||||
|       parent: folder1.uuid | ||||
|     }); | ||||
|     await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Folder', | ||||
|       parent: folder1.uuid | ||||
|     }); | ||||
|     await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Display Layout', | ||||
|       parent: folder2.uuid | ||||
|     }); | ||||
|     await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Folder', | ||||
|       parent: folder2.uuid | ||||
|     }); | ||||
|  | ||||
|     await page.goto('./#/browse/mine'); | ||||
|     await expandEntireTree(page); | ||||
|     const treePane = page.getByRole('tree', { | ||||
|       name: 'Main Tree' | ||||
|     }); | ||||
|     const treePaneCollapsedItems = treePane.getByRole('treeitem', { expanded: false }); | ||||
|     expect(await treePaneCollapsedItems.count()).toBe(0); | ||||
|  | ||||
|     await page.goto('./#/browse/mine'); | ||||
|     //Click the Create button | ||||
|     await page.click('button:has-text("Create")'); | ||||
|  | ||||
|     // Click the object specified by 'type' | ||||
|     await page.click(`li[role='menuitem']:text("Clock")`); | ||||
|     await expandEntireTree(page, 'Create Modal Tree'); | ||||
|     const locatorTree = page.getByRole('tree', { | ||||
|       name: 'Create Modal Tree' | ||||
|     }); | ||||
|     const locatorTreeCollapsedItems = locatorTree.locator('role=treeitem[expanded=false]'); | ||||
|     expect(await locatorTreeCollapsedItems.count()).toBe(0); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -29,27 +29,25 @@ relates to how we've extended it (i.e. ./e2e/baseFixtures.js) and assumptions ma | ||||
| const { test } = require('../../baseFixtures.js'); | ||||
|  | ||||
| test.describe('baseFixtures tests', () => { | ||||
|     test('Verify that tests fail if console.error is thrown', async ({ page }) => { | ||||
|         test.fail(); | ||||
|         //Go to baseURL | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|   test('Verify that tests fail if console.error is thrown', async ({ page }) => { | ||||
|     test.fail(); | ||||
|     //Go to baseURL | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|  | ||||
|         //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! | ||||
|         ]); | ||||
|     //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: 'domcontentloaded' }); | ||||
|  | ||||
|     }); | ||||
|     test('Verify that tests pass if console.warn is thrown', async ({ page }) => { | ||||
|         //Go to baseURL | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|  | ||||
|         //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! | ||||
|         ]); | ||||
|  | ||||
|     }); | ||||
|     //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! | ||||
|     ]); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -21,28 +21,28 @@ | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* | ||||
| * This test suite template is to be used when creating new test suites. It will be kept up to date with the latest improvements | ||||
| * made by the Open MCT team. It will also follow our best pratices as those evolve. Please use this structure as a _reference_ and clear | ||||
| * or update any references when creating a new test suite! | ||||
| * | ||||
| * To illustrate current best practices, we've included a mocked up test suite for Renaming a Timer domain object. | ||||
| * | ||||
| * Demonstrated: | ||||
| * - Using appActions to leverage existing functions | ||||
| * - Structure | ||||
| * - @unstable annotation | ||||
| * - await, expect, test, describe syntax | ||||
| * - Writing a custom function for a test suite | ||||
| * - Test stub for unfinished test coverage (test.fixme) | ||||
| * | ||||
| * The structure should follow | ||||
| * 1. imports | ||||
| * 2. test.describe() | ||||
| * 3. -> test1 | ||||
| *    -> test2 | ||||
| *    -> test3(stub) | ||||
| * 4. Any custom functions | ||||
| */ | ||||
|  * This test suite template is to be used when creating new test suites. It will be kept up to date with the latest improvements | ||||
|  * made by the Open MCT team. It will also follow our best pratices as those evolve. Please use this structure as a _reference_ and clear | ||||
|  * or update any references when creating a new test suite! | ||||
|  * | ||||
|  * To illustrate current best practices, we've included a mocked up test suite for Renaming a Timer domain object. | ||||
|  * | ||||
|  * Demonstrated: | ||||
|  * - Using appActions to leverage existing functions | ||||
|  * - Structure | ||||
|  * - @unstable annotation | ||||
|  * - await, expect, test, describe syntax | ||||
|  * - Writing a custom function for a test suite | ||||
|  * - Test stub for unfinished test coverage (test.fixme) | ||||
|  * | ||||
|  * The structure should follow | ||||
|  * 1. imports | ||||
|  * 2. test.describe() | ||||
|  * 3. -> test1 | ||||
|  *    -> test2 | ||||
|  *    -> test3(stub) | ||||
|  * 4. Any custom functions | ||||
|  */ | ||||
|  | ||||
| // Structure: Some standard Imports. Please update the required pathing. | ||||
| const { test, expect } = require('../../pluginFixtures'); | ||||
| @@ -58,63 +58,63 @@ const { createDomainObjectWithDefaults } = require('../../appActions'); | ||||
|  *  as a part of our test promotion pipeline. | ||||
|  */ | ||||
| test.describe('Renaming Timer Object', () => { | ||||
|     // Top-level declaration of the Timer object created in beforeEach(). | ||||
|     // We can then use this throughout the entire test suite. | ||||
|     let timer; | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         // Open a browser, navigate to the main page, and wait until all network events to resolve | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|   // Top-level declaration of the Timer object created in beforeEach(). | ||||
|   // We can then use this throughout the entire test suite. | ||||
|   let timer; | ||||
|   test.beforeEach(async ({ page }) => { | ||||
|     // Open a browser, navigate to the main page, and wait until all network events to resolve | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|  | ||||
|         // We provide some helper functions in appActions like `createDomainObjectWithDefaults()`. | ||||
|         // This example will create a Timer object with default properties, under the root folder: | ||||
|         timer = await createDomainObjectWithDefaults(page, { type: 'Timer' }); | ||||
|     // We provide some helper functions in appActions like `createDomainObjectWithDefaults()`. | ||||
|     // This example will create a Timer object with default properties, under the root folder: | ||||
|     timer = await createDomainObjectWithDefaults(page, { type: 'Timer' }); | ||||
|  | ||||
|         // Assert the object to be created and check its name in the title | ||||
|         await expect(page.locator('.l-browse-bar__object-name')).toContainText(timer.name); | ||||
|     }); | ||||
|     // Assert the object to be created and check its name in the title | ||||
|     await expect(page.locator('.l-browse-bar__object-name')).toContainText(timer.name); | ||||
|   }); | ||||
|  | ||||
|     /** | ||||
|      * Make sure to use testcase names which are descriptive and easy to understand. | ||||
|      * A good testcase name concisely describes the test's goal(s) and should give | ||||
|      * some hint as to what went wrong if the test fails. | ||||
|      */ | ||||
|     test('An existing Timer object can be renamed via the 3dot actions menu', async ({ page }) => { | ||||
|         const newObjectName = "Renamed Timer"; | ||||
|   /** | ||||
|    * Make sure to use testcase names which are descriptive and easy to understand. | ||||
|    * A good testcase name concisely describes the test's goal(s) and should give | ||||
|    * some hint as to what went wrong if the test fails. | ||||
|    */ | ||||
|   test('An existing Timer object can be renamed via the 3dot actions menu', async ({ page }) => { | ||||
|     const newObjectName = 'Renamed Timer'; | ||||
|  | ||||
|         // We've created an example of a shared function which pases the page and newObjectName values | ||||
|         await renameTimerFrom3DotMenu(page, timer.url, newObjectName); | ||||
|     // We've created an example of a shared function which pases the page and newObjectName values | ||||
|     await renameTimerFrom3DotMenu(page, timer.url, newObjectName); | ||||
|  | ||||
|         // Assert that the name has changed in the browser bar to the value we assigned above | ||||
|         await expect(page.locator('.l-browse-bar__object-name')).toContainText(newObjectName); | ||||
|     }); | ||||
|     // Assert that the name has changed in the browser bar to the value we assigned above | ||||
|     await expect(page.locator('.l-browse-bar__object-name')).toContainText(newObjectName); | ||||
|   }); | ||||
|  | ||||
|     test('An existing Timer object can be renamed twice', async ({ page }) => { | ||||
|         const newObjectName = "Renamed Timer"; | ||||
|         const newObjectName2 = "Re-Renamed Timer"; | ||||
|   test('An existing Timer object can be renamed twice', async ({ page }) => { | ||||
|     const newObjectName = 'Renamed Timer'; | ||||
|     const newObjectName2 = 'Re-Renamed Timer'; | ||||
|  | ||||
|         await renameTimerFrom3DotMenu(page, timer.url, newObjectName); | ||||
|     await renameTimerFrom3DotMenu(page, timer.url, newObjectName); | ||||
|  | ||||
|         // Assert that the name has changed in the browser bar to the value we assigned above | ||||
|         await expect(page.locator('.l-browse-bar__object-name')).toContainText(newObjectName); | ||||
|     // Assert that the name has changed in the browser bar to the value we assigned above | ||||
|     await expect(page.locator('.l-browse-bar__object-name')).toContainText(newObjectName); | ||||
|  | ||||
|         // Rename the Timer object again | ||||
|         await renameTimerFrom3DotMenu(page, timer.url, newObjectName2); | ||||
|     // Rename the Timer object again | ||||
|     await renameTimerFrom3DotMenu(page, timer.url, newObjectName2); | ||||
|  | ||||
|         // Assert that the name has changed in the browser bar to the second value | ||||
|         await expect(page.locator('.l-browse-bar__object-name')).toContainText(newObjectName2); | ||||
|     }); | ||||
|     // Assert that the name has changed in the browser bar to the second value | ||||
|     await expect(page.locator('.l-browse-bar__object-name')).toContainText(newObjectName2); | ||||
|   }); | ||||
|  | ||||
|     /** | ||||
|      * If you run out of time to write new tests, please stub in the missing tests | ||||
|      * in-place with a test.fixme and BDD-style test steps. | ||||
|      * Someone will carry the baton! | ||||
|      */ | ||||
|     test.fixme('Can Rename Timer Object from Tree', async ({ page }) => { | ||||
|         //Create a new object | ||||
|         //Copy this object | ||||
|         //Delete first object | ||||
|         //Expect copied object to persist | ||||
|     }); | ||||
|   /** | ||||
|    * If you run out of time to write new tests, please stub in the missing tests | ||||
|    * in-place with a test.fixme and BDD-style test steps. | ||||
|    * Someone will carry the baton! | ||||
|    */ | ||||
|   test.fixme('Can Rename Timer Object from Tree', async ({ page }) => { | ||||
|     //Create a new object | ||||
|     //Copy this object | ||||
|     //Delete first object | ||||
|     //Expect copied object to persist | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| /** | ||||
| @@ -131,18 +131,18 @@ test.describe('Renaming Timer Object', () => { | ||||
|  * @param {string} newNameForTimer New name for object | ||||
|  */ | ||||
| async function renameTimerFrom3DotMenu(page, timerUrl, newNameForTimer) { | ||||
|     // Navigate to the timer object | ||||
|     await page.goto(timerUrl); | ||||
|   // Navigate to the timer object | ||||
|   await page.goto(timerUrl); | ||||
|  | ||||
|     // Click on 3 Dot Menu | ||||
|     await page.locator('button[title="More options"]').click(); | ||||
|   // Click on 3 Dot Menu | ||||
|   await page.locator('button[title="More options"]').click(); | ||||
|  | ||||
|     // Click text=Edit Properties... | ||||
|     await page.locator('text=Edit Properties...').click(); | ||||
|   // Click text=Edit Properties... | ||||
|   await page.locator('text=Edit Properties...').click(); | ||||
|  | ||||
|     // Rename the timer object | ||||
|     await page.locator('text=Properties Title Notes >> input[type="text"]').fill(newNameForTimer); | ||||
|   // Rename the timer object | ||||
|   await page.locator('text=Properties Title Notes >> input[type="text"]').fill(newNameForTimer); | ||||
|  | ||||
|     // Click Ok button to Save | ||||
|     await page.locator('button:has-text("OK")').click(); | ||||
|   // Click Ok button to Save | ||||
|   await page.locator('button:has-text("OK")').click(); | ||||
| } | ||||
|   | ||||
| @@ -35,30 +35,30 @@ const { createDomainObjectWithDefaults } = require('../../appActions.js'); | ||||
| const { test, expect } = require('../../pluginFixtures.js'); | ||||
|  | ||||
| test('Generate Visual Test Data @localStorage', async ({ page, context }) => { | ||||
|     //Go to baseURL | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|     const overlayPlot = await createDomainObjectWithDefaults(page, { type: 'Overlay Plot' }); | ||||
|   //Go to baseURL | ||||
|   await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|   const overlayPlot = await createDomainObjectWithDefaults(page, { type: 'Overlay Plot' }); | ||||
|  | ||||
|     // click create button | ||||
|     await page.locator('button:has-text("Create")').click(); | ||||
|   // click create button | ||||
|   await page.locator('button:has-text("Create")').click(); | ||||
|  | ||||
|     // add sine wave generator with defaults | ||||
|     await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click(); | ||||
|   // add sine wave generator with defaults | ||||
|   await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click(); | ||||
|  | ||||
|     //Add a 5000 ms Delay | ||||
|     await page.locator('[aria-label="Loading Delay \\(ms\\)"]').fill('5000'); | ||||
|   //Add a 5000 ms Delay | ||||
|   await page.locator('[aria-label="Loading Delay \\(ms\\)"]').fill('5000'); | ||||
|  | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('button:has-text("OK")').click(), | ||||
|         //Wait for Save Banner to appear | ||||
|         page.waitForSelector('.c-message-banner__message') | ||||
|     ]); | ||||
|   await Promise.all([ | ||||
|     page.waitForNavigation(), | ||||
|     page.locator('button:has-text("OK")').click(), | ||||
|     //Wait for Save Banner to appear | ||||
|     page.waitForSelector('.c-message-banner__message') | ||||
|   ]); | ||||
|  | ||||
|     // focus the overlay plot | ||||
|     await page.goto(overlayPlot.url); | ||||
|   // focus the overlay plot | ||||
|   await page.goto(overlayPlot.url); | ||||
|  | ||||
|     await expect(page.locator('.l-browse-bar__object-name')).toContainText(overlayPlot.name); | ||||
|     //Save localStorage for future test execution | ||||
|     await context.storageState({ path: './e2e/test-data/VisualTestData_storage.json' }); | ||||
|   await expect(page.locator('.l-browse-bar__object-name')).toContainText(overlayPlot.name); | ||||
|   //Save localStorage for future test execution | ||||
|   await context.storageState({ path: './e2e/test-data/VisualTestData_storage.json' }); | ||||
| }); | ||||
|   | ||||
| @@ -29,18 +29,16 @@ const { test } = require('../../pluginFixtures.js'); | ||||
|  | ||||
| // eslint-disable-next-line playwright/no-skipped-test | ||||
| test.describe.skip('pluginFixtures tests', () => { | ||||
|     // test.use({ domainObjectName: 'Timer' }); | ||||
|     // let timerUUID; | ||||
|  | ||||
|     // test('Creates a timer object @framework @unstable', ({ domainObject }) => { | ||||
|     //     const { uuid } = domainObject; | ||||
|     //     const uuidRegexp = /[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/; | ||||
|     //     expect(uuid).toMatch(uuidRegexp); | ||||
|     //     timerUUID = uuid; | ||||
|     // }); | ||||
|  | ||||
|     // test('Provides same uuid for subsequent uses of the same object @framework', ({ domainObject }) => { | ||||
|     //     const { uuid } = domainObject; | ||||
|     //     expect(uuid).toEqual(timerUUID); | ||||
|     // }); | ||||
|   // test.use({ domainObjectName: 'Timer' }); | ||||
|   // let timerUUID; | ||||
|   // test('Creates a timer object @framework @unstable', ({ domainObject }) => { | ||||
|   //     const { uuid } = domainObject; | ||||
|   //     const uuidRegexp = /[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/; | ||||
|   //     expect(uuid).toMatch(uuidRegexp); | ||||
|   //     timerUUID = uuid; | ||||
|   // }); | ||||
|   // test('Provides same uuid for subsequent uses of the same object @framework', ({ domainObject }) => { | ||||
|   //     const { uuid } = domainObject; | ||||
|   //     expect(uuid).toEqual(timerUUID); | ||||
|   // }); | ||||
| }); | ||||
|   | ||||
| @@ -21,16 +21,15 @@ | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* | ||||
| * This test suite template is to be used when verifying Test Data files found in /e2e/test-data/ | ||||
| */ | ||||
|  * This test suite template is to be used when verifying Test Data files found in /e2e/test-data/ | ||||
|  */ | ||||
|  | ||||
| const { test } = require('../../baseFixtures'); | ||||
|  | ||||
| test.describe('recycled_local_storage @localStorage', () => { | ||||
|     //We may want to do some additional level of verification of this file. For now, we just verify that it exists and can be used in a test suite. | ||||
|     test.use({ storageState: './e2e/test-data/recycled_local_storage.json' }); | ||||
|     test('Can use recycled_local_storage file', async ({ page }) => { | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|     }); | ||||
|   //We may want to do some additional level of verification of this file. For now, we just verify that it exists and can be used in a test suite. | ||||
|   test.use({ storageState: './e2e/test-data/recycled_local_storage.json' }); | ||||
|   test('Can use recycled_local_storage file', async ({ page }) => { | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
|   | ||||
| @@ -27,37 +27,39 @@ This test suite is dedicated to tests which verify branding related components. | ||||
| const { test, expect } = require('../../baseFixtures.js'); | ||||
|  | ||||
| test.describe('Branding tests', () => { | ||||
|     test('About Modal launches with basic branding properties', async ({ page }) => { | ||||
|         // Go to baseURL | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|   test('About Modal launches with basic branding properties', async ({ page }) => { | ||||
|     // Go to baseURL | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|  | ||||
|         // Click About button | ||||
|         await page.click('.l-shell__app-logo'); | ||||
|     // Click About button | ||||
|     await page.click('.l-shell__app-logo'); | ||||
|  | ||||
|         // Verify that the NASA Logo Appears | ||||
|         await expect(page.locator('.c-about__image')).toBeVisible(); | ||||
|     // Verify that the NASA Logo Appears | ||||
|     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').first(); | ||||
|         await expect(versionInformationLocator).toBeEnabled(); | ||||
|         await expect.soft(versionInformationLocator).toContainText(/Version: \d/); | ||||
|         await expect.soft(versionInformationLocator).toContainText(/Build Date: ((?:Mon|Tue|Wed|Thu|Fri|Sat|Sun))/); | ||||
|         await expect.soft(versionInformationLocator).toContainText(/Revision: \b[0-9a-f]{5,40}\b/); | ||||
|         await expect.soft(versionInformationLocator).toContainText(/Branch: ./); | ||||
|     }); | ||||
|     test('Verify Links in About Modal @2p', async ({ page }) => { | ||||
|         // Go to baseURL | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|     // Modify the Build information in 'about' Modal | ||||
|     const versionInformationLocator = page.locator('ul.t-info.l-info.s-info').first(); | ||||
|     await expect(versionInformationLocator).toBeEnabled(); | ||||
|     await expect.soft(versionInformationLocator).toContainText(/Version: \d/); | ||||
|     await expect | ||||
|       .soft(versionInformationLocator) | ||||
|       .toContainText(/Build Date: ((?:Mon|Tue|Wed|Thu|Fri|Sat|Sun))/); | ||||
|     await expect.soft(versionInformationLocator).toContainText(/Revision: \b[0-9a-f]{5,40}\b/); | ||||
|     await expect.soft(versionInformationLocator).toContainText(/Branch: ./); | ||||
|   }); | ||||
|   test('Verify Links in About Modal @2p', async ({ page }) => { | ||||
|     // Go to baseURL | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|  | ||||
|         // Click About button | ||||
|         await page.click('.l-shell__app-logo'); | ||||
|     // Click About button | ||||
|     await page.click('.l-shell__app-logo'); | ||||
|  | ||||
|         // Verify that clicking on the third party licenses information opens up another tab on licenses url | ||||
|         const [page2] = await Promise.all([ | ||||
|             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(); | ||||
|     }); | ||||
|     // Verify that clicking on the third party licenses information opens up another tab on licenses url | ||||
|     const [page2] = await Promise.all([ | ||||
|       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(); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -21,91 +21,98 @@ | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* | ||||
| * This test suite is meant to be executed against a couchdb container. More doc to come | ||||
| * | ||||
| */ | ||||
|  * This test suite is meant to be executed against a couchdb container. More doc to come | ||||
|  * | ||||
|  */ | ||||
|  | ||||
| const { test, expect } = require('../../pluginFixtures'); | ||||
|  | ||||
| test.describe("CouchDB Status Indicator with mocked responses @couchdb", () => { | ||||
|     test.use({ failOnConsoleError: false }); | ||||
|     //TODO BeforeAll Verify CouchDB Connectivity with APIContext | ||||
|     test('Shows green if connected', async ({ page }) => { | ||||
|         await page.route('**/openmct/mine', route => { | ||||
|             route.fulfill({ | ||||
|                 status: 200, | ||||
|                 contentType: 'application/json', | ||||
|                 body: JSON.stringify({}) | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         //Go to baseURL | ||||
|         await page.goto('./#/browse/mine?hideTree=true&hideInspector=true', { waitUntil: 'networkidle' }); | ||||
|         await expect(page.locator('div:has-text("CouchDB is connected")').nth(3)).toBeVisible(); | ||||
| test.describe('CouchDB Status Indicator with mocked responses @couchdb', () => { | ||||
|   test.use({ failOnConsoleError: false }); | ||||
|   //TODO BeforeAll Verify CouchDB Connectivity with APIContext | ||||
|   test('Shows green if connected', async ({ page }) => { | ||||
|     await page.route('**/openmct/mine', (route) => { | ||||
|       route.fulfill({ | ||||
|         status: 200, | ||||
|         contentType: 'application/json', | ||||
|         body: JSON.stringify({}) | ||||
|       }); | ||||
|     }); | ||||
|     test('Shows red if not connected', async ({ page }) => { | ||||
|         await page.route('**/openmct/**', route => { | ||||
|             route.fulfill({ | ||||
|                 status: 503, | ||||
|                 contentType: 'application/json', | ||||
|                 body: JSON.stringify({}) | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         //Go to baseURL | ||||
|         await page.goto('./#/browse/mine?hideTree=true&hideInspector=true', { waitUntil: 'networkidle' }); | ||||
|         await expect(page.locator('div:has-text("CouchDB is offline")').nth(3)).toBeVisible(); | ||||
|     //Go to baseURL | ||||
|     await page.goto('./#/browse/mine?hideTree=true&hideInspector=true', { | ||||
|       waitUntil: 'networkidle' | ||||
|     }); | ||||
|     await expect(page.locator('div:has-text("CouchDB is connected")').nth(3)).toBeVisible(); | ||||
|   }); | ||||
|   test('Shows red if not connected', async ({ page }) => { | ||||
|     await page.route('**/openmct/**', (route) => { | ||||
|       route.fulfill({ | ||||
|         status: 503, | ||||
|         contentType: 'application/json', | ||||
|         body: JSON.stringify({}) | ||||
|       }); | ||||
|     }); | ||||
|     test('Shows unknown if it receives an unexpected response code', async ({ page }) => { | ||||
|         await page.route('**/openmct/mine', route => { | ||||
|             route.fulfill({ | ||||
|                 status: 418, | ||||
|                 contentType: 'application/json', | ||||
|                 body: JSON.stringify({}) | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         //Go to baseURL | ||||
|         await page.goto('./#/browse/mine?hideTree=true&hideInspector=true', { waitUntil: 'networkidle' }); | ||||
|         await expect(page.locator('div:has-text("CouchDB connectivity unknown")').nth(3)).toBeVisible(); | ||||
|     //Go to baseURL | ||||
|     await page.goto('./#/browse/mine?hideTree=true&hideInspector=true', { | ||||
|       waitUntil: 'networkidle' | ||||
|     }); | ||||
|     await expect(page.locator('div:has-text("CouchDB is offline")').nth(3)).toBeVisible(); | ||||
|   }); | ||||
|   test('Shows unknown if it receives an unexpected response code', async ({ page }) => { | ||||
|     await page.route('**/openmct/mine', (route) => { | ||||
|       route.fulfill({ | ||||
|         status: 418, | ||||
|         contentType: 'application/json', | ||||
|         body: JSON.stringify({}) | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     //Go to baseURL | ||||
|     await page.goto('./#/browse/mine?hideTree=true&hideInspector=true', { | ||||
|       waitUntil: 'networkidle' | ||||
|     }); | ||||
|     await expect(page.locator('div:has-text("CouchDB connectivity unknown")').nth(3)).toBeVisible(); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| test.describe("CouchDB initialization with mocked responses @couchdb", () => { | ||||
|     test.use({ failOnConsoleError: false }); | ||||
|     test("'My Items' folder is created if it doesn't exist", async ({ page }) => { | ||||
|         const mockedMissingObjectResponsefromCouchDB = { | ||||
|             status: 404, | ||||
|             contentType: 'application/json', | ||||
|             body: JSON.stringify({}) | ||||
|         }; | ||||
| test.describe('CouchDB initialization with mocked responses @couchdb', () => { | ||||
|   test.use({ failOnConsoleError: false }); | ||||
|   test("'My Items' folder is created if it doesn't exist", async ({ page }) => { | ||||
|     const mockedMissingObjectResponsefromCouchDB = { | ||||
|       status: 404, | ||||
|       contentType: 'application/json', | ||||
|       body: JSON.stringify({}) | ||||
|     }; | ||||
|  | ||||
|         // Override the first request to GET openmct/mine to return a 404. | ||||
|         // This simulates the case of starting Open MCT with a fresh database | ||||
|         // and no "My Items" folder created yet. | ||||
|         await page.route('**/mine', route => { | ||||
|             route.fulfill(mockedMissingObjectResponsefromCouchDB); | ||||
|         }, { times: 1 }); | ||||
|     // Override the first request to GET openmct/mine to return a 404. | ||||
|     // This simulates the case of starting Open MCT with a fresh database | ||||
|     // and no "My Items" folder created yet. | ||||
|     await page.route( | ||||
|       '**/mine', | ||||
|       (route) => { | ||||
|         route.fulfill(mockedMissingObjectResponsefromCouchDB); | ||||
|       }, | ||||
|       { times: 1 } | ||||
|     ); | ||||
|  | ||||
|         // Set up promise to verify that a PUT request to create "My Items" | ||||
|         // folder was made. | ||||
|         const putMineFolderRequest = page.waitForRequest(req => | ||||
|             req.url().endsWith('/mine') | ||||
|             && req.method() === 'PUT'); | ||||
|     // Set up promise to verify that a PUT request to create "My Items" | ||||
|     // folder was made. | ||||
|     const putMineFolderRequest = page.waitForRequest( | ||||
|       (req) => req.url().endsWith('/mine') && req.method() === 'PUT' | ||||
|     ); | ||||
|  | ||||
|         // Set up promise to verify that a GET request to retrieve "My Items" | ||||
|         // folder was made. | ||||
|         const getMineFolderRequest = page.waitForRequest(req => | ||||
|             req.url().endsWith('/mine') | ||||
|             && req.method() === 'GET'); | ||||
|     // Set up promise to verify that a GET request to retrieve "My Items" | ||||
|     // folder was made. | ||||
|     const getMineFolderRequest = page.waitForRequest( | ||||
|       (req) => req.url().endsWith('/mine') && req.method() === 'GET' | ||||
|     ); | ||||
|  | ||||
|         // Go to baseURL. | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|     // Go to baseURL. | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|  | ||||
|         // Wait for both requests to resolve. | ||||
|         await Promise.all([ | ||||
|             putMineFolderRequest, | ||||
|             getMineFolderRequest | ||||
|         ]); | ||||
|     }); | ||||
|     // Wait for both requests to resolve. | ||||
|     await Promise.all([putMineFolderRequest, getMineFolderRequest]); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -28,32 +28,31 @@ const { test, expect } = require('../../../pluginFixtures'); | ||||
| const { createDomainObjectWithDefaults } = require('../../../appActions'); | ||||
|  | ||||
| test.describe('Example Event Generator CRUD Operations', () => { | ||||
|     test('Can create a Test Event Generator and it results in the table View', async ({ page }) => { | ||||
|         //Go to baseURL | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|   test('Can create a Test Event Generator and it results in the table View', async ({ page }) => { | ||||
|     //Go to baseURL | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|  | ||||
|         //Create a name for the object | ||||
|         const newObjectName = 'Test Event Generator'; | ||||
|     //Create a name for the object | ||||
|     const newObjectName = 'Test Event Generator'; | ||||
|  | ||||
|         await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Event Message Generator', | ||||
|             name: newObjectName | ||||
|         }); | ||||
|  | ||||
|         //Assertions against newly created object which define standard behavior | ||||
|         await expect(page.waitForURL(/.*&view=table/)).toBeTruthy(); | ||||
|         await expect(page.locator('.l-browse-bar__object-name')).toContainText(newObjectName); | ||||
|     await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Event Message Generator', | ||||
|       name: newObjectName | ||||
|     }); | ||||
|  | ||||
|     //Assertions against newly created object which define standard behavior | ||||
|     await expect(page.waitForURL(/.*&view=table/)).toBeTruthy(); | ||||
|     await expect(page.locator('.l-browse-bar__object-name')).toContainText(newObjectName); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| test.describe('Example Event Generator Telemetry Event Verficiation', () => { | ||||
|  | ||||
|     test.fixme('telemetry is coming in for test event', async ({ page }) => { | ||||
|   test.fixme('telemetry is coming in for test event', async ({ page }) => { | ||||
|     // Go to object created in step one | ||||
|     // Verify the telemetry table is filled with > 1 row | ||||
|     }); | ||||
|     test.fixme('telemetry is sorted by time ascending', async ({ page }) => { | ||||
|   }); | ||||
|   test.fixme('telemetry is sorted by time ascending', async ({ page }) => { | ||||
|     // Go to object created in step one | ||||
|     // Verify the telemetry table has a class with "is-sorting asc" | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -27,93 +27,113 @@ This test suite is dedicated to tests which verify the basic operations surround | ||||
| const { test, expect } = require('../../../../baseFixtures'); | ||||
|  | ||||
| test.describe('Sine Wave Generator', () => { | ||||
|     test('Create new Sine Wave Generator Object and validate create Form Logic', async ({ page, browserName }) => { | ||||
|         // eslint-disable-next-line playwright/no-skipped-test | ||||
|         test.skip(browserName === 'firefox', 'This test needs to be updated to work with firefox'); | ||||
|   test('Create new Sine Wave Generator Object and validate create Form Logic', async ({ | ||||
|     page, | ||||
|     browserName | ||||
|   }) => { | ||||
|     // eslint-disable-next-line playwright/no-skipped-test | ||||
|     test.skip(browserName === 'firefox', 'This test needs to be updated to work with firefox'); | ||||
|  | ||||
|         //Go to baseURL | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|     //Go to baseURL | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|  | ||||
|         //Click the Create button | ||||
|         await page.click('button:has-text("Create")'); | ||||
|     //Click the Create button | ||||
|     await page.click('button:has-text("Create")'); | ||||
|  | ||||
|         // Click Sine Wave Generator | ||||
|         await page.click('text=Sine Wave Generator'); | ||||
|     // Click Sine Wave Generator | ||||
|     await page.click('text=Sine Wave Generator'); | ||||
|  | ||||
|         // Verify that the each required field has required indicator | ||||
|         // Title | ||||
|         await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(/req/); | ||||
|     // Verify that the each required field has required indicator | ||||
|     // Title | ||||
|     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'); | ||||
|     // Verify that the Notes row does not have a required indicator | ||||
|     await expect( | ||||
|       page.locator('.c-form__section div:nth-child(3) .form-row .c-form-row__state-indicator') | ||||
|     ).not.toContain('.req'); | ||||
|     await page.locator('textarea[type="text"]').fill('Optional Note Text'); | ||||
|  | ||||
|         // Period | ||||
|         await expect(page.locator('div:nth-child(4) .c-form-row__state-indicator')).toHaveClass(/req/); | ||||
|     // Period | ||||
|     await expect(page.locator('div:nth-child(4) .c-form-row__state-indicator')).toHaveClass(/req/); | ||||
|  | ||||
|         // Amplitude | ||||
|         await expect(page.locator('div:nth-child(5) .c-form-row__state-indicator')).toHaveClass(/req/); | ||||
|     // Amplitude | ||||
|     await expect(page.locator('div:nth-child(5) .c-form-row__state-indicator')).toHaveClass(/req/); | ||||
|  | ||||
|         // Offset | ||||
|         await expect(page.locator('div:nth-child(6) .c-form-row__state-indicator')).toHaveClass(/req/); | ||||
|     // Offset | ||||
|     await expect(page.locator('div:nth-child(6) .c-form-row__state-indicator')).toHaveClass(/req/); | ||||
|  | ||||
|         // Data Rate | ||||
|         await expect(page.locator('div:nth-child(7) .c-form-row__state-indicator')).toHaveClass(/req/); | ||||
|     // Data Rate | ||||
|     await expect(page.locator('div:nth-child(7) .c-form-row__state-indicator')).toHaveClass(/req/); | ||||
|  | ||||
|         // Phase | ||||
|         await expect(page.locator('div:nth-child(8) .c-form-row__state-indicator')).toHaveClass(/req/); | ||||
|     // Phase | ||||
|     await expect(page.locator('div:nth-child(8) .c-form-row__state-indicator')).toHaveClass(/req/); | ||||
|  | ||||
|         // Randomness | ||||
|         await expect(page.locator('div:nth-child(9) .c-form-row__state-indicator')).toHaveClass(/req/); | ||||
|     // Randomness | ||||
|     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(/invalid/); | ||||
|     // Verify that by removing value from required text field shows invalid indicator | ||||
|     await page | ||||
|       .locator( | ||||
|         'text=Properties Title Notes Period Amplitude Offset Data Rate (hz) Phase (radians) Ra >> input[type="text"]' | ||||
|       ) | ||||
|       .fill(''); | ||||
|     await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(/invalid/); | ||||
|  | ||||
|         // Verify that by adding value to empty required text field changes invalid to valid indicator | ||||
|         await page.locator('text=Properties Title Notes Period Amplitude Offset Data Rate (hz) Phase (radians) Ra >> input[type="text"]').fill('New Sine Wave Generator'); | ||||
|         await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(/valid/); | ||||
|     // Verify that by adding value to empty required text field changes invalid to valid indicator | ||||
|     await page | ||||
|       .locator( | ||||
|         'text=Properties Title Notes Period Amplitude Offset Data Rate (hz) Phase (radians) Ra >> input[type="text"]' | ||||
|       ) | ||||
|       .fill('New Sine Wave Generator'); | ||||
|     await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(/valid/); | ||||
|  | ||||
|         // Verify that by removing value from required number field shows invalid indicator | ||||
|         await page.locator('.field.control.l-input-sm input').first().fill(''); | ||||
|         await expect(page.locator('div:nth-child(4) .c-form-row__state-indicator')).toHaveClass(/invalid/); | ||||
|     // Verify that by removing value from required number field shows invalid indicator | ||||
|     await page.locator('.field.control.l-input-sm input').first().fill(''); | ||||
|     await expect(page.locator('div:nth-child(4) .c-form-row__state-indicator')).toHaveClass( | ||||
|       /invalid/ | ||||
|     ); | ||||
|  | ||||
|         // Verify that by adding value to empty required number field changes invalid to valid indicator | ||||
|         await page.locator('.field.control.l-input-sm input').first().fill('3'); | ||||
|         await expect(page.locator('div:nth-child(4) .c-form-row__state-indicator')).toHaveClass(/valid/); | ||||
|     // Verify that by adding value to empty required number field changes invalid to valid indicator | ||||
|     await page.locator('.field.control.l-input-sm input').first().fill('3'); | ||||
|     await expect(page.locator('div:nth-child(4) .c-form-row__state-indicator')).toHaveClass( | ||||
|       /valid/ | ||||
|     ); | ||||
|  | ||||
|         // Verify that can change value of number field by up/down arrows keys | ||||
|         // Click .field.control.l-input-sm input >> nth=0 | ||||
|         await page.locator('.field.control.l-input-sm input').first().click(); | ||||
|         // Press ArrowUp 3 times to change value from 3 to 6 | ||||
|         await page.locator('.field.control.l-input-sm input').first().press('ArrowUp'); | ||||
|         await page.locator('.field.control.l-input-sm input').first().press('ArrowUp'); | ||||
|         await page.locator('.field.control.l-input-sm input').first().press('ArrowUp'); | ||||
|     // Verify that can change value of number field by up/down arrows keys | ||||
|     // Click .field.control.l-input-sm input >> nth=0 | ||||
|     await page.locator('.field.control.l-input-sm input').first().click(); | ||||
|     // Press ArrowUp 3 times to change value from 3 to 6 | ||||
|     await page.locator('.field.control.l-input-sm input').first().press('ArrowUp'); | ||||
|     await page.locator('.field.control.l-input-sm input').first().press('ArrowUp'); | ||||
|     await page.locator('.field.control.l-input-sm input').first().press('ArrowUp'); | ||||
|  | ||||
|         const value = await page.locator('.field.control.l-input-sm input').first().inputValue(); | ||||
|         await expect(value).toBe('6'); | ||||
|     const value = await page.locator('.field.control.l-input-sm input').first().inputValue(); | ||||
|     await expect(value).toBe('6'); | ||||
|  | ||||
|         //Click text=OK | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.click('button:has-text("OK")') | ||||
|         ]); | ||||
|     //Click text=OK | ||||
|     await Promise.all([page.waitForNavigation(), page.click('button:has-text("OK")')]); | ||||
|  | ||||
|         // Verify that the Sine Wave Generator is displayed and correct | ||||
|         // Verify object properties | ||||
|         await expect(page.locator('.l-browse-bar__object-name')).toContainText('New Sine Wave Generator'); | ||||
|     // Verify that the Sine Wave Generator is displayed and correct | ||||
|     // Verify object properties | ||||
|     await expect(page.locator('.l-browse-bar__object-name')).toContainText( | ||||
|       'New Sine Wave Generator' | ||||
|     ); | ||||
|  | ||||
|         // Verify canvas rendered and can be interacted with | ||||
|         await page.locator('canvas').nth(1).click({ | ||||
|             position: { | ||||
|                 x: 341, | ||||
|                 y: 28 | ||||
|             } | ||||
|         }); | ||||
|     // Verify canvas rendered and can be interacted with | ||||
|     await page | ||||
|       .locator('canvas') | ||||
|       .nth(1) | ||||
|       .click({ | ||||
|         position: { | ||||
|           x: 341, | ||||
|           y: 28 | ||||
|         } | ||||
|       }); | ||||
|  | ||||
|         // Verify that where we click on canvas shows the number we clicked on | ||||
|         // Note that any number will do, we just care that a number exists | ||||
|         await expect(page.locator('.value-to-display-nearestValue')).toContainText(/[+-]?([0-9]*[.])?[0-9]+/); | ||||
|  | ||||
|     }); | ||||
|     // Verify that where we click on canvas shows the number we clicked on | ||||
|     // Note that any number will do, we just care that a number exists | ||||
|     await expect(page.locator('.value-to-display-nearestValue')).toContainText( | ||||
|       /[+-]?([0-9]*[.])?[0-9]+/ | ||||
|     ); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -19,7 +19,7 @@ | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* global __dirname */ | ||||
| /* | ||||
| This test suite is dedicated to tests which verify form functionality in isolation | ||||
| */ | ||||
| @@ -34,251 +34,265 @@ const jsonFilePath = 'e2e/test-data/ExampleLayouts.json'; | ||||
| const imageFilePath = 'e2e/test-data/rick.jpg'; | ||||
|  | ||||
| test.describe('Form Validation Behavior', () => { | ||||
|     test('Required Field indicators appear if title is empty and can be corrected', async ({ page }) => { | ||||
|         //Go to baseURL | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|   test('Required Field indicators appear if title is empty and can be corrected', async ({ | ||||
|     page | ||||
|   }) => { | ||||
|     //Go to baseURL | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|  | ||||
|         await page.click('button:has-text("Create")'); | ||||
|         await page.click(':nth-match(:text("Folder"), 2)'); | ||||
|     await page.click('button:has-text("Create")'); | ||||
|     await page.click(':nth-match(:text("Folder"), 2)'); | ||||
|  | ||||
|         // Fill in empty string into title and trigger validation with 'Tab' | ||||
|         await page.click('text=Properties Title Notes >> input[type="text"]'); | ||||
|         await page.fill('text=Properties Title Notes >> input[type="text"]', ''); | ||||
|         await page.press('text=Properties Title Notes >> input[type="text"]', 'Tab'); | ||||
|     // Fill in empty string into title and trigger validation with 'Tab' | ||||
|     await page.click('text=Properties Title Notes >> input[type="text"]'); | ||||
|     await page.fill('text=Properties Title Notes >> input[type="text"]', ''); | ||||
|     await page.press('text=Properties Title Notes >> input[type="text"]', 'Tab'); | ||||
|  | ||||
|         //Required Field Form Validation | ||||
|         await expect(page.locator('button:has-text("OK")')).toBeDisabled(); | ||||
|         await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(/invalid/); | ||||
|     //Required Field Form Validation | ||||
|     await expect(page.locator('button:has-text("OK")')).toBeDisabled(); | ||||
|     await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(/invalid/); | ||||
|  | ||||
|         //Correct Form Validation for missing title and trigger validation with 'Tab' | ||||
|         await page.click('text=Properties Title Notes >> input[type="text"]'); | ||||
|         await page.fill('text=Properties Title Notes >> input[type="text"]', TEST_FOLDER); | ||||
|         await page.press('text=Properties Title Notes >> input[type="text"]', 'Tab'); | ||||
|     //Correct Form Validation for missing title and trigger validation with 'Tab' | ||||
|     await page.click('text=Properties Title Notes >> input[type="text"]'); | ||||
|     await page.fill('text=Properties Title Notes >> input[type="text"]', TEST_FOLDER); | ||||
|     await page.press('text=Properties Title Notes >> input[type="text"]', 'Tab'); | ||||
|  | ||||
|         //Required Field Form Validation is corrected | ||||
|         await expect(page.locator('button:has-text("OK")')).toBeEnabled(); | ||||
|         await expect(page.locator('.c-form-row__state-indicator').first()).not.toHaveClass(/invalid/); | ||||
|     //Required Field Form Validation is corrected | ||||
|     await expect(page.locator('button:has-text("OK")')).toBeEnabled(); | ||||
|     await expect(page.locator('.c-form-row__state-indicator').first()).not.toHaveClass(/invalid/); | ||||
|  | ||||
|         //Finish Creating Domain Object | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.click('button:has-text("OK")') | ||||
|         ]); | ||||
|     //Finish Creating Domain Object | ||||
|     await Promise.all([page.waitForNavigation(), page.click('button:has-text("OK")')]); | ||||
|  | ||||
|         //Verify that the Domain Object has been created with the corrected title property | ||||
|         await expect(page.locator('.l-browse-bar__object-name')).toContainText(TEST_FOLDER); | ||||
|     }); | ||||
|     //Verify that the Domain Object has been created with the corrected title property | ||||
|     await expect(page.locator('.l-browse-bar__object-name')).toContainText(TEST_FOLDER); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| test.describe('Form File Input Behavior', () => { | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         // eslint-disable-next-line no-undef | ||||
|         await page.addInitScript({ path: path.join(__dirname, '../../helper', 'addInitFileInputObject.js') }); | ||||
|   test.beforeEach(async ({ page }) => { | ||||
|     await page.addInitScript({ | ||||
|       path: path.join(__dirname, '../../helper', 'addInitFileInputObject.js') | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|     test('Can select a JSON file type', async ({ page }) => { | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|   test('Can select a JSON file type', async ({ page }) => { | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|  | ||||
|         await page.getByRole('button', { name: ' Create ' }).click(); | ||||
|         await page.getByRole('menuitem', { name: 'JSON File Input Object' }).click(); | ||||
|     await page.getByRole('button', { name: ' Create ' }).click(); | ||||
|     await page.getByRole('menuitem', { name: 'JSON File Input Object' }).click(); | ||||
|  | ||||
|         await page.setInputFiles('#fileElem', jsonFilePath); | ||||
|     await page.setInputFiles('#fileElem', jsonFilePath); | ||||
|  | ||||
|         await page.getByRole('button', { name: 'Save' }).click(); | ||||
|     await page.getByRole('button', { name: 'Save' }).click(); | ||||
|  | ||||
|         const type = await page.locator('#file-input-type').textContent(); | ||||
|         await expect(type).toBe(`"string"`); | ||||
|     }); | ||||
|     const type = await page.locator('#file-input-type').textContent(); | ||||
|     await expect(type).toBe(`"string"`); | ||||
|   }); | ||||
|  | ||||
|     test('Can select an image file type', async ({ page }) => { | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|   test('Can select an image file type', async ({ page }) => { | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|  | ||||
|         await page.getByRole('button', { name: ' Create ' }).click(); | ||||
|         await page.getByRole('menuitem', { name: 'Image File Input Object' }).click(); | ||||
|     await page.getByRole('button', { name: ' Create ' }).click(); | ||||
|     await page.getByRole('menuitem', { name: 'Image File Input Object' }).click(); | ||||
|  | ||||
|         await page.setInputFiles('#fileElem', imageFilePath); | ||||
|     await page.setInputFiles('#fileElem', imageFilePath); | ||||
|  | ||||
|         await page.getByRole('button', { name: 'Save' }).click(); | ||||
|     await page.getByRole('button', { name: 'Save' }).click(); | ||||
|  | ||||
|         const type = await page.locator('#file-input-type').textContent(); | ||||
|         await expect(type).toBe(`"object"`); | ||||
|     }); | ||||
|     const type = await page.locator('#file-input-type').textContent(); | ||||
|     await expect(type).toBe(`"object"`); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| test.describe('Persistence operations @addInit', () => { | ||||
|     // add non persistable root item | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         // eslint-disable-next-line no-undef | ||||
|         await page.addInitScript({ path: path.join(__dirname, '../../helper', 'addNoneditableObject.js') }); | ||||
|   // add non persistable root item | ||||
|   test.beforeEach(async ({ page }) => { | ||||
|     await page.addInitScript({ | ||||
|       path: path.join(__dirname, '../../helper', 'addNoneditableObject.js') | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|     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' | ||||
|         }); | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|  | ||||
|         await page.click('button:has-text("Create")'); | ||||
|  | ||||
|         await page.click('text=Condition Set'); | ||||
|  | ||||
|         await page.locator('form[name="mctForm"] >> text=Persistence Testing').click(); | ||||
|  | ||||
|         const okButton = page.locator('button:has-text("OK")'); | ||||
|         await expect(okButton).toBeDisabled(); | ||||
|   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' | ||||
|     }); | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|  | ||||
|     await page.click('button:has-text("Create")'); | ||||
|  | ||||
|     await page.click('text=Condition Set'); | ||||
|  | ||||
|     await page.locator('form[name="mctForm"] >> text=Persistence Testing').click(); | ||||
|  | ||||
|     const okButton = page.locator('button:has-text("OK")'); | ||||
|     await expect(okButton).toBeDisabled(); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| test.describe('Persistence operations @couchdb', () => { | ||||
|     test.use({ failOnConsoleError: false }); | ||||
|     test('Editing object properties should generate a single persistence operation', async ({ page }) => { | ||||
|         test.info().annotations.push({ | ||||
|             type: 'issue', | ||||
|             description: 'https://github.com/nasa/openmct/issues/5616' | ||||
|         }); | ||||
|  | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|  | ||||
|         // Create a new 'Clock' object with default settings | ||||
|         const clock = await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Clock' | ||||
|         }); | ||||
|  | ||||
|         // Count all persistence operations (PUT requests) for this specific object | ||||
|         let putRequestCount = 0; | ||||
|         page.on('request', req => { | ||||
|             if (req.method() === 'PUT' && req.url().endsWith(clock.uuid)) { | ||||
|                 putRequestCount += 1; | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         // Open the edit form for the clock object | ||||
|         await page.click('button[title="More options"]'); | ||||
|         await page.click('li[title="Edit properties of this object."]'); | ||||
|  | ||||
|         // Modify the display format from default 12hr -> 24hr and click 'Save' | ||||
|         await page.locator('select[aria-label="12 or 24 hour clock"]').selectOption({ value: 'clock24' }); | ||||
|         await page.click('button[aria-label="Save"]'); | ||||
|  | ||||
|         await expect.poll(() => putRequestCount, { | ||||
|             message: 'Verify a single PUT request was made to persist the object', | ||||
|             timeout: 1000 | ||||
|         }).toEqual(1); | ||||
|   test.use({ failOnConsoleError: false }); | ||||
|   test('Editing object properties should generate a single persistence operation', async ({ | ||||
|     page | ||||
|   }) => { | ||||
|     test.info().annotations.push({ | ||||
|       type: 'issue', | ||||
|       description: 'https://github.com/nasa/openmct/issues/5616' | ||||
|     }); | ||||
|     test('Can create an object after a conflict error @couchdb @2p', async ({ page, openmctConfig }) => { | ||||
|         test.info().annotations.push({ | ||||
|             type: 'issue', | ||||
|             description: 'https://github.com/nasa/openmct/issues/5982' | ||||
|         }); | ||||
|         const { myItemsFolderName } = openmctConfig; | ||||
|         // Instantiate a second page/tab | ||||
|         const page2 = await page.context().newPage(); | ||||
|  | ||||
|         // Both pages: Go to baseURL | ||||
|         await Promise.all([ | ||||
|             page.goto('./', { waitUntil: 'networkidle' }), | ||||
|             page2.goto('./', { waitUntil: 'networkidle' }) | ||||
|         ]); | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|  | ||||
|         //Slow down the test a bit | ||||
|         await expect(page.getByRole('treeitem', { name: `  ${myItemsFolderName}` })).toBeVisible(); | ||||
|         await expect(page2.getByRole('treeitem', { name: `  ${myItemsFolderName}` })).toBeVisible(); | ||||
|  | ||||
|         // Both pages: Click the Create button | ||||
|         await Promise.all([ | ||||
|             page.click('button:has-text("Create")'), | ||||
|             page2.click('button:has-text("Create")') | ||||
|         ]); | ||||
|  | ||||
|         // Both pages: Click "Clock" in the Create menu | ||||
|         await Promise.all([ | ||||
|             page.click(`li[role='menuitem']:text("Clock")`), | ||||
|             page2.click(`li[role='menuitem']:text("Clock")`) | ||||
|         ]); | ||||
|  | ||||
|         // Generate unique names for both objects | ||||
|         const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]'); | ||||
|         const nameInput2 = page2.locator('form[name="mctForm"] .first input[type="text"]'); | ||||
|  | ||||
|         // Both pages: Fill in the 'Name' form field. | ||||
|         await Promise.all([ | ||||
|             nameInput.fill(""), | ||||
|             nameInput.fill(`Clock:${genUuid()}`), | ||||
|             nameInput2.fill(""), | ||||
|             nameInput2.fill(`Clock:${genUuid()}`) | ||||
|         ]); | ||||
|  | ||||
|         // Both pages: Fill the "Notes" section with information about the | ||||
|         // currently running test and its project. | ||||
|         const testNotes = page.testNotes; | ||||
|         const notesInput = page.locator('form[name="mctForm"] #notes-textarea'); | ||||
|         const notesInput2 = page2.locator('form[name="mctForm"] #notes-textarea'); | ||||
|         await Promise.all([ | ||||
|             notesInput.fill(testNotes), | ||||
|             notesInput2.fill(testNotes) | ||||
|         ]); | ||||
|  | ||||
|         // Page 2: Click "OK" to create the domain object and wait for navigation. | ||||
|         // This will update the composition of the parent folder, setting the | ||||
|         // conditions for a conflict error from the first page. | ||||
|         await Promise.all([ | ||||
|             page2.waitForLoadState(), | ||||
|             page2.click('[aria-label="Save"]'), | ||||
|             // Wait for Save Banner to appear | ||||
|             page2.waitForSelector('.c-message-banner__message') | ||||
|         ]); | ||||
|  | ||||
|         // Close Page 2, we're done with it. | ||||
|         await page2.close(); | ||||
|  | ||||
|         // Page 1: Click "OK" to create the domain object and wait for navigation. | ||||
|         // This will trigger a conflict error upon attempting to update | ||||
|         // the composition of the parent folder. | ||||
|         await Promise.all([ | ||||
|             page.waitForLoadState(), | ||||
|             page.click('[aria-label="Save"]'), | ||||
|             // Wait for Save Banner to appear | ||||
|             page.waitForSelector('.c-message-banner__message') | ||||
|         ]); | ||||
|  | ||||
|         // Page 1: Verify that the conflict has occurred and an error notification is displayed. | ||||
|         await expect(page.locator('.c-message-banner__message', { | ||||
|             hasText: "Conflict detected while saving mine" | ||||
|         })).toBeVisible(); | ||||
|  | ||||
|         // Page 1: Start logging console errors from this point on | ||||
|         let errors = []; | ||||
|         page.on('console', (msg) => { | ||||
|             if (msg.type() === 'error') { | ||||
|                 errors.push(msg.text()); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         // Page 1: Try to create a clock with the page that received the conflict. | ||||
|         const clockAfterConflict = await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Clock' | ||||
|         }); | ||||
|  | ||||
|         // Page 1: Wait for save progress dialog to appear/disappear | ||||
|         await page.locator('.c-message-banner__message', { | ||||
|             hasText: 'Do not navigate away from this page or close this browser tab while this message is displayed.', | ||||
|             state: 'visible' | ||||
|         }).waitFor({ state: 'hidden' }); | ||||
|  | ||||
|         // Page 1: Navigate to 'My Items' and verify that the second clock was created | ||||
|         await page.goto('./#/browse/mine'); | ||||
|         await expect(page.locator(`.c-grid-item__name[title="${clockAfterConflict.name}"]`)).toBeVisible(); | ||||
|  | ||||
|         // Verify no console errors occurred | ||||
|         expect(errors).toHaveLength(0); | ||||
|     // Create a new 'Clock' object with default settings | ||||
|     const clock = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Clock' | ||||
|     }); | ||||
|  | ||||
|     // Count all persistence operations (PUT requests) for this specific object | ||||
|     let putRequestCount = 0; | ||||
|     page.on('request', (req) => { | ||||
|       if (req.method() === 'PUT' && req.url().endsWith(clock.uuid)) { | ||||
|         putRequestCount += 1; | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     // Open the edit form for the clock object | ||||
|     await page.click('button[title="More options"]'); | ||||
|     await page.click('li[title="Edit properties of this object."]'); | ||||
|  | ||||
|     // Modify the display format from default 12hr -> 24hr and click 'Save' | ||||
|     await page | ||||
|       .locator('select[aria-label="12 or 24 hour clock"]') | ||||
|       .selectOption({ value: 'clock24' }); | ||||
|     await page.click('button[aria-label="Save"]'); | ||||
|  | ||||
|     await expect | ||||
|       .poll(() => putRequestCount, { | ||||
|         message: 'Verify a single PUT request was made to persist the object', | ||||
|         timeout: 1000 | ||||
|       }) | ||||
|       .toEqual(1); | ||||
|   }); | ||||
|   test('Can create an object after a conflict error @couchdb @2p', async ({ | ||||
|     page, | ||||
|     openmctConfig | ||||
|   }) => { | ||||
|     test.info().annotations.push({ | ||||
|       type: 'issue', | ||||
|       description: 'https://github.com/nasa/openmct/issues/5982' | ||||
|     }); | ||||
|     const { myItemsFolderName } = openmctConfig; | ||||
|     // Instantiate a second page/tab | ||||
|     const page2 = await page.context().newPage(); | ||||
|  | ||||
|     // Both pages: Go to baseURL | ||||
|     await Promise.all([ | ||||
|       page.goto('./', { waitUntil: 'networkidle' }), | ||||
|       page2.goto('./', { waitUntil: 'networkidle' }) | ||||
|     ]); | ||||
|  | ||||
|     //Slow down the test a bit | ||||
|     await expect(page.getByRole('treeitem', { name: `  ${myItemsFolderName}` })).toBeVisible(); | ||||
|     await expect(page2.getByRole('treeitem', { name: `  ${myItemsFolderName}` })).toBeVisible(); | ||||
|  | ||||
|     // Both pages: Click the Create button | ||||
|     await Promise.all([ | ||||
|       page.click('button:has-text("Create")'), | ||||
|       page2.click('button:has-text("Create")') | ||||
|     ]); | ||||
|  | ||||
|     // Both pages: Click "Clock" in the Create menu | ||||
|     await Promise.all([ | ||||
|       page.click(`li[role='menuitem']:text("Clock")`), | ||||
|       page2.click(`li[role='menuitem']:text("Clock")`) | ||||
|     ]); | ||||
|  | ||||
|     // Generate unique names for both objects | ||||
|     const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]'); | ||||
|     const nameInput2 = page2.locator('form[name="mctForm"] .first input[type="text"]'); | ||||
|  | ||||
|     // Both pages: Fill in the 'Name' form field. | ||||
|     await Promise.all([ | ||||
|       nameInput.fill(''), | ||||
|       nameInput.fill(`Clock:${genUuid()}`), | ||||
|       nameInput2.fill(''), | ||||
|       nameInput2.fill(`Clock:${genUuid()}`) | ||||
|     ]); | ||||
|  | ||||
|     // Both pages: Fill the "Notes" section with information about the | ||||
|     // currently running test and its project. | ||||
|     const testNotes = page.testNotes; | ||||
|     const notesInput = page.locator('form[name="mctForm"] #notes-textarea'); | ||||
|     const notesInput2 = page2.locator('form[name="mctForm"] #notes-textarea'); | ||||
|     await Promise.all([notesInput.fill(testNotes), notesInput2.fill(testNotes)]); | ||||
|  | ||||
|     // Page 2: Click "OK" to create the domain object and wait for navigation. | ||||
|     // This will update the composition of the parent folder, setting the | ||||
|     // conditions for a conflict error from the first page. | ||||
|     await Promise.all([ | ||||
|       page2.waitForLoadState(), | ||||
|       page2.click('[aria-label="Save"]'), | ||||
|       // Wait for Save Banner to appear | ||||
|       page2.waitForSelector('.c-message-banner__message') | ||||
|     ]); | ||||
|  | ||||
|     // Close Page 2, we're done with it. | ||||
|     await page2.close(); | ||||
|  | ||||
|     // Page 1: Click "OK" to create the domain object and wait for navigation. | ||||
|     // This will trigger a conflict error upon attempting to update | ||||
|     // the composition of the parent folder. | ||||
|     await Promise.all([ | ||||
|       page.waitForLoadState(), | ||||
|       page.click('[aria-label="Save"]'), | ||||
|       // Wait for Save Banner to appear | ||||
|       page.waitForSelector('.c-message-banner__message') | ||||
|     ]); | ||||
|  | ||||
|     // Page 1: Verify that the conflict has occurred and an error notification is displayed. | ||||
|     await expect( | ||||
|       page.locator('.c-message-banner__message', { | ||||
|         hasText: 'Conflict detected while saving mine' | ||||
|       }) | ||||
|     ).toBeVisible(); | ||||
|  | ||||
|     // Page 1: Start logging console errors from this point on | ||||
|     let errors = []; | ||||
|     page.on('console', (msg) => { | ||||
|       if (msg.type() === 'error') { | ||||
|         errors.push(msg.text()); | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     // Page 1: Try to create a clock with the page that received the conflict. | ||||
|     const clockAfterConflict = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Clock' | ||||
|     }); | ||||
|  | ||||
|     // Page 1: Wait for save progress dialog to appear/disappear | ||||
|     await page | ||||
|       .locator('.c-message-banner__message', { | ||||
|         hasText: | ||||
|           'Do not navigate away from this page or close this browser tab while this message is displayed.', | ||||
|         state: 'visible' | ||||
|       }) | ||||
|       .waitFor({ state: 'hidden' }); | ||||
|  | ||||
|     // Page 1: Navigate to 'My Items' and verify that the second clock was created | ||||
|     await page.goto('./#/browse/mine'); | ||||
|     await expect( | ||||
|       page.locator(`.c-grid-item__name[title="${clockAfterConflict.name}"]`) | ||||
|     ).toBeVisible(); | ||||
|  | ||||
|     // Verify no console errors occurred | ||||
|     expect(errors).toHaveLength(0); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| test.describe('Form Correctness by Object Type', () => { | ||||
|     test.fixme('Verify correct behavior of number object (SWG)', async ({page}) => {}); | ||||
|     test.fixme('Verify correct behavior of number object Timer', async ({page}) => {}); | ||||
|     test.fixme('Verify correct behavior of number object Plan View', async ({page}) => {}); | ||||
|     test.fixme('Verify correct behavior of number object Clock', async ({page}) => {}); | ||||
|     test.fixme('Verify correct behavior of number object Hyperlink', async ({page}) => {}); | ||||
|   test.fixme('Verify correct behavior of number object (SWG)', async ({ page }) => {}); | ||||
|   test.fixme('Verify correct behavior of number object Timer', async ({ page }) => {}); | ||||
|   test.fixme('Verify correct behavior of number object Plan View', async ({ page }) => {}); | ||||
|   test.fixme('Verify correct behavior of number object Clock', async ({ page }) => {}); | ||||
|   test.fixme('Verify correct behavior of number object Hyperlink', async ({ page }) => {}); | ||||
| }); | ||||
|   | ||||
| @@ -19,7 +19,7 @@ | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* global __dirname */ | ||||
| /* | ||||
| This test suite is dedicated to tests which verify persistability checks | ||||
| */ | ||||
| @@ -29,22 +29,31 @@ const { test, expect } = require('../../baseFixtures.js'); | ||||
| const path = require('path'); | ||||
|  | ||||
| test.describe('Persistence operations @addInit', () => { | ||||
|     // add non persistable root item | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         // eslint-disable-next-line no-undef | ||||
|         await page.addInitScript({ path: path.join(__dirname, '../../helper', 'addNoneditableObject.js') }); | ||||
|   // add non persistable root item | ||||
|   test.beforeEach(async ({ page }) => { | ||||
|     await page.addInitScript({ | ||||
|       path: path.join(__dirname, '../../helper', 'addNoneditableObject.js') | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   test('Non-persistable objects should not show persistence related actions', async ({ page }) => { | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|  | ||||
|     await page.locator('text=Persistence Testing').first().click({ | ||||
|       button: 'right' | ||||
|     }); | ||||
|  | ||||
|     test('Non-persistable objects should not show persistence related actions', async ({ page }) => { | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|     const menuOptions = page.locator('.c-menu li'); | ||||
|  | ||||
|         await page.locator('text=Persistence Testing').first().click({ | ||||
|             button: 'right' | ||||
|         }); | ||||
|  | ||||
|         const menuOptions = page.locator('.c-menu li'); | ||||
|  | ||||
|         await expect.soft(menuOptions).toContainText(['Open In New Tab', 'View', 'Create Link']); | ||||
|         await expect(menuOptions).not.toContainText(['Move', 'Duplicate', 'Remove', 'Add New Folder', 'Edit Properties...', 'Export as JSON', 'Import from JSON']); | ||||
|     }); | ||||
|     await expect.soft(menuOptions).toContainText(['Open In New Tab', 'View', 'Create Link']); | ||||
|     await expect(menuOptions).not.toContainText([ | ||||
|       'Move', | ||||
|       'Duplicate', | ||||
|       'Remove', | ||||
|       'Add New Folder', | ||||
|       'Edit Properties...', | ||||
|       'Export as JSON', | ||||
|       'Import from JSON' | ||||
|     ]); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -1,276 +1,303 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2023, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* | ||||
| This test suite is dedicated to tests which verify the basic operations surrounding moving & linking objects. | ||||
| */ | ||||
|  | ||||
| const { test, expect } = require('../../pluginFixtures'); | ||||
| const { createDomainObjectWithDefaults } = require('../../appActions'); | ||||
|  | ||||
| test.describe('Move & link item tests', () => { | ||||
|     test('Create a basic object and verify that it can be moved to another folder', async ({ page, openmctConfig }) => { | ||||
|         const { myItemsFolderName } = openmctConfig; | ||||
|  | ||||
|         // Go to Open MCT | ||||
|         await page.goto('./'); | ||||
|  | ||||
|         const parentFolder = await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Folder', | ||||
|             name: 'Parent Folder' | ||||
|         }); | ||||
|         const childFolder = await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Folder', | ||||
|             name: 'Child Folder', | ||||
|             parent: parentFolder.uuid | ||||
|         }); | ||||
|         const grandchildFolder = await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Folder', | ||||
|             name: 'Grandchild Folder', | ||||
|             parent: childFolder.uuid | ||||
|         }); | ||||
|  | ||||
|         // Attempt to move parent to its own grandparent | ||||
|         await page.locator('button[title="Show selected item in tree"]').click(); | ||||
|  | ||||
|         const treePane = page.getByRole('tree', { | ||||
|             name: 'Main Tree' | ||||
|         }); | ||||
|         await treePane.getByRole('treeitem', { | ||||
|             name: 'Parent Folder' | ||||
|         }).click({ | ||||
|             button: 'right' | ||||
|         }); | ||||
|  | ||||
|         await page.getByRole('menuitem', { | ||||
|             name: /Move/ | ||||
|         }).click(); | ||||
|  | ||||
|         const createModalTree = page.getByRole('tree', { | ||||
|             name: "Create Modal Tree" | ||||
|         }); | ||||
|         const myItemsLocatorTreeItem = createModalTree.getByRole('treeitem', { | ||||
|             name: myItemsFolderName | ||||
|         }); | ||||
|         await myItemsLocatorTreeItem.locator('.c-disclosure-triangle').click(); | ||||
|         await myItemsLocatorTreeItem.click(); | ||||
|  | ||||
|         const parentFolderLocatorTreeItem = createModalTree.getByRole('treeitem', { | ||||
|             name: parentFolder.name | ||||
|         }); | ||||
|         await parentFolderLocatorTreeItem.locator('.c-disclosure-triangle').click(); | ||||
|         await parentFolderLocatorTreeItem.click(); | ||||
|         await expect(page.locator('[aria-label="Save"]')).toBeDisabled(); | ||||
|  | ||||
|         const childFolderLocatorTreeItem = createModalTree.getByRole('treeitem', { | ||||
|             name: new RegExp(childFolder.name) | ||||
|         }); | ||||
|         await childFolderLocatorTreeItem.locator('.c-disclosure-triangle').click(); | ||||
|         await childFolderLocatorTreeItem.click(); | ||||
|         await expect(page.locator('[aria-label="Save"]')).toBeDisabled(); | ||||
|  | ||||
|         const grandchildFolderLocatorTreeItem = createModalTree.getByRole('treeitem', { | ||||
|             name: grandchildFolder.name | ||||
|         }); | ||||
|         await grandchildFolderLocatorTreeItem.locator('.c-disclosure-triangle').click(); | ||||
|         await grandchildFolderLocatorTreeItem.click(); | ||||
|         await expect(page.locator('[aria-label="Save"]')).toBeDisabled(); | ||||
|  | ||||
|         await parentFolderLocatorTreeItem.click(); | ||||
|         await expect(page.locator('[aria-label="Save"]')).toBeDisabled(); | ||||
|         await page.locator('[aria-label="Cancel"]').click(); | ||||
|  | ||||
|         // Move Child Folder from Parent Folder to My Items | ||||
|         await treePane.getByRole('treeitem', { | ||||
|             name: new RegExp(childFolder.name) | ||||
|         }).click({ | ||||
|             button: 'right' | ||||
|         }); | ||||
|         await page.getByRole('menuitem', { | ||||
|             name: /Move/ | ||||
|         }).click(); | ||||
|         await myItemsLocatorTreeItem.click(); | ||||
|  | ||||
|         await page.locator('[aria-label="Save"]').click(); | ||||
|         const myItemsPaneTreeItem = treePane.getByRole('treeitem', { | ||||
|             name: myItemsFolderName | ||||
|         }); | ||||
|  | ||||
|         // Expect that Child Folder is in My Items, the root folder | ||||
|         expect(myItemsPaneTreeItem.locator('nth=0:has(text=Child Folder)')).toBeTruthy(); | ||||
|     }); | ||||
|     test('Create a basic object and verify that it cannot be moved to telemetry object without Composition Provider', async ({ page, openmctConfig }) => { | ||||
|         const { myItemsFolderName } = openmctConfig; | ||||
|  | ||||
|         // Go to Open MCT | ||||
|         await page.goto('./'); | ||||
|  | ||||
|         // Create Telemetry Table | ||||
|         let telemetryTable = 'Test Telemetry Table'; | ||||
|         await page.locator('button:has-text("Create")').click(); | ||||
|         await page.locator('li[role="menuitem"]:has-text("Telemetry Table")').click(); | ||||
|         await page.locator('text=Properties Title Notes >> input[type="text"]').click(); | ||||
|         await page.locator('text=Properties Title Notes >> input[type="text"]').fill(telemetryTable); | ||||
|  | ||||
|         await page.locator('button:has-text("OK")').click(); | ||||
|  | ||||
|         // Finish editing and save Telemetry Table | ||||
|         await page.locator('.c-button--menu.c-button--major.icon-save').click(); | ||||
|         await page.locator('text=Save and Finish Editing').click(); | ||||
|  | ||||
|         // Create New Folder Basic Domain Object | ||||
|         let folder = 'Test Folder'; | ||||
|         await page.locator('button:has-text("Create")').click(); | ||||
|         await page.locator('li[role="menuitem"]:has-text("Folder")').click(); | ||||
|         await page.locator('text=Properties Title Notes >> input[type="text"]').click(); | ||||
|         await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder); | ||||
|  | ||||
|         // See if it's possible to put the folder in the Telemetry object during creation (Soft Assert) | ||||
|         await page.locator(`form[name="mctForm"] >> text=${telemetryTable}`).click(); | ||||
|         let okButton = page.locator('button.c-button.c-button--major:has-text("OK")'); | ||||
|         let okButtonStateDisabled = await okButton.isDisabled(); | ||||
|         expect.soft(okButtonStateDisabled).toBeTruthy(); | ||||
|  | ||||
|         // Continue test regardless of assertion and create it in My Items | ||||
|         await page.locator(`form[name="mctForm"] >> text=${myItemsFolderName}`).click(); | ||||
|         await page.locator('button:has-text("OK")').click(); | ||||
|  | ||||
|         // Open My Items | ||||
|         await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click(); | ||||
|  | ||||
|         // Select Folder Object and select Move from context menu | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator(`a:has-text("${folder}")`).click() | ||||
|         ]); | ||||
|         await page.locator('.c-tree__item.is-navigated-object .c-tree__item__label .c-tree__item__type-icon').click({ | ||||
|             button: 'right' | ||||
|         }); | ||||
|         await page.locator('li.icon-move').click(); | ||||
|  | ||||
|         // See if it's possible to put the folder in the Telemetry object after creation | ||||
|         await page.locator(`text=Location Open MCT ${myItemsFolderName} >> span`).nth(3).click(); | ||||
|         await page.locator(`form[name="mctForm"] >> text=${telemetryTable}`).click(); | ||||
|         let okButton2 = page.locator('button.c-button.c-button--major:has-text("OK")'); | ||||
|         let okButtonStateDisabled2 = await okButton2.isDisabled(); | ||||
|         expect(okButtonStateDisabled2).toBeTruthy(); | ||||
|     }); | ||||
|  | ||||
|     test('Create a basic object and verify that it can be linked to another folder', async ({ page, openmctConfig }) => { | ||||
|         const { myItemsFolderName } = openmctConfig; | ||||
|  | ||||
|         // Go to Open MCT | ||||
|         await page.goto('./'); | ||||
|  | ||||
|         const parentFolder = await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Folder', | ||||
|             name: 'Parent Folder' | ||||
|         }); | ||||
|         const childFolder = await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Folder', | ||||
|             name: 'Child Folder', | ||||
|             parent: parentFolder.uuid | ||||
|         }); | ||||
|         const grandchildFolder = await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Folder', | ||||
|             name: 'Grandchild Folder', | ||||
|             parent: childFolder.uuid | ||||
|         }); | ||||
|  | ||||
|         // Attempt to move parent to its own grandparent | ||||
|         await page.locator('button[title="Show selected item in tree"]').click(); | ||||
|  | ||||
|         const treePane = page.getByRole('tree', { | ||||
|             name: 'Main Tree' | ||||
|         }); | ||||
|         await treePane.getByRole('treeitem', { | ||||
|             name: 'Parent Folder' | ||||
|         }).click({ | ||||
|             button: 'right' | ||||
|         }); | ||||
|  | ||||
|         await page.getByRole('menuitem', { | ||||
|             name: /Move/ | ||||
|         }).click(); | ||||
|  | ||||
|         const createModalTree = page.getByRole('tree', { | ||||
|             name: "Create Modal Tree" | ||||
|         }); | ||||
|         const myItemsLocatorTreeItem = createModalTree.getByRole('treeitem', { | ||||
|             name: myItemsFolderName | ||||
|         }); | ||||
|         await myItemsLocatorTreeItem.locator('.c-disclosure-triangle').click(); | ||||
|         await myItemsLocatorTreeItem.click(); | ||||
|  | ||||
|         const parentFolderLocatorTreeItem = createModalTree.getByRole('treeitem', { | ||||
|             name: parentFolder.name | ||||
|         }); | ||||
|         await parentFolderLocatorTreeItem.locator('.c-disclosure-triangle').click(); | ||||
|         await parentFolderLocatorTreeItem.click(); | ||||
|         await expect(page.locator('[aria-label="Save"]')).toBeDisabled(); | ||||
|  | ||||
|         const childFolderLocatorTreeItem = createModalTree.getByRole('treeitem', { | ||||
|             name: new RegExp(childFolder.name) | ||||
|         }); | ||||
|         await childFolderLocatorTreeItem.locator('.c-disclosure-triangle').click(); | ||||
|         await childFolderLocatorTreeItem.click(); | ||||
|         await expect(page.locator('[aria-label="Save"]')).toBeDisabled(); | ||||
|  | ||||
|         const grandchildFolderLocatorTreeItem = createModalTree.getByRole('treeitem', { | ||||
|             name: grandchildFolder.name | ||||
|         }); | ||||
|         await grandchildFolderLocatorTreeItem.locator('.c-disclosure-triangle').click(); | ||||
|         await grandchildFolderLocatorTreeItem.click(); | ||||
|         await expect(page.locator('[aria-label="Save"]')).toBeDisabled(); | ||||
|  | ||||
|         await parentFolderLocatorTreeItem.click(); | ||||
|         await expect(page.locator('[aria-label="Save"]')).toBeDisabled(); | ||||
|         await page.locator('[aria-label="Cancel"]').click(); | ||||
|  | ||||
|         // Move Child Folder from Parent Folder to My Items | ||||
|         await treePane.getByRole('treeitem', { | ||||
|             name: new RegExp(childFolder.name) | ||||
|         }).click({ | ||||
|             button: 'right' | ||||
|         }); | ||||
|         await page.getByRole('menuitem', { | ||||
|             name: /Link/ | ||||
|         }).click(); | ||||
|         await myItemsLocatorTreeItem.click(); | ||||
|  | ||||
|         await page.locator('[aria-label="Save"]').click(); | ||||
|         const myItemsPaneTreeItem = treePane.getByRole('treeitem', { | ||||
|             name: myItemsFolderName | ||||
|         }); | ||||
|  | ||||
|         // Expect that Child Folder is in My Items, the root folder | ||||
|         expect(myItemsPaneTreeItem.locator('nth=0:has(text=Child Folder)')).toBeTruthy(); | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| test.fixme('Cannot move a previously created domain object to non-peristable object in Move Modal', async ({ page }) => { | ||||
|     //Create a domain object | ||||
|     //Save Domain object | ||||
|     //Move Object and verify that cannot select non-persistable object | ||||
|     //Move Object to My Items | ||||
|     //Verify successful move | ||||
| }); | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2023, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* | ||||
| This test suite is dedicated to tests which verify the basic operations surrounding moving & linking objects. | ||||
| */ | ||||
|  | ||||
| const { test, expect } = require('../../pluginFixtures'); | ||||
| const { createDomainObjectWithDefaults } = require('../../appActions'); | ||||
|  | ||||
| test.describe('Move & link item tests', () => { | ||||
|   test('Create a basic object and verify that it can be moved to another folder', async ({ | ||||
|     page, | ||||
|     openmctConfig | ||||
|   }) => { | ||||
|     const { myItemsFolderName } = openmctConfig; | ||||
|  | ||||
|     // Go to Open MCT | ||||
|     await page.goto('./'); | ||||
|  | ||||
|     const parentFolder = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Folder', | ||||
|       name: 'Parent Folder' | ||||
|     }); | ||||
|     const childFolder = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Folder', | ||||
|       name: 'Child Folder', | ||||
|       parent: parentFolder.uuid | ||||
|     }); | ||||
|     const grandchildFolder = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Folder', | ||||
|       name: 'Grandchild Folder', | ||||
|       parent: childFolder.uuid | ||||
|     }); | ||||
|  | ||||
|     // Attempt to move parent to its own grandparent | ||||
|     await page.locator('button[title="Show selected item in tree"]').click(); | ||||
|  | ||||
|     const treePane = page.getByRole('tree', { | ||||
|       name: 'Main Tree' | ||||
|     }); | ||||
|     await treePane | ||||
|       .getByRole('treeitem', { | ||||
|         name: 'Parent Folder' | ||||
|       }) | ||||
|       .click({ | ||||
|         button: 'right' | ||||
|       }); | ||||
|  | ||||
|     await page | ||||
|       .getByRole('menuitem', { | ||||
|         name: /Move/ | ||||
|       }) | ||||
|       .click(); | ||||
|  | ||||
|     const createModalTree = page.getByRole('tree', { | ||||
|       name: 'Create Modal Tree' | ||||
|     }); | ||||
|     const myItemsLocatorTreeItem = createModalTree.getByRole('treeitem', { | ||||
|       name: myItemsFolderName | ||||
|     }); | ||||
|     await myItemsLocatorTreeItem.locator('.c-disclosure-triangle').click(); | ||||
|     await myItemsLocatorTreeItem.click(); | ||||
|  | ||||
|     const parentFolderLocatorTreeItem = createModalTree.getByRole('treeitem', { | ||||
|       name: parentFolder.name | ||||
|     }); | ||||
|     await parentFolderLocatorTreeItem.locator('.c-disclosure-triangle').click(); | ||||
|     await parentFolderLocatorTreeItem.click(); | ||||
|     await expect(page.locator('[aria-label="Save"]')).toBeDisabled(); | ||||
|  | ||||
|     const childFolderLocatorTreeItem = createModalTree.getByRole('treeitem', { | ||||
|       name: new RegExp(childFolder.name) | ||||
|     }); | ||||
|     await childFolderLocatorTreeItem.locator('.c-disclosure-triangle').click(); | ||||
|     await childFolderLocatorTreeItem.click(); | ||||
|     await expect(page.locator('[aria-label="Save"]')).toBeDisabled(); | ||||
|  | ||||
|     const grandchildFolderLocatorTreeItem = createModalTree.getByRole('treeitem', { | ||||
|       name: grandchildFolder.name | ||||
|     }); | ||||
|     await grandchildFolderLocatorTreeItem.locator('.c-disclosure-triangle').click(); | ||||
|     await grandchildFolderLocatorTreeItem.click(); | ||||
|     await expect(page.locator('[aria-label="Save"]')).toBeDisabled(); | ||||
|  | ||||
|     await parentFolderLocatorTreeItem.click(); | ||||
|     await expect(page.locator('[aria-label="Save"]')).toBeDisabled(); | ||||
|     await page.locator('[aria-label="Cancel"]').click(); | ||||
|  | ||||
|     // Move Child Folder from Parent Folder to My Items | ||||
|     await treePane | ||||
|       .getByRole('treeitem', { | ||||
|         name: new RegExp(childFolder.name) | ||||
|       }) | ||||
|       .click({ | ||||
|         button: 'right' | ||||
|       }); | ||||
|     await page | ||||
|       .getByRole('menuitem', { | ||||
|         name: /Move/ | ||||
|       }) | ||||
|       .click(); | ||||
|     await myItemsLocatorTreeItem.click(); | ||||
|  | ||||
|     await page.locator('[aria-label="Save"]').click(); | ||||
|     const myItemsPaneTreeItem = treePane.getByRole('treeitem', { | ||||
|       name: myItemsFolderName | ||||
|     }); | ||||
|  | ||||
|     // Expect that Child Folder is in My Items, the root folder | ||||
|     expect(myItemsPaneTreeItem.locator('nth=0:has(text=Child Folder)')).toBeTruthy(); | ||||
|   }); | ||||
|   test('Create a basic object and verify that it cannot be moved to telemetry object without Composition Provider', async ({ | ||||
|     page, | ||||
|     openmctConfig | ||||
|   }) => { | ||||
|     const { myItemsFolderName } = openmctConfig; | ||||
|  | ||||
|     // Go to Open MCT | ||||
|     await page.goto('./'); | ||||
|  | ||||
|     // Create Telemetry Table | ||||
|     let telemetryTable = 'Test Telemetry Table'; | ||||
|     await page.locator('button:has-text("Create")').click(); | ||||
|     await page.locator('li[role="menuitem"]:has-text("Telemetry Table")').click(); | ||||
|     await page.locator('text=Properties Title Notes >> input[type="text"]').click(); | ||||
|     await page.locator('text=Properties Title Notes >> input[type="text"]').fill(telemetryTable); | ||||
|  | ||||
|     await page.locator('button:has-text("OK")').click(); | ||||
|  | ||||
|     // Finish editing and save Telemetry Table | ||||
|     await page.locator('.c-button--menu.c-button--major.icon-save').click(); | ||||
|     await page.locator('text=Save and Finish Editing').click(); | ||||
|  | ||||
|     // Create New Folder Basic Domain Object | ||||
|     let folder = 'Test Folder'; | ||||
|     await page.locator('button:has-text("Create")').click(); | ||||
|     await page.locator('li[role="menuitem"]:has-text("Folder")').click(); | ||||
|     await page.locator('text=Properties Title Notes >> input[type="text"]').click(); | ||||
|     await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder); | ||||
|  | ||||
|     // See if it's possible to put the folder in the Telemetry object during creation (Soft Assert) | ||||
|     await page.locator(`form[name="mctForm"] >> text=${telemetryTable}`).click(); | ||||
|     let okButton = page.locator('button.c-button.c-button--major:has-text("OK")'); | ||||
|     let okButtonStateDisabled = await okButton.isDisabled(); | ||||
|     expect.soft(okButtonStateDisabled).toBeTruthy(); | ||||
|  | ||||
|     // Continue test regardless of assertion and create it in My Items | ||||
|     await page.locator(`form[name="mctForm"] >> text=${myItemsFolderName}`).click(); | ||||
|     await page.locator('button:has-text("OK")').click(); | ||||
|  | ||||
|     // Open My Items | ||||
|     await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click(); | ||||
|  | ||||
|     // Select Folder Object and select Move from context menu | ||||
|     await Promise.all([page.waitForNavigation(), page.locator(`a:has-text("${folder}")`).click()]); | ||||
|     await page | ||||
|       .locator('.c-tree__item.is-navigated-object .c-tree__item__label .c-tree__item__type-icon') | ||||
|       .click({ | ||||
|         button: 'right' | ||||
|       }); | ||||
|     await page.locator('li.icon-move').click(); | ||||
|  | ||||
|     // See if it's possible to put the folder in the Telemetry object after creation | ||||
|     await page.locator(`text=Location Open MCT ${myItemsFolderName} >> span`).nth(3).click(); | ||||
|     await page.locator(`form[name="mctForm"] >> text=${telemetryTable}`).click(); | ||||
|     let okButton2 = page.locator('button.c-button.c-button--major:has-text("OK")'); | ||||
|     let okButtonStateDisabled2 = await okButton2.isDisabled(); | ||||
|     expect(okButtonStateDisabled2).toBeTruthy(); | ||||
|   }); | ||||
|  | ||||
|   test('Create a basic object and verify that it can be linked to another folder', async ({ | ||||
|     page, | ||||
|     openmctConfig | ||||
|   }) => { | ||||
|     const { myItemsFolderName } = openmctConfig; | ||||
|  | ||||
|     // Go to Open MCT | ||||
|     await page.goto('./'); | ||||
|  | ||||
|     const parentFolder = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Folder', | ||||
|       name: 'Parent Folder' | ||||
|     }); | ||||
|     const childFolder = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Folder', | ||||
|       name: 'Child Folder', | ||||
|       parent: parentFolder.uuid | ||||
|     }); | ||||
|     const grandchildFolder = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Folder', | ||||
|       name: 'Grandchild Folder', | ||||
|       parent: childFolder.uuid | ||||
|     }); | ||||
|  | ||||
|     // Attempt to move parent to its own grandparent | ||||
|     await page.locator('button[title="Show selected item in tree"]').click(); | ||||
|  | ||||
|     const treePane = page.getByRole('tree', { | ||||
|       name: 'Main Tree' | ||||
|     }); | ||||
|     await treePane | ||||
|       .getByRole('treeitem', { | ||||
|         name: 'Parent Folder' | ||||
|       }) | ||||
|       .click({ | ||||
|         button: 'right' | ||||
|       }); | ||||
|  | ||||
|     await page | ||||
|       .getByRole('menuitem', { | ||||
|         name: /Move/ | ||||
|       }) | ||||
|       .click(); | ||||
|  | ||||
|     const createModalTree = page.getByRole('tree', { | ||||
|       name: 'Create Modal Tree' | ||||
|     }); | ||||
|     const myItemsLocatorTreeItem = createModalTree.getByRole('treeitem', { | ||||
|       name: myItemsFolderName | ||||
|     }); | ||||
|     await myItemsLocatorTreeItem.locator('.c-disclosure-triangle').click(); | ||||
|     await myItemsLocatorTreeItem.click(); | ||||
|  | ||||
|     const parentFolderLocatorTreeItem = createModalTree.getByRole('treeitem', { | ||||
|       name: parentFolder.name | ||||
|     }); | ||||
|     await parentFolderLocatorTreeItem.locator('.c-disclosure-triangle').click(); | ||||
|     await parentFolderLocatorTreeItem.click(); | ||||
|     await expect(page.locator('[aria-label="Save"]')).toBeDisabled(); | ||||
|  | ||||
|     const childFolderLocatorTreeItem = createModalTree.getByRole('treeitem', { | ||||
|       name: new RegExp(childFolder.name) | ||||
|     }); | ||||
|     await childFolderLocatorTreeItem.locator('.c-disclosure-triangle').click(); | ||||
|     await childFolderLocatorTreeItem.click(); | ||||
|     await expect(page.locator('[aria-label="Save"]')).toBeDisabled(); | ||||
|  | ||||
|     const grandchildFolderLocatorTreeItem = createModalTree.getByRole('treeitem', { | ||||
|       name: grandchildFolder.name | ||||
|     }); | ||||
|     await grandchildFolderLocatorTreeItem.locator('.c-disclosure-triangle').click(); | ||||
|     await grandchildFolderLocatorTreeItem.click(); | ||||
|     await expect(page.locator('[aria-label="Save"]')).toBeDisabled(); | ||||
|  | ||||
|     await parentFolderLocatorTreeItem.click(); | ||||
|     await expect(page.locator('[aria-label="Save"]')).toBeDisabled(); | ||||
|     await page.locator('[aria-label="Cancel"]').click(); | ||||
|  | ||||
|     // Move Child Folder from Parent Folder to My Items | ||||
|     await treePane | ||||
|       .getByRole('treeitem', { | ||||
|         name: new RegExp(childFolder.name) | ||||
|       }) | ||||
|       .click({ | ||||
|         button: 'right' | ||||
|       }); | ||||
|     await page | ||||
|       .getByRole('menuitem', { | ||||
|         name: /Link/ | ||||
|       }) | ||||
|       .click(); | ||||
|     await myItemsLocatorTreeItem.click(); | ||||
|  | ||||
|     await page.locator('[aria-label="Save"]').click(); | ||||
|     const myItemsPaneTreeItem = treePane.getByRole('treeitem', { | ||||
|       name: myItemsFolderName | ||||
|     }); | ||||
|  | ||||
|     // Expect that Child Folder is in My Items, the root folder | ||||
|     expect(myItemsPaneTreeItem.locator('nth=0:has(text=Child Folder)')).toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| test.fixme( | ||||
|   'Cannot move a previously created domain object to non-peristable object in Move Modal', | ||||
|   async ({ page }) => { | ||||
|     //Create a domain object | ||||
|     //Save Domain object | ||||
|     //Move Object and verify that cannot select non-persistable object | ||||
|     //Move Object to My Items | ||||
|     //Verify successful move | ||||
|   } | ||||
| ); | ||||
|   | ||||
| @@ -28,85 +28,91 @@ const { createDomainObjectWithDefaults, createNotification } = require('../../ap | ||||
| const { test, expect } = require('../../pluginFixtures'); | ||||
|  | ||||
| test.describe('Notifications List', () => { | ||||
|     test('Notifications can be dismissed individually', async ({ page }) => { | ||||
|         test.info().annotations.push({ | ||||
|             type: 'issue', | ||||
|             description: 'https://github.com/nasa/openmct/issues/6122' | ||||
|         }); | ||||
|  | ||||
|         // Go to baseURL | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|  | ||||
|         // Create an error notification with the message "Error message" | ||||
|         await createNotification(page, { | ||||
|             severity: 'error', | ||||
|             message: 'Error message' | ||||
|         }); | ||||
|  | ||||
|         // Create an alert notification with the message "Alert message" | ||||
|         await createNotification(page, { | ||||
|             severity: 'alert', | ||||
|             message: 'Alert message' | ||||
|         }); | ||||
|  | ||||
|         // Verify that there is a button with aria-label "Review 2 Notifications" | ||||
|         expect(await page.locator('button[aria-label="Review 2 Notifications"]').count()).toBe(1); | ||||
|  | ||||
|         // Click on button with aria-label "Review 2 Notifications" | ||||
|         await page.click('button[aria-label="Review 2 Notifications"]'); | ||||
|  | ||||
|         // Click on button with aria-label="Dismiss notification of Error message" | ||||
|         await page.click('button[aria-label="Dismiss notification of Error message"]'); | ||||
|  | ||||
|         // Verify there is no a notification (listitem) with the text "Error message" since it was dismissed | ||||
|         expect(await page.locator('div[role="dialog"] div[role="listitem"]').innerText()).not.toContain('Error message'); | ||||
|  | ||||
|         // Verify there is still a notification (listitem) with the text "Alert message" | ||||
|         expect(await page.locator('div[role="dialog"] div[role="listitem"]').innerText()).toContain('Alert message'); | ||||
|  | ||||
|         // Click on button with aria-label="Dismiss notification of Alert message" | ||||
|         await page.click('button[aria-label="Dismiss notification of Alert message"]'); | ||||
|  | ||||
|         // Verify that there is no dialog since the notification overlay was closed automatically after all notifications were dismissed | ||||
|         expect(await page.locator('div[role="dialog"]').count()).toBe(0); | ||||
|   test('Notifications can be dismissed individually', async ({ page }) => { | ||||
|     test.info().annotations.push({ | ||||
|       type: 'issue', | ||||
|       description: 'https://github.com/nasa/openmct/issues/6122' | ||||
|     }); | ||||
|  | ||||
|     // Go to baseURL | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|  | ||||
|     // Create an error notification with the message "Error message" | ||||
|     await createNotification(page, { | ||||
|       severity: 'error', | ||||
|       message: 'Error message' | ||||
|     }); | ||||
|  | ||||
|     // Create an alert notification with the message "Alert message" | ||||
|     await createNotification(page, { | ||||
|       severity: 'alert', | ||||
|       message: 'Alert message' | ||||
|     }); | ||||
|  | ||||
|     // Verify that there is a button with aria-label "Review 2 Notifications" | ||||
|     expect(await page.locator('button[aria-label="Review 2 Notifications"]').count()).toBe(1); | ||||
|  | ||||
|     // Click on button with aria-label "Review 2 Notifications" | ||||
|     await page.click('button[aria-label="Review 2 Notifications"]'); | ||||
|  | ||||
|     // Click on button with aria-label="Dismiss notification of Error message" | ||||
|     await page.click('button[aria-label="Dismiss notification of Error message"]'); | ||||
|  | ||||
|     // Verify there is no a notification (listitem) with the text "Error message" since it was dismissed | ||||
|     expect(await page.locator('div[role="dialog"] div[role="listitem"]').innerText()).not.toContain( | ||||
|       'Error message' | ||||
|     ); | ||||
|  | ||||
|     // Verify there is still a notification (listitem) with the text "Alert message" | ||||
|     expect(await page.locator('div[role="dialog"] div[role="listitem"]').innerText()).toContain( | ||||
|       'Alert message' | ||||
|     ); | ||||
|  | ||||
|     // Click on button with aria-label="Dismiss notification of Alert message" | ||||
|     await page.click('button[aria-label="Dismiss notification of Alert message"]'); | ||||
|  | ||||
|     // Verify that there is no dialog since the notification overlay was closed automatically after all notifications were dismissed | ||||
|     expect(await page.locator('div[role="dialog"]').count()).toBe(0); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| test.describe('Notification Overlay', () => { | ||||
|     test('Closing notification list after notification banner disappeared does not cause it to open automatically', async ({ page }) => { | ||||
|         test.info().annotations.push({ | ||||
|             type: 'issue', | ||||
|             description: 'https://github.com/nasa/openmct/issues/6130' | ||||
|         }); | ||||
|  | ||||
|         // Go to baseURL | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|  | ||||
|         // Create a new Display Layout object | ||||
|         await createDomainObjectWithDefaults(page, { type: 'Display Layout' }); | ||||
|  | ||||
|         // Click on the button "Review 1 Notification" | ||||
|         await page.click('button[aria-label="Review 1 Notification"]'); | ||||
|  | ||||
|         // Verify that Notification List is open | ||||
|         expect(await page.locator('div[role="dialog"]').isVisible()).toBe(true); | ||||
|  | ||||
|         // Wait until there is no Notification Banner | ||||
|         await page.waitForSelector('div[role="alert"]', { state: 'detached'}); | ||||
|  | ||||
|         // Click on the "Close" button of the Notification List | ||||
|         await page.click('button[aria-label="Close"]'); | ||||
|  | ||||
|         // On the Display Layout object, click on the "Edit" button | ||||
|         await page.click('button[title="Edit"]'); | ||||
|  | ||||
|         // Click on the "Save" button | ||||
|         await page.click('button[title="Save"]'); | ||||
|  | ||||
|         // Click on the "Save and Finish Editing" option | ||||
|         await page.click('li[title="Save and Finish Editing"]'); | ||||
|  | ||||
|         // Verify that Notification List is NOT open | ||||
|         expect(await page.locator('div[role="dialog"]').isVisible()).toBe(false); | ||||
|   test('Closing notification list after notification banner disappeared does not cause it to open automatically', async ({ | ||||
|     page | ||||
|   }) => { | ||||
|     test.info().annotations.push({ | ||||
|       type: 'issue', | ||||
|       description: 'https://github.com/nasa/openmct/issues/6130' | ||||
|     }); | ||||
|  | ||||
|     // Go to baseURL | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|  | ||||
|     // Create a new Display Layout object | ||||
|     await createDomainObjectWithDefaults(page, { type: 'Display Layout' }); | ||||
|  | ||||
|     // Click on the button "Review 1 Notification" | ||||
|     await page.click('button[aria-label="Review 1 Notification"]'); | ||||
|  | ||||
|     // Verify that Notification List is open | ||||
|     expect(await page.locator('div[role="dialog"]').isVisible()).toBe(true); | ||||
|  | ||||
|     // Wait until there is no Notification Banner | ||||
|     await page.waitForSelector('div[role="alert"]', { state: 'detached' }); | ||||
|  | ||||
|     // Click on the "Close" button of the Notification List | ||||
|     await page.click('button[aria-label="Close"]'); | ||||
|  | ||||
|     // On the Display Layout object, click on the "Edit" button | ||||
|     await page.click('button[title="Edit"]'); | ||||
|  | ||||
|     // Click on the "Save" button | ||||
|     await page.click('button[title="Save"]'); | ||||
|  | ||||
|     // Click on the "Save and Finish Editing" option | ||||
|     await page.click('li[title="Save and Finish Editing"]'); | ||||
|  | ||||
|     // Verify that Notification List is NOT open | ||||
|     expect(await page.locator('div[role="dialog"]').isVisible()).toBe(false); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -20,66 +20,108 @@ | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
| const { test, expect } = require('../../../pluginFixtures'); | ||||
| const { createPlanFromJSON, createDomainObjectWithDefaults, selectInspectorTab } = require('../../../appActions'); | ||||
| const { | ||||
|   createPlanFromJSON, | ||||
|   createDomainObjectWithDefaults, | ||||
|   selectInspectorTab | ||||
| } = require('../../../appActions'); | ||||
| const testPlan1 = require('../../../test-data/examplePlans/ExamplePlan_Small1.json'); | ||||
| const testPlan2 = require('../../../test-data/examplePlans/ExamplePlan_Small2.json'); | ||||
| const { assertPlanActivities, setBoundsToSpanAllActivities } = require('../../../helper/planningUtils'); | ||||
| const { | ||||
|   assertPlanActivities, | ||||
|   setBoundsToSpanAllActivities | ||||
| } = require('../../../helper/planningUtils'); | ||||
| const { getPreciseDuration } = require('../../../../src/utils/duration'); | ||||
|  | ||||
| test.describe("Gantt Chart", () => { | ||||
|     let ganttChart; | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|         ganttChart = await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Gantt Chart' | ||||
|         }); | ||||
|         await createPlanFromJSON(page, { | ||||
|             json: testPlan1, | ||||
|             parent: ganttChart.uuid | ||||
|         }); | ||||
| test.describe('Gantt Chart', () => { | ||||
|   let ganttChart; | ||||
|   let plan; | ||||
|   test.beforeEach(async ({ page }) => { | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|     ganttChart = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Gantt Chart' | ||||
|     }); | ||||
|     plan = await createPlanFromJSON(page, { | ||||
|       json: testPlan1, | ||||
|       parent: ganttChart.uuid | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   test('Displays all plan events', async ({ page }) => { | ||||
|     await page.goto(ganttChart.url); | ||||
|  | ||||
|     await assertPlanActivities(page, testPlan1, ganttChart.url); | ||||
|   }); | ||||
|   test('Replaces a plan with a new plan', async ({ page }) => { | ||||
|     await assertPlanActivities(page, testPlan1, ganttChart.url); | ||||
|     await createPlanFromJSON(page, { | ||||
|       json: testPlan2, | ||||
|       parent: ganttChart.uuid | ||||
|     }); | ||||
|     const replaceModal = page | ||||
|       .getByRole('dialog') | ||||
|       .filter({ hasText: 'This action will replace the current Plan. Do you want to continue?' }); | ||||
|     await expect(replaceModal).toBeVisible(); | ||||
|     await page.getByRole('button', { name: 'OK' }).click(); | ||||
|  | ||||
|     await assertPlanActivities(page, testPlan2, ganttChart.url); | ||||
|   }); | ||||
|   test('Can select a single activity and display its details in the inspector', async ({ | ||||
|     page | ||||
|   }) => { | ||||
|     test.slow(); | ||||
|     await page.goto(ganttChart.url); | ||||
|  | ||||
|     await setBoundsToSpanAllActivities(page, testPlan1, ganttChart.url); | ||||
|  | ||||
|     const activities = Object.values(testPlan1).flat(); | ||||
|     const activity = activities[0]; | ||||
|     await page | ||||
|       .locator('g') | ||||
|       .filter({ hasText: new RegExp(activity.name) }) | ||||
|       .click(); | ||||
|     await selectInspectorTab(page, 'Activity'); | ||||
|  | ||||
|     const startDateTime = await page | ||||
|       .locator( | ||||
|         '.c-inspect-properties__label:has-text("Start DateTime")+.c-inspect-properties__value' | ||||
|       ) | ||||
|       .innerText(); | ||||
|     const endDateTime = await page | ||||
|       .locator('.c-inspect-properties__label:has-text("End DateTime")+.c-inspect-properties__value') | ||||
|       .innerText(); | ||||
|     const duration = await page | ||||
|       .locator('.c-inspect-properties__label:has-text("duration")+.c-inspect-properties__value') | ||||
|       .innerText(); | ||||
|  | ||||
|     const expectedStartDate = new Date(activity.start).toISOString(); | ||||
|     const actualStartDate = new Date(startDateTime).toISOString(); | ||||
|     const expectedEndDate = new Date(activity.end).toISOString(); | ||||
|     const actualEndDate = new Date(endDateTime).toISOString(); | ||||
|     const expectedDuration = getPreciseDuration(activity.end - activity.start); | ||||
|     const actualDuration = duration; | ||||
|  | ||||
|     expect(expectedStartDate).toEqual(actualStartDate); | ||||
|     expect(expectedEndDate).toEqual(actualEndDate); | ||||
|     expect(expectedDuration).toEqual(actualDuration); | ||||
|   }); | ||||
|   test("Displays a Plan's draft status", async ({ page }) => { | ||||
|     test.info().annotations.push({ | ||||
|       type: 'issue', | ||||
|       description: 'https://github.com/nasa/openmct/issues/6641' | ||||
|     }); | ||||
|  | ||||
|     test("Displays all plan events", async ({ page }) => { | ||||
|         await page.goto(ganttChart.url); | ||||
|     // Mark the Plan's status as draft in the OpenMCT API | ||||
|     await page.evaluate(async (planObject) => { | ||||
|       await window.openmct.status.set(planObject.uuid, 'draft'); | ||||
|     }, plan); | ||||
|  | ||||
|         await assertPlanActivities(page, testPlan1, ganttChart.url); | ||||
|     }); | ||||
|     test("Replaces a plan with a new plan", async ({ page }) => { | ||||
|         await assertPlanActivities(page, testPlan1, ganttChart.url); | ||||
|         await createPlanFromJSON(page, { | ||||
|             json: testPlan2, | ||||
|             parent: ganttChart.uuid | ||||
|         }); | ||||
|         const replaceModal = page.getByRole('dialog').filter({ hasText: "This action will replace the current Plan. Do you want to continue?" }); | ||||
|         await expect(replaceModal).toBeVisible(); | ||||
|         await page.getByRole('button', { name: 'OK' }).click(); | ||||
|     // Navigate to the Gantt Chart | ||||
|     await page.goto(ganttChart.url); | ||||
|  | ||||
|         await assertPlanActivities(page, testPlan2, ganttChart.url); | ||||
|     }); | ||||
|     test("Can select a single activity and display its details in the inspector", async ({ page }) => { | ||||
|         test.slow(); | ||||
|         await page.goto(ganttChart.url); | ||||
|  | ||||
|         await setBoundsToSpanAllActivities(page, testPlan1, ganttChart.url); | ||||
|  | ||||
|         const activities = Object.values(testPlan1).flat(); | ||||
|         const activity = activities[0]; | ||||
|         await page.locator('g').filter({ hasText: new RegExp(activity.name) }).click(); | ||||
|         await selectInspectorTab(page, 'Activity'); | ||||
|  | ||||
|         const startDateTime = await page.locator('.c-inspect-properties__label:has-text("Start DateTime")+.c-inspect-properties__value').innerText(); | ||||
|         const endDateTime = await page.locator('.c-inspect-properties__label:has-text("End DateTime")+.c-inspect-properties__value').innerText(); | ||||
|         const duration = await page.locator('.c-inspect-properties__label:has-text("duration")+.c-inspect-properties__value').innerText(); | ||||
|  | ||||
|         const expectedStartDate = new Date(activity.start).toISOString(); | ||||
|         const actualStartDate = new Date(startDateTime).toISOString(); | ||||
|         const expectedEndDate = new Date(activity.end).toISOString(); | ||||
|         const actualEndDate = new Date(endDateTime).toISOString(); | ||||
|         const expectedDuration = getPreciseDuration(activity.end - activity.start); | ||||
|         const actualDuration = duration; | ||||
|  | ||||
|         expect(expectedStartDate).toEqual(actualStartDate); | ||||
|         expect(expectedEndDate).toEqual(actualEndDate); | ||||
|         expect(expectedDuration).toEqual(actualDuration); | ||||
|     }); | ||||
|     // Assert that the Plan's status is displayed as draft | ||||
|     expect(await page.locator('.u-contents.c-swimlane.is-status--draft').count()).toBe( | ||||
|       Object.keys(testPlan1).length | ||||
|     ); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -24,16 +24,16 @@ const { createPlanFromJSON } = require('../../../appActions'); | ||||
| const testPlan1 = require('../../../test-data/examplePlans/ExamplePlan_Small1.json'); | ||||
| const { assertPlanActivities } = require('../../../helper/planningUtils'); | ||||
|  | ||||
| test.describe("Plan", () => { | ||||
|     let plan; | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|         plan = await createPlanFromJSON(page, { | ||||
|             json: testPlan1 | ||||
|         }); | ||||
| test.describe('Plan', () => { | ||||
|   let plan; | ||||
|   test.beforeEach(async ({ page }) => { | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|     plan = await createPlanFromJSON(page, { | ||||
|       json: testPlan1 | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|     test("Displays all plan events", async ({ page }) => { | ||||
|         await assertPlanActivities(page, testPlan1, plan.url); | ||||
|     }); | ||||
|   test('Displays all plan events', async ({ page }) => { | ||||
|     await assertPlanActivities(page, testPlan1, plan.url); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -24,158 +24,164 @@ const { test, expect } = require('../../../pluginFixtures'); | ||||
| const { createDomainObjectWithDefaults, createPlanFromJSON } = require('../../../appActions'); | ||||
|  | ||||
| const testPlan = { | ||||
|     "TEST_GROUP": [ | ||||
|         { | ||||
|             "name": "Past event 1", | ||||
|             "start": 1660320408000, | ||||
|             "end": 1660343797000, | ||||
|             "type": "TEST-GROUP", | ||||
|             "color": "orange", | ||||
|             "textColor": "white" | ||||
|         }, | ||||
|         { | ||||
|             "name": "Past event 2", | ||||
|             "start": 1660406808000, | ||||
|             "end": 1660429160000, | ||||
|             "type": "TEST-GROUP", | ||||
|             "color": "orange", | ||||
|             "textColor": "white" | ||||
|         }, | ||||
|         { | ||||
|             "name": "Past event 3", | ||||
|             "start": 1660493208000, | ||||
|             "end": 1660503981000, | ||||
|             "type": "TEST-GROUP", | ||||
|             "color": "orange", | ||||
|             "textColor": "white" | ||||
|         }, | ||||
|         { | ||||
|             "name": "Past event 4", | ||||
|             "start": 1660579608000, | ||||
|             "end": 1660624108000, | ||||
|             "type": "TEST-GROUP", | ||||
|             "color": "orange", | ||||
|             "textColor": "white" | ||||
|         }, | ||||
|         { | ||||
|             "name": "Past event 5", | ||||
|             "start": 1660666008000, | ||||
|             "end": 1660681529000, | ||||
|             "type": "TEST-GROUP", | ||||
|             "color": "orange", | ||||
|             "textColor": "white" | ||||
|         } | ||||
|     ] | ||||
|   TEST_GROUP: [ | ||||
|     { | ||||
|       name: 'Past event 1', | ||||
|       start: 1660320408000, | ||||
|       end: 1660343797000, | ||||
|       type: 'TEST-GROUP', | ||||
|       color: 'orange', | ||||
|       textColor: 'white' | ||||
|     }, | ||||
|     { | ||||
|       name: 'Past event 2', | ||||
|       start: 1660406808000, | ||||
|       end: 1660429160000, | ||||
|       type: 'TEST-GROUP', | ||||
|       color: 'orange', | ||||
|       textColor: 'white' | ||||
|     }, | ||||
|     { | ||||
|       name: 'Past event 3', | ||||
|       start: 1660493208000, | ||||
|       end: 1660503981000, | ||||
|       type: 'TEST-GROUP', | ||||
|       color: 'orange', | ||||
|       textColor: 'white' | ||||
|     }, | ||||
|     { | ||||
|       name: 'Past event 4', | ||||
|       start: 1660579608000, | ||||
|       end: 1660624108000, | ||||
|       type: 'TEST-GROUP', | ||||
|       color: 'orange', | ||||
|       textColor: 'white' | ||||
|     }, | ||||
|     { | ||||
|       name: 'Past event 5', | ||||
|       start: 1660666008000, | ||||
|       end: 1660681529000, | ||||
|       type: 'TEST-GROUP', | ||||
|       color: 'orange', | ||||
|       textColor: 'white' | ||||
|     } | ||||
|   ] | ||||
| }; | ||||
|  | ||||
| test.describe("Time Strip", () => { | ||||
|     test("Create two Time Strips, add a single Plan to both, and verify they can have separate Indepdenent Time Contexts @unstable", async ({ page }) => { | ||||
|         test.info().annotations.push({ | ||||
|             type: 'issue', | ||||
|             description: 'https://github.com/nasa/openmct/issues/5627' | ||||
|         }); | ||||
|  | ||||
|         // Constant locators | ||||
|         const independentTimeConductorInputs = page.locator('.l-shell__main-independent-time-conductor .c-input--datetime'); | ||||
|         const activityBounds = page.locator('.activity-bounds'); | ||||
|  | ||||
|         // Goto baseURL | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|  | ||||
|         const timestrip = await test.step("Create a Time Strip", async () => { | ||||
|             const createdTimeStrip = await createDomainObjectWithDefaults(page, { type: 'Time Strip' }); | ||||
|             const objectName = await page.locator('.l-browse-bar__object-name').innerText(); | ||||
|             expect(objectName).toBe(createdTimeStrip.name); | ||||
|  | ||||
|             return createdTimeStrip; | ||||
|         }); | ||||
|  | ||||
|         const plan = await test.step("Create a Plan and add it to the timestrip", async () => { | ||||
|             const createdPlan = await createPlanFromJSON(page, { | ||||
|                 name: 'Test Plan', | ||||
|                 json: testPlan | ||||
|             }); | ||||
|  | ||||
|             await page.goto(timestrip.url); | ||||
|             // Expand the tree to show the plan | ||||
|             await page.click("button[title='Show selected item in tree']"); | ||||
|             await page.dragAndDrop(`role=treeitem[name=/${createdPlan.name}/]`, '.c-object-view'); | ||||
|             await page.click("button[title='Save']"); | ||||
|             await page.click("li[title='Save and Finish Editing']"); | ||||
|             const startBound = testPlan.TEST_GROUP[0].start; | ||||
|             const endBound = testPlan.TEST_GROUP[testPlan.TEST_GROUP.length - 1].end; | ||||
|  | ||||
|             // Switch to fixed time mode with all plan events within the bounds | ||||
|             await page.goto(`${timestrip.url}?tc.mode=fixed&tc.startBound=${startBound}&tc.endBound=${endBound}&tc.timeSystem=utc&view=time-strip.view`); | ||||
|  | ||||
|             // Verify all events are displayed | ||||
|             const eventCount = await page.locator('.activity-bounds').count(); | ||||
|             expect(eventCount).toEqual(testPlan.TEST_GROUP.length); | ||||
|  | ||||
|             return createdPlan; | ||||
|         }); | ||||
|  | ||||
|         await test.step("TimeStrip can use the Independent Time Conductor", async () => { | ||||
|             // Activate Independent Time Conductor in Fixed Time Mode | ||||
|             await page.click('.c-toggle-switch__slider'); | ||||
|             expect(await activityBounds.count()).toEqual(0); | ||||
|  | ||||
|             // Set the independent time bounds so that only one event is shown | ||||
|             const startBound = testPlan.TEST_GROUP[0].start; | ||||
|             const endBound = testPlan.TEST_GROUP[0].end; | ||||
|             const startBoundString = new Date(startBound).toISOString().replace('T', ' '); | ||||
|             const endBoundString = new Date(endBound).toISOString().replace('T', ' '); | ||||
|  | ||||
|             await independentTimeConductorInputs.nth(0).fill(''); | ||||
|             await independentTimeConductorInputs.nth(0).fill(startBoundString); | ||||
|             await page.keyboard.press('Enter'); | ||||
|             await independentTimeConductorInputs.nth(1).fill(''); | ||||
|             await independentTimeConductorInputs.nth(1).fill(endBoundString); | ||||
|             await page.keyboard.press('Enter'); | ||||
|             expect(await activityBounds.count()).toEqual(1); | ||||
|         }); | ||||
|  | ||||
|         await test.step("Can have multiple TimeStrips with the same plan linked and different Independent Time Contexts", async () => { | ||||
|             // Create another Time Strip and verify that it has been created | ||||
|             const createdTimeStrip = await createDomainObjectWithDefaults(page, { | ||||
|                 type: 'Time Strip', | ||||
|                 name: "Another Time Strip" | ||||
|             }); | ||||
|  | ||||
|             const objectName = await page.locator('.l-browse-bar__object-name').innerText(); | ||||
|             expect(objectName).toBe(createdTimeStrip.name); | ||||
|  | ||||
|             // Drag the existing Plan onto the newly created Time Strip, and save. | ||||
|             await page.dragAndDrop(`role=treeitem[name=/${plan.name}/]`, '.c-object-view'); | ||||
|             await page.click("button[title='Save']"); | ||||
|             await page.click("li[title='Save and Finish Editing']"); | ||||
|  | ||||
|             // Activate Independent Time Conductor in Fixed Time Mode | ||||
|             await page.click('.c-toggle-switch__slider'); | ||||
|  | ||||
|             // All events should be displayed at this point because the | ||||
|             // initial independent context bounds will match the global bounds | ||||
|             expect(await activityBounds.count()).toEqual(5); | ||||
|  | ||||
|             // Set the independent time bounds so that two events are shown | ||||
|             const startBound = testPlan.TEST_GROUP[0].start; | ||||
|             const endBound = testPlan.TEST_GROUP[1].end; | ||||
|             const startBoundString = new Date(startBound).toISOString().replace('T', ' '); | ||||
|             const endBoundString = new Date(endBound).toISOString().replace('T', ' '); | ||||
|  | ||||
|             await independentTimeConductorInputs.nth(0).fill(''); | ||||
|             await independentTimeConductorInputs.nth(0).fill(startBoundString); | ||||
|             await page.keyboard.press('Enter'); | ||||
|             await independentTimeConductorInputs.nth(1).fill(''); | ||||
|             await independentTimeConductorInputs.nth(1).fill(endBoundString); | ||||
|             await page.keyboard.press('Enter'); | ||||
|  | ||||
|             // Verify that two events are displayed | ||||
|             expect(await activityBounds.count()).toEqual(2); | ||||
|  | ||||
|             // Switch to the previous Time Strip and verify that only one event is displayed | ||||
|             await page.goto(timestrip.url); | ||||
|             expect(await activityBounds.count()).toEqual(1); | ||||
|         }); | ||||
| test.describe('Time Strip', () => { | ||||
|   test('Create two Time Strips, add a single Plan to both, and verify they can have separate Indepdenent Time Contexts @unstable', async ({ | ||||
|     page | ||||
|   }) => { | ||||
|     test.info().annotations.push({ | ||||
|       type: 'issue', | ||||
|       description: 'https://github.com/nasa/openmct/issues/5627' | ||||
|     }); | ||||
|  | ||||
|     // Constant locators | ||||
|     const independentTimeConductorInputs = page.locator( | ||||
|       '.l-shell__main-independent-time-conductor .c-input--datetime' | ||||
|     ); | ||||
|     const activityBounds = page.locator('.activity-bounds'); | ||||
|  | ||||
|     // Goto baseURL | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|  | ||||
|     const timestrip = await test.step('Create a Time Strip', async () => { | ||||
|       const createdTimeStrip = await createDomainObjectWithDefaults(page, { type: 'Time Strip' }); | ||||
|       const objectName = await page.locator('.l-browse-bar__object-name').innerText(); | ||||
|       expect(objectName).toBe(createdTimeStrip.name); | ||||
|  | ||||
|       return createdTimeStrip; | ||||
|     }); | ||||
|  | ||||
|     const plan = await test.step('Create a Plan and add it to the timestrip', async () => { | ||||
|       const createdPlan = await createPlanFromJSON(page, { | ||||
|         name: 'Test Plan', | ||||
|         json: testPlan | ||||
|       }); | ||||
|  | ||||
|       await page.goto(timestrip.url); | ||||
|       // Expand the tree to show the plan | ||||
|       await page.click("button[title='Show selected item in tree']"); | ||||
|       await page.dragAndDrop(`role=treeitem[name=/${createdPlan.name}/]`, '.c-object-view'); | ||||
|       await page.click("button[title='Save']"); | ||||
|       await page.click("li[title='Save and Finish Editing']"); | ||||
|       const startBound = testPlan.TEST_GROUP[0].start; | ||||
|       const endBound = testPlan.TEST_GROUP[testPlan.TEST_GROUP.length - 1].end; | ||||
|  | ||||
|       // Switch to fixed time mode with all plan events within the bounds | ||||
|       await page.goto( | ||||
|         `${timestrip.url}?tc.mode=fixed&tc.startBound=${startBound}&tc.endBound=${endBound}&tc.timeSystem=utc&view=time-strip.view` | ||||
|       ); | ||||
|  | ||||
|       // Verify all events are displayed | ||||
|       const eventCount = await page.locator('.activity-bounds').count(); | ||||
|       expect(eventCount).toEqual(testPlan.TEST_GROUP.length); | ||||
|  | ||||
|       return createdPlan; | ||||
|     }); | ||||
|  | ||||
|     await test.step('TimeStrip can use the Independent Time Conductor', async () => { | ||||
|       // Activate Independent Time Conductor in Fixed Time Mode | ||||
|       await page.click('.c-toggle-switch__slider'); | ||||
|       expect(await activityBounds.count()).toEqual(0); | ||||
|  | ||||
|       // Set the independent time bounds so that only one event is shown | ||||
|       const startBound = testPlan.TEST_GROUP[0].start; | ||||
|       const endBound = testPlan.TEST_GROUP[0].end; | ||||
|       const startBoundString = new Date(startBound).toISOString().replace('T', ' '); | ||||
|       const endBoundString = new Date(endBound).toISOString().replace('T', ' '); | ||||
|  | ||||
|       await independentTimeConductorInputs.nth(0).fill(''); | ||||
|       await independentTimeConductorInputs.nth(0).fill(startBoundString); | ||||
|       await page.keyboard.press('Enter'); | ||||
|       await independentTimeConductorInputs.nth(1).fill(''); | ||||
|       await independentTimeConductorInputs.nth(1).fill(endBoundString); | ||||
|       await page.keyboard.press('Enter'); | ||||
|       expect(await activityBounds.count()).toEqual(1); | ||||
|     }); | ||||
|  | ||||
|     await test.step('Can have multiple TimeStrips with the same plan linked and different Independent Time Contexts', async () => { | ||||
|       // Create another Time Strip and verify that it has been created | ||||
|       const createdTimeStrip = await createDomainObjectWithDefaults(page, { | ||||
|         type: 'Time Strip', | ||||
|         name: 'Another Time Strip' | ||||
|       }); | ||||
|  | ||||
|       const objectName = await page.locator('.l-browse-bar__object-name').innerText(); | ||||
|       expect(objectName).toBe(createdTimeStrip.name); | ||||
|  | ||||
|       // Drag the existing Plan onto the newly created Time Strip, and save. | ||||
|       await page.dragAndDrop(`role=treeitem[name=/${plan.name}/]`, '.c-object-view'); | ||||
|       await page.click("button[title='Save']"); | ||||
|       await page.click("li[title='Save and Finish Editing']"); | ||||
|  | ||||
|       // Activate Independent Time Conductor in Fixed Time Mode | ||||
|       await page.click('.c-toggle-switch__slider'); | ||||
|  | ||||
|       // All events should be displayed at this point because the | ||||
|       // initial independent context bounds will match the global bounds | ||||
|       expect(await activityBounds.count()).toEqual(5); | ||||
|  | ||||
|       // Set the independent time bounds so that two events are shown | ||||
|       const startBound = testPlan.TEST_GROUP[0].start; | ||||
|       const endBound = testPlan.TEST_GROUP[1].end; | ||||
|       const startBoundString = new Date(startBound).toISOString().replace('T', ' '); | ||||
|       const endBoundString = new Date(endBound).toISOString().replace('T', ' '); | ||||
|  | ||||
|       await independentTimeConductorInputs.nth(0).fill(''); | ||||
|       await independentTimeConductorInputs.nth(0).fill(startBoundString); | ||||
|       await page.keyboard.press('Enter'); | ||||
|       await independentTimeConductorInputs.nth(1).fill(''); | ||||
|       await independentTimeConductorInputs.nth(1).fill(endBoundString); | ||||
|       await page.keyboard.press('Enter'); | ||||
|  | ||||
|       // Verify that two events are displayed | ||||
|       expect(await activityBounds.count()).toEqual(2); | ||||
|  | ||||
|       // Switch to the previous Time Strip and verify that only one event is displayed | ||||
|       await page.goto(timestrip.url); | ||||
|       expect(await activityBounds.count()).toEqual(1); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -27,40 +27,40 @@ This test suite is dedicated to tests which verify the basic operations surround | ||||
| const { test, expect } = require('../../../../baseFixtures'); | ||||
|  | ||||
| test.describe('Clock Generator CRUD Operations', () => { | ||||
|  | ||||
|     test('Timezone dropdown will collapse when clicked outside or on dropdown icon again', async ({ page }) => { | ||||
|         test.info().annotations.push({ | ||||
|             type: 'issue', | ||||
|             description: 'https://github.com/nasa/openmct/issues/4878' | ||||
|         }); | ||||
|         //Go to baseURL | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|  | ||||
|         //Click the Create button | ||||
|         await page.click('button:has-text("Create")'); | ||||
|  | ||||
|         // Click Clock | ||||
|         await page.click('text=Clock'); | ||||
|  | ||||
|         // Click .icon-arrow-down | ||||
|         await page.locator('.icon-arrow-down').click(); | ||||
|         //verify if the autocomplete dropdown is visible | ||||
|         await expect(page.locator(".c-input--autocomplete__options")).toBeVisible(); | ||||
|         // Click .icon-arrow-down | ||||
|         await page.locator('.icon-arrow-down').click(); | ||||
|  | ||||
|         // Verify clicking on the autocomplete arrow collapses the dropdown | ||||
|         await expect(page.locator(".c-input--autocomplete__options")).toBeHidden(); | ||||
|  | ||||
|         // Click timezone input to open dropdown | ||||
|         await page.locator('.c-input--autocomplete__input').click(); | ||||
|         //verify if the autocomplete dropdown is visible | ||||
|         await expect(page.locator(".c-input--autocomplete__options")).toBeVisible(); | ||||
|  | ||||
|         // Verify clicking outside the autocomplete dropdown collapses it | ||||
|         await page.locator('text=Timezone').click(); | ||||
|         // Verify clicking on the autocomplete arrow collapses the dropdown | ||||
|         await expect(page.locator(".c-input--autocomplete__options")).toBeHidden(); | ||||
|  | ||||
|   test('Timezone dropdown will collapse when clicked outside or on dropdown icon again', async ({ | ||||
|     page | ||||
|   }) => { | ||||
|     test.info().annotations.push({ | ||||
|       type: 'issue', | ||||
|       description: 'https://github.com/nasa/openmct/issues/4878' | ||||
|     }); | ||||
|     //Go to baseURL | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|  | ||||
|     //Click the Create button | ||||
|     await page.click('button:has-text("Create")'); | ||||
|  | ||||
|     // Click Clock | ||||
|     await page.click('text=Clock'); | ||||
|  | ||||
|     // Click .icon-arrow-down | ||||
|     await page.locator('.icon-arrow-down').click(); | ||||
|     //verify if the autocomplete dropdown is visible | ||||
|     await expect(page.locator('.c-input--autocomplete__options')).toBeVisible(); | ||||
|     // Click .icon-arrow-down | ||||
|     await page.locator('.icon-arrow-down').click(); | ||||
|  | ||||
|     // Verify clicking on the autocomplete arrow collapses the dropdown | ||||
|     await expect(page.locator('.c-input--autocomplete__options')).toBeHidden(); | ||||
|  | ||||
|     // Click timezone input to open dropdown | ||||
|     await page.locator('.c-input--autocomplete__input').click(); | ||||
|     //verify if the autocomplete dropdown is visible | ||||
|     await expect(page.locator('.c-input--autocomplete__options')).toBeVisible(); | ||||
|  | ||||
|     // Verify clicking outside the autocomplete dropdown collapses it | ||||
|     await page.locator('text=Timezone').click(); | ||||
|     // Verify clicking on the autocomplete arrow collapses the dropdown | ||||
|     await expect(page.locator('.c-input--autocomplete__options')).toBeHidden(); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -25,17 +25,17 @@ | ||||
| const { test, expect } = require('../../../../baseFixtures'); | ||||
|  | ||||
| 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 | ||||
|   // 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 | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -33,293 +33,336 @@ let conditionSetUrl; | ||||
| let getConditionSetIdentifierFromUrl; | ||||
|  | ||||
| test.describe.serial('Condition Set CRUD Operations on @localStorage', () => { | ||||
|     test.beforeAll(async ({ browser}) => { | ||||
|         //TODO: This needs to be refactored | ||||
|         const context = await browser.newContext(); | ||||
|         const page = await context.newPage(); | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|         await page.click('button:has-text("Create")'); | ||||
|   test.beforeAll(async ({ browser }) => { | ||||
|     //TODO: This needs to be refactored | ||||
|     const context = await browser.newContext(); | ||||
|     const page = await context.newPage(); | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|     await page.click('button:has-text("Create")'); | ||||
|  | ||||
|         await page.locator('li[role="menuitem"]:has-text("Condition Set")').click(); | ||||
|     await page.locator('li[role="menuitem"]:has-text("Condition Set")').click(); | ||||
|  | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.click('button:has-text("OK")') | ||||
|         ]); | ||||
|     await Promise.all([page.waitForNavigation(), page.click('button:has-text("OK")')]); | ||||
|  | ||||
|         //Save localStorage for future test execution | ||||
|         await context.storageState({ path: './e2e/test-data/recycled_local_storage.json' }); | ||||
|     //Save localStorage for future test execution | ||||
|     await context.storageState({ path: './e2e/test-data/recycled_local_storage.json' }); | ||||
|  | ||||
|         //Set object identifier from url | ||||
|         conditionSetUrl = page.url(); | ||||
|     //Set object identifier from url | ||||
|     conditionSetUrl = page.url(); | ||||
|  | ||||
|         getConditionSetIdentifierFromUrl = conditionSetUrl.split('/').pop().split('?')[0]; | ||||
|         console.debug(`getConditionSetIdentifierFromUrl: ${getConditionSetIdentifierFromUrl}`); | ||||
|         await page.close(); | ||||
|     }); | ||||
|     getConditionSetIdentifierFromUrl = conditionSetUrl.split('/').pop().split('?')[0]; | ||||
|     console.debug(`getConditionSetIdentifierFromUrl: ${getConditionSetIdentifierFromUrl}`); | ||||
|     await page.close(); | ||||
|   }); | ||||
|  | ||||
|     //Load localStorage for subsequent tests | ||||
|     test.use({ storageState: './e2e/test-data/recycled_local_storage.json' }); | ||||
|   //Load localStorage for subsequent tests | ||||
|   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 @localStorage', async ({ page }) => { | ||||
|         //Navigate to baseURL with injected localStorage | ||||
|         await page.goto(conditionSetUrl, { waitUntil: 'networkidle' }); | ||||
|   //Begin suite of tests again localStorage | ||||
|   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. 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 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 | ||||
|         expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy(); | ||||
|     //Assertions on loaded Condition Set in Inspector | ||||
|     expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy(); | ||||
|  | ||||
|         //Reload Page | ||||
|         await Promise.all([ | ||||
|             page.reload(), | ||||
|             page.waitForLoadState('networkidle') | ||||
|         ]); | ||||
|     //Reload Page | ||||
|     await Promise.all([page.reload(), page.waitForLoadState('networkidle')]); | ||||
|  | ||||
|         //Re-verify after reload | ||||
|         await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set'); | ||||
|         //Assertions on loaded Condition Set in Inspector | ||||
|         expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy(); | ||||
|     //Re-verify after reload | ||||
|     await expect | ||||
|       .soft(page.locator('.l-browse-bar__object-name')) | ||||
|       .toContainText('Unnamed Condition Set'); | ||||
|     //Assertions on loaded Condition Set in Inspector | ||||
|     expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy(); | ||||
|   }); | ||||
|   test('condition set object can be modified on @localStorage', async ({ page, openmctConfig }) => { | ||||
|     const { myItemsFolderName } = openmctConfig; | ||||
|  | ||||
|     }); | ||||
|     test('condition set object can be modified on @localStorage', async ({ page, openmctConfig }) => { | ||||
|         const { myItemsFolderName } = openmctConfig; | ||||
|     await page.goto(conditionSetUrl, { waitUntil: 'networkidle' }); | ||||
|  | ||||
|         await page.goto(conditionSetUrl, { waitUntil: 'networkidle' }); | ||||
|     //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 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 | ||||
|     // Click Edit Button | ||||
|     await page.locator('text=Conditions View Snapshot >> button').nth(3).click(); | ||||
|  | ||||
|         //Update the Condition Set properties | ||||
|         // Click Edit Button | ||||
|         await page.locator('text=Conditions View Snapshot >> button').nth(3).click(); | ||||
|     //Edit Condition Set Name from main view | ||||
|     await page | ||||
|       .locator('.l-browse-bar__object-name') | ||||
|       .filter({ hasText: 'Unnamed Condition Set' }) | ||||
|       .first() | ||||
|       .fill('Renamed Condition Set'); | ||||
|     await page | ||||
|       .locator('.l-browse-bar__object-name') | ||||
|       .filter({ hasText: 'Renamed Condition Set' }) | ||||
|       .first() | ||||
|       .press('Enter'); | ||||
|     // Click Save Button | ||||
|     await page | ||||
|       .locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button') | ||||
|       .nth(1) | ||||
|       .click(); | ||||
|     // Click Save and Finish Editing Option | ||||
|     await page.locator('text=Save and Finish Editing').click(); | ||||
|  | ||||
|         //Edit Condition Set Name from main view | ||||
|         await page.locator('.l-browse-bar__object-name').filter({ hasText: 'Unnamed Condition Set' }).first().fill('Renamed Condition Set'); | ||||
|         await page.locator('.l-browse-bar__object-name').filter({ hasText: 'Renamed Condition Set' }).first().press('Enter'); | ||||
|         // Click Save Button | ||||
|         await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click(); | ||||
|         // Click Save and Finish Editing Option | ||||
|         await page.locator('text=Save and Finish Editing').click(); | ||||
|     //Verify Main section reflects updated Name Property | ||||
|     await expect | ||||
|       .soft(page.locator('.l-browse-bar__object-name')) | ||||
|       .toContainText('Renamed Condition Set'); | ||||
|  | ||||
|         //Verify Main section reflects updated Name Property | ||||
|         await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Renamed Condition Set'); | ||||
|     // Verify Inspector properties | ||||
|     // Verify Inspector has updated Name property | ||||
|     expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy(); | ||||
|     // Verify Inspector Details has updated Name property | ||||
|     expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy(); | ||||
|  | ||||
|         // Verify Inspector properties | ||||
|         // Verify Inspector has updated Name property | ||||
|         expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy(); | ||||
|         // Verify Inspector Details has updated Name property | ||||
|         expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy(); | ||||
|     // Verify Tree reflects updated Name proprety | ||||
|     // Expand Tree | ||||
|     await page.locator(`text=Open MCT ${myItemsFolderName} >> span >> nth=3`).click(); | ||||
|     // Verify Condition Set Object is renamed in Tree | ||||
|     expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); | ||||
|     // Verify Search Tree reflects renamed Name property | ||||
|     await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Renamed'); | ||||
|     expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); | ||||
|  | ||||
|         // Verify Tree reflects updated Name proprety | ||||
|         // Expand Tree | ||||
|         await page.locator(`text=Open MCT ${myItemsFolderName} >> span >> nth=3`).click(); | ||||
|         // Verify Condition Set Object is renamed in Tree | ||||
|         expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); | ||||
|         // Verify Search Tree reflects renamed Name property | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Renamed'); | ||||
|         expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); | ||||
|     //Reload Page | ||||
|     await Promise.all([page.reload(), page.waitForLoadState('networkidle')]); | ||||
|  | ||||
|         //Reload Page | ||||
|         await Promise.all([ | ||||
|             page.reload(), | ||||
|             page.waitForLoadState('networkidle') | ||||
|         ]); | ||||
|     //Verify Main section reflects updated Name Property | ||||
|     await expect | ||||
|       .soft(page.locator('.l-browse-bar__object-name')) | ||||
|       .toContainText('Renamed Condition Set'); | ||||
|  | ||||
|         //Verify Main section reflects updated Name Property | ||||
|         await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Renamed Condition Set'); | ||||
|     // Verify Inspector properties | ||||
|     // Verify Inspector has updated Name property | ||||
|     expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy(); | ||||
|     // Verify Inspector Details has updated Name property | ||||
|     expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy(); | ||||
|  | ||||
|         // Verify Inspector properties | ||||
|         // Verify Inspector has updated Name property | ||||
|         expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy(); | ||||
|         // Verify Inspector Details has updated Name property | ||||
|         expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy(); | ||||
|     // Verify Tree reflects updated Name proprety | ||||
|     // Expand Tree | ||||
|     await page.locator(`text=Open MCT ${myItemsFolderName} >> span >> nth=3`).click(); | ||||
|     // Verify Condition Set Object is renamed in Tree | ||||
|     expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); | ||||
|     // Verify Search Tree reflects renamed Name property | ||||
|     await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Renamed'); | ||||
|     expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); | ||||
|   }); | ||||
|   test('condition set object can be deleted by Search Tree Actions menu on @localStorage', async ({ | ||||
|     page | ||||
|   }) => { | ||||
|     //Navigate to baseURL | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|  | ||||
|         // Verify Tree reflects updated Name proprety | ||||
|         // Expand Tree | ||||
|         await page.locator(`text=Open MCT ${myItemsFolderName} >> span >> nth=3`).click(); | ||||
|         // Verify Condition Set Object is renamed in Tree | ||||
|         expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); | ||||
|         // Verify Search Tree reflects renamed Name property | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Renamed'); | ||||
|         expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); | ||||
|     }); | ||||
|     test('condition set object can be deleted by Search Tree Actions menu on @localStorage', async ({ page }) => { | ||||
|         //Navigate to baseURL | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|     //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(); | ||||
|  | ||||
|         //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(); | ||||
|  | ||||
|         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(); | ||||
|  | ||||
|         // 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 'Remove' and press OK | ||||
|     await page.locator('li[role="menuitem"]:has-text("Remove")').click(); | ||||
|     await page.locator('button:has-text("OK")').click(); | ||||
|  | ||||
|         // Click 'Remove' and press OK | ||||
|         await page.locator('li[role="menuitem"]:has-text("Remove")').click(); | ||||
|         await page.locator('button:has-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 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); | ||||
|  | ||||
|         expect(numberOfConditionSetsAtEnd).toEqual(numberOfConditionSetsToStart - 1); | ||||
|  | ||||
|         //Feature? | ||||
|         //Domain Object is still available by direct URL after delete | ||||
|         await page.goto(conditionSetUrl, { waitUntil: 'networkidle' }); | ||||
|         await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set'); | ||||
|  | ||||
|     }); | ||||
|     //Feature? | ||||
|     //Domain Object is still available by direct URL after delete | ||||
|     await page.goto(conditionSetUrl, { waitUntil: 'networkidle' }); | ||||
|     await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set'); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| test.describe('Basic Condition Set Use', () => { | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         // Open a browser, navigate to the main page, and wait until all network events to resolve | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|   test.beforeEach(async ({ page }) => { | ||||
|     // Open a browser, navigate to the main page, and wait until all network events to resolve | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|   }); | ||||
|   test('Can add a condition', async ({ page }) => { | ||||
|     // Create a new condition set | ||||
|     await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Condition Set', | ||||
|       name: 'Test Condition Set' | ||||
|     }); | ||||
|     test('Can add a condition', async ({ page }) => { | ||||
|         // Create a new condition set | ||||
|         await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Condition Set', | ||||
|             name: "Test Condition Set" | ||||
|         }); | ||||
|         // Change the object to edit mode | ||||
|         await page.locator('[title="Edit"]').click(); | ||||
|     // Change the object to edit mode | ||||
|     await page.locator('[title="Edit"]').click(); | ||||
|  | ||||
|         // Click Add Condition button | ||||
|         await page.locator('#addCondition').click(); | ||||
|         // Check that the new Unnamed Condition section appears | ||||
|         const numOfUnnamedConditions = await page.locator('text=Unnamed Condition').count(); | ||||
|         expect(numOfUnnamedConditions).toEqual(1); | ||||
|     // Click Add Condition button | ||||
|     await page.locator('#addCondition').click(); | ||||
|     // Check that the new Unnamed Condition section appears | ||||
|     const numOfUnnamedConditions = await page.locator('text=Unnamed Condition').count(); | ||||
|     expect(numOfUnnamedConditions).toEqual(1); | ||||
|   }); | ||||
|   test('ConditionSet should display appropriate view options', async ({ page }) => { | ||||
|     test.info().annotations.push({ | ||||
|       type: 'issue', | ||||
|       description: 'https://github.com/nasa/openmct/issues/5924' | ||||
|     }); | ||||
|     test('ConditionSet should display appropriate view options', async ({ page }) => { | ||||
|         test.info().annotations.push({ | ||||
|             type: 'issue', | ||||
|             description: 'https://github.com/nasa/openmct/issues/5924' | ||||
|         }); | ||||
|  | ||||
|         await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Sine Wave Generator', | ||||
|             name: "Alpha Sine Wave Generator" | ||||
|         }); | ||||
|         await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Sine Wave Generator', | ||||
|             name: "Beta Sine Wave Generator" | ||||
|         }); | ||||
|         const conditionSet1 = await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Condition Set', | ||||
|             name: "Test Condition Set" | ||||
|         }); | ||||
|  | ||||
|         // Change the object to edit mode | ||||
|         await page.locator('[title="Edit"]').click(); | ||||
|  | ||||
|         // Expand the 'My Items' folder in the left tree | ||||
|         await page.goto(conditionSet1.url); | ||||
|         page.click('button[title="Show selected item in tree"]'); | ||||
|         // Add the Alpha & Beta Sine Wave Generator to the Condition Set and save changes | ||||
|         const treePane = page.getByRole('tree', { | ||||
|             name: 'Main Tree' | ||||
|         }); | ||||
|         const alphaGeneratorTreeItem = treePane.getByRole('treeitem', { name: "Alpha Sine Wave Generator"}); | ||||
|         const betaGeneratorTreeItem = treePane.getByRole('treeitem', { name: "Beta Sine Wave Generator"}); | ||||
|         const conditionCollection = page.locator('#conditionCollection'); | ||||
|  | ||||
|         await alphaGeneratorTreeItem.dragTo(conditionCollection); | ||||
|         await betaGeneratorTreeItem.dragTo(conditionCollection); | ||||
|  | ||||
|         const saveButtonLocator = page.locator('button[title="Save"]'); | ||||
|         await saveButtonLocator.click(); | ||||
|         await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); | ||||
|         await page.click('button[title="Change the current view"]'); | ||||
|  | ||||
|         await expect(page.getByRole('menuitem', { name: /Lad Table/ })).toBeHidden(); | ||||
|         await expect(page.getByRole('menuitem', { name: /Conditions View/ })).toBeVisible(); | ||||
|         await expect(page.getByRole('menuitem', { name: /Plot/ })).toBeVisible(); | ||||
|         await expect(page.getByRole('menuitem', { name: /Telemetry Table/ })).toBeVisible(); | ||||
|     await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Sine Wave Generator', | ||||
|       name: 'Alpha Sine Wave Generator' | ||||
|     }); | ||||
|     test('ConditionSet should output blank instead of the default value', async ({ page }) => { | ||||
|         //Navigate to baseURL | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|  | ||||
|         //Click the Create button | ||||
|         await page.click('button:has-text("Create")'); | ||||
|  | ||||
|         // Click the object specified by 'type' | ||||
|         await page.click(`li[role='menuitem']:text("Sine Wave Generator")`); | ||||
|         await page.getByRole('spinbutton', { name: 'Loading Delay (ms)' }).fill('8000'); | ||||
|         const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]'); | ||||
|         await nameInput.fill("Delayed Sine Wave Generator"); | ||||
|  | ||||
|         // Click OK button and wait for Navigate event | ||||
|         await Promise.all([ | ||||
|             page.waitForLoadState(), | ||||
|             page.click('[aria-label="Save"]'), | ||||
|             // Wait for Save Banner to appear | ||||
|             page.waitForSelector('.c-message-banner__message') | ||||
|         ]); | ||||
|  | ||||
|         // Create a new condition set | ||||
|         await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Condition Set', | ||||
|             name: "Test Blank Output of Condition Set" | ||||
|         }); | ||||
|         // Change the object to edit mode | ||||
|         await page.locator('[title="Edit"]').click(); | ||||
|  | ||||
|         // Click Add Condition button twice | ||||
|         await page.locator('#addCondition').click(); | ||||
|         await page.locator('#addCondition').click(); | ||||
|         await page.locator('#conditionCollection').getByRole('textbox').nth(0).fill('First Condition'); | ||||
|         await page.locator('#conditionCollection').getByRole('textbox').nth(1).fill('Second Condition'); | ||||
|  | ||||
|         // Expand the 'My Items' folder in the left tree | ||||
|         await page.locator('.c-tree__item__view-control.c-disclosure-triangle').first().click(); | ||||
|         // Add the Sine Wave Generator to the Condition Set and save changes | ||||
|         const treePane = page.getByRole('tree', { | ||||
|             name: 'Main Tree' | ||||
|         }); | ||||
|         const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', { name: "Delayed Sine Wave Generator"}); | ||||
|         const conditionCollection = await page.locator('#conditionCollection'); | ||||
|  | ||||
|         await sineWaveGeneratorTreeItem.dragTo(conditionCollection); | ||||
|  | ||||
|         const firstCriterionTelemetry = await page.locator('[aria-label="Criterion Telemetry Selection"] >> nth=0'); | ||||
|         firstCriterionTelemetry.selectOption({ label: 'Delayed Sine Wave Generator' }); | ||||
|  | ||||
|         const secondCriterionTelemetry = await page.locator('[aria-label="Criterion Telemetry Selection"] >> nth=1'); | ||||
|         secondCriterionTelemetry.selectOption({ label: 'Delayed Sine Wave Generator' }); | ||||
|  | ||||
|         const firstCriterionMetadata = await page.locator('[aria-label="Criterion Metadata Selection"] >> nth=0'); | ||||
|         firstCriterionMetadata.selectOption({ label: 'Sine' }); | ||||
|  | ||||
|         const secondCriterionMetadata = await page.locator('[aria-label="Criterion Metadata Selection"] >> nth=1'); | ||||
|         secondCriterionMetadata.selectOption({ label: 'Sine' }); | ||||
|  | ||||
|         const firstCriterionComparison = await page.locator('[aria-label="Criterion Comparison Selection"] >> nth=0'); | ||||
|         firstCriterionComparison.selectOption({ label: 'is greater than or equal to' }); | ||||
|  | ||||
|         const secondCriterionComparison = await page.locator('[aria-label="Criterion Comparison Selection"] >> nth=1'); | ||||
|         secondCriterionComparison.selectOption({ label: 'is less than' }); | ||||
|  | ||||
|         const firstCriterionInput = await page.locator('[aria-label="Criterion Input"] >> nth=0'); | ||||
|         await firstCriterionInput.fill("0"); | ||||
|  | ||||
|         const secondCriterionInput = await page.locator('[aria-label="Criterion Input"] >> nth=1'); | ||||
|         await secondCriterionInput.fill("0"); | ||||
|  | ||||
|         const saveButtonLocator = page.locator('button[title="Save"]'); | ||||
|         await saveButtonLocator.click(); | ||||
|         await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); | ||||
|  | ||||
|         const outputValue = await page.locator('[aria-label="Current Output Value"]'); | ||||
|         await expect(outputValue).toHaveText('---'); | ||||
|     await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Sine Wave Generator', | ||||
|       name: 'Beta Sine Wave Generator' | ||||
|     }); | ||||
|     const conditionSet1 = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Condition Set', | ||||
|       name: 'Test Condition Set' | ||||
|     }); | ||||
|  | ||||
|     // Change the object to edit mode | ||||
|     await page.locator('[title="Edit"]').click(); | ||||
|  | ||||
|     // Expand the 'My Items' folder in the left tree | ||||
|     await page.goto(conditionSet1.url); | ||||
|     page.click('button[title="Show selected item in tree"]'); | ||||
|     // Add the Alpha & Beta Sine Wave Generator to the Condition Set and save changes | ||||
|     const treePane = page.getByRole('tree', { | ||||
|       name: 'Main Tree' | ||||
|     }); | ||||
|     const alphaGeneratorTreeItem = treePane.getByRole('treeitem', { | ||||
|       name: 'Alpha Sine Wave Generator' | ||||
|     }); | ||||
|     const betaGeneratorTreeItem = treePane.getByRole('treeitem', { | ||||
|       name: 'Beta Sine Wave Generator' | ||||
|     }); | ||||
|     const conditionCollection = page.locator('#conditionCollection'); | ||||
|  | ||||
|     await alphaGeneratorTreeItem.dragTo(conditionCollection); | ||||
|     await betaGeneratorTreeItem.dragTo(conditionCollection); | ||||
|  | ||||
|     const saveButtonLocator = page.locator('button[title="Save"]'); | ||||
|     await saveButtonLocator.click(); | ||||
|     await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); | ||||
|     await page.click('button[title="Change the current view"]'); | ||||
|  | ||||
|     await expect(page.getByRole('menuitem', { name: /Lad Table/ })).toBeHidden(); | ||||
|     await expect(page.getByRole('menuitem', { name: /Conditions View/ })).toBeVisible(); | ||||
|     await expect(page.getByRole('menuitem', { name: /Plot/ })).toBeVisible(); | ||||
|     await expect(page.getByRole('menuitem', { name: /Telemetry Table/ })).toBeVisible(); | ||||
|   }); | ||||
|   test('ConditionSet should output blank instead of the default value', async ({ page }) => { | ||||
|     //Navigate to baseURL | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|  | ||||
|     //Click the Create button | ||||
|     await page.click('button:has-text("Create")'); | ||||
|  | ||||
|     // Click the object specified by 'type' | ||||
|     await page.click(`li[role='menuitem']:text("Sine Wave Generator")`); | ||||
|     await page.getByRole('spinbutton', { name: 'Loading Delay (ms)' }).fill('8000'); | ||||
|     const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]'); | ||||
|     await nameInput.fill('Delayed Sine Wave Generator'); | ||||
|  | ||||
|     // Click OK button and wait for Navigate event | ||||
|     await Promise.all([ | ||||
|       page.waitForLoadState(), | ||||
|       page.click('[aria-label="Save"]'), | ||||
|       // Wait for Save Banner to appear | ||||
|       page.waitForSelector('.c-message-banner__message') | ||||
|     ]); | ||||
|  | ||||
|     // Create a new condition set | ||||
|     await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Condition Set', | ||||
|       name: 'Test Blank Output of Condition Set' | ||||
|     }); | ||||
|     // Change the object to edit mode | ||||
|     await page.locator('[title="Edit"]').click(); | ||||
|  | ||||
|     // Click Add Condition button twice | ||||
|     await page.locator('#addCondition').click(); | ||||
|     await page.locator('#addCondition').click(); | ||||
|     await page.locator('#conditionCollection').getByRole('textbox').nth(0).fill('First Condition'); | ||||
|     await page.locator('#conditionCollection').getByRole('textbox').nth(1).fill('Second Condition'); | ||||
|  | ||||
|     // Expand the 'My Items' folder in the left tree | ||||
|     await page.locator('.c-tree__item__view-control.c-disclosure-triangle').first().click(); | ||||
|     // Add the Sine Wave Generator to the Condition Set and save changes | ||||
|     const treePane = page.getByRole('tree', { | ||||
|       name: 'Main Tree' | ||||
|     }); | ||||
|     const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', { | ||||
|       name: 'Delayed Sine Wave Generator' | ||||
|     }); | ||||
|     const conditionCollection = await page.locator('#conditionCollection'); | ||||
|  | ||||
|     await sineWaveGeneratorTreeItem.dragTo(conditionCollection); | ||||
|  | ||||
|     const firstCriterionTelemetry = await page.locator( | ||||
|       '[aria-label="Criterion Telemetry Selection"] >> nth=0' | ||||
|     ); | ||||
|     firstCriterionTelemetry.selectOption({ label: 'Delayed Sine Wave Generator' }); | ||||
|  | ||||
|     const secondCriterionTelemetry = await page.locator( | ||||
|       '[aria-label="Criterion Telemetry Selection"] >> nth=1' | ||||
|     ); | ||||
|     secondCriterionTelemetry.selectOption({ label: 'Delayed Sine Wave Generator' }); | ||||
|  | ||||
|     const firstCriterionMetadata = await page.locator( | ||||
|       '[aria-label="Criterion Metadata Selection"] >> nth=0' | ||||
|     ); | ||||
|     firstCriterionMetadata.selectOption({ label: 'Sine' }); | ||||
|  | ||||
|     const secondCriterionMetadata = await page.locator( | ||||
|       '[aria-label="Criterion Metadata Selection"] >> nth=1' | ||||
|     ); | ||||
|     secondCriterionMetadata.selectOption({ label: 'Sine' }); | ||||
|  | ||||
|     const firstCriterionComparison = await page.locator( | ||||
|       '[aria-label="Criterion Comparison Selection"] >> nth=0' | ||||
|     ); | ||||
|     firstCriterionComparison.selectOption({ label: 'is greater than or equal to' }); | ||||
|  | ||||
|     const secondCriterionComparison = await page.locator( | ||||
|       '[aria-label="Criterion Comparison Selection"] >> nth=1' | ||||
|     ); | ||||
|     secondCriterionComparison.selectOption({ label: 'is less than' }); | ||||
|  | ||||
|     const firstCriterionInput = await page.locator('[aria-label="Criterion Input"] >> nth=0'); | ||||
|     await firstCriterionInput.fill('0'); | ||||
|  | ||||
|     const secondCriterionInput = await page.locator('[aria-label="Criterion Input"] >> nth=1'); | ||||
|     await secondCriterionInput.fill('0'); | ||||
|  | ||||
|     const saveButtonLocator = page.locator('button[title="Save"]'); | ||||
|     await saveButtonLocator.click(); | ||||
|     await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click(); | ||||
|  | ||||
|     const outputValue = await page.locator('[aria-label="Current Output Value"]'); | ||||
|     await expect(outputValue).toHaveText('---'); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -21,173 +21,255 @@ | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| const { test, expect } = require('../../../../pluginFixtures'); | ||||
| const { createDomainObjectWithDefaults, setStartOffset, setFixedTimeMode, setRealTimeMode } = require('../../../../appActions'); | ||||
| const { | ||||
|   createDomainObjectWithDefaults, | ||||
|   setStartOffset, | ||||
|   setFixedTimeMode, | ||||
|   setRealTimeMode | ||||
| } = require('../../../../appActions'); | ||||
|  | ||||
| test.describe('Display Layout', () => { | ||||
|     /** @type {import('../../../../appActions').CreatedObjectInfo} */ | ||||
|     let sineWaveObject; | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|         await setRealTimeMode(page); | ||||
|   /** @type {import('../../../../appActions').CreatedObjectInfo} */ | ||||
|   let sineWaveObject; | ||||
|   test.beforeEach(async ({ page }) => { | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|     await setRealTimeMode(page); | ||||
|  | ||||
|         // Create Sine Wave Generator | ||||
|         sineWaveObject = await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Sine Wave Generator' | ||||
|         }); | ||||
|     // Create Sine Wave Generator | ||||
|     sineWaveObject = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Sine Wave Generator' | ||||
|     }); | ||||
|     test('alpha-numeric widget telemetry value exactly matches latest telemetry value received in real time', async ({ page }) => { | ||||
|         // Create a Display Layout | ||||
|         await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Display Layout', | ||||
|             name: "Test Display Layout" | ||||
|         }); | ||||
|         // Edit Display Layout | ||||
|         await page.locator('[title="Edit"]').click(); | ||||
|  | ||||
|         // Expand the 'My Items' folder in the left tree | ||||
|         await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click(); | ||||
|         // Add the Sine Wave Generator to the Display Layout and save changes | ||||
|         const treePane = page.getByRole('tree', { | ||||
|             name: 'Main Tree' | ||||
|         }); | ||||
|         const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', { | ||||
|             name: new RegExp(sineWaveObject.name) | ||||
|         }); | ||||
|         const layoutGridHolder = page.locator('.l-layout__grid-holder'); | ||||
|         await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder); | ||||
|         await page.locator('button[title="Save"]').click(); | ||||
|         await page.locator('text=Save and Finish Editing').click(); | ||||
|  | ||||
|         // Subscribe to the Sine Wave Generator data | ||||
|         // On getting data, check if the value found in the  Display Layout is the most recent value | ||||
|         // from the Sine Wave Generator | ||||
|         const getTelemValuePromise = await subscribeToTelemetry(page, sineWaveObject.uuid); | ||||
|         const formattedTelemetryValue = getTelemValuePromise; | ||||
|         const displayLayoutValuePromise = await page.waitForSelector(`text="${formattedTelemetryValue}"`); | ||||
|         const displayLayoutValue = await displayLayoutValuePromise.textContent(); | ||||
|         const trimmedDisplayValue = displayLayoutValue.trim(); | ||||
|  | ||||
|         expect(trimmedDisplayValue).toBe(formattedTelemetryValue); | ||||
|   }); | ||||
|   test('alpha-numeric widget telemetry value exactly matches latest telemetry value received in real time', async ({ | ||||
|     page | ||||
|   }) => { | ||||
|     // Create a Display Layout | ||||
|     await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Display Layout', | ||||
|       name: 'Test Display Layout' | ||||
|     }); | ||||
|     test('alpha-numeric widget telemetry value exactly matches latest telemetry value received in fixed time', async ({ page }) => { | ||||
|         // Create a Display Layout | ||||
|         await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Display Layout', | ||||
|             name: "Test Display Layout" | ||||
|         }); | ||||
|         // Edit Display Layout | ||||
|         await page.locator('[title="Edit"]').click(); | ||||
|     // Edit Display Layout | ||||
|     await page.locator('[title="Edit"]').click(); | ||||
|  | ||||
|         // Expand the 'My Items' folder in the left tree | ||||
|         await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click(); | ||||
|         // Add the Sine Wave Generator to the Display Layout and save changes | ||||
|         const treePane = page.getByRole('tree', { | ||||
|             name: 'Main Tree' | ||||
|         }); | ||||
|         const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', { | ||||
|             name: new RegExp(sineWaveObject.name) | ||||
|         }); | ||||
|         const layoutGridHolder = page.locator('.l-layout__grid-holder'); | ||||
|         await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder); | ||||
|         await page.locator('button[title="Save"]').click(); | ||||
|         await page.locator('text=Save and Finish Editing').click(); | ||||
|  | ||||
|         // Subscribe to the Sine Wave Generator data | ||||
|         const getTelemValuePromise = await subscribeToTelemetry(page, sineWaveObject.uuid); | ||||
|         // Set an offset of 1 minute and then change the time mode to fixed to set a 1 minute historical window | ||||
|         await setStartOffset(page, { mins: '1' }); | ||||
|         await setFixedTimeMode(page); | ||||
|  | ||||
|         // On getting data, check if the value found in the Display Layout is the most recent value | ||||
|         // from the Sine Wave Generator | ||||
|         const formattedTelemetryValue = getTelemValuePromise; | ||||
|         const displayLayoutValuePromise = await page.waitForSelector(`text="${formattedTelemetryValue}"`); | ||||
|         const displayLayoutValue = await displayLayoutValuePromise.textContent(); | ||||
|         const trimmedDisplayValue = displayLayoutValue.trim(); | ||||
|  | ||||
|         expect(trimmedDisplayValue).toBe(formattedTelemetryValue); | ||||
|     // Expand the 'My Items' folder in the left tree | ||||
|     await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click(); | ||||
|     // Add the Sine Wave Generator to the Display Layout and save changes | ||||
|     const treePane = page.getByRole('tree', { | ||||
|       name: 'Main Tree' | ||||
|     }); | ||||
|     test('items in a display layout can be removed with object tree context menu when viewing the display layout', async ({ page }) => { | ||||
|         // Create a Display Layout | ||||
|         await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Display Layout', | ||||
|             name: "Test Display Layout" | ||||
|         }); | ||||
|         // Edit Display Layout | ||||
|         await page.locator('[title="Edit"]').click(); | ||||
|  | ||||
|         // Expand the 'My Items' folder in the left tree | ||||
|         await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click(); | ||||
|         // Add the Sine Wave Generator to the Display Layout and save changes | ||||
|         const treePane = page.getByRole('tree', { | ||||
|             name: 'Main Tree' | ||||
|         }); | ||||
|         const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', { | ||||
|             name: new RegExp(sineWaveObject.name) | ||||
|         }); | ||||
|         const layoutGridHolder = page.locator('.l-layout__grid-holder'); | ||||
|         await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder); | ||||
|         await page.locator('button[title="Save"]').click(); | ||||
|         await page.locator('text=Save and Finish Editing').click(); | ||||
|  | ||||
|         expect.soft(await page.locator('.l-layout .l-layout__frame').count()).toEqual(1); | ||||
|  | ||||
|         // Expand the Display Layout so we can remove the sine wave generator | ||||
|         await page.locator('.c-tree__item.is-navigated-object .c-disclosure-triangle').click(); | ||||
|  | ||||
|         // Bring up context menu and remove | ||||
|         await sineWaveGeneratorTreeItem.nth(1).click({ button: 'right' }); | ||||
|         await page.locator('li[role="menuitem"]:has-text("Remove")').click(); | ||||
|         await page.locator('button:has-text("OK")').click(); | ||||
|  | ||||
|         // delete | ||||
|  | ||||
|         expect(await page.locator('.l-layout .l-layout__frame').count()).toEqual(0); | ||||
|     const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', { | ||||
|       name: new RegExp(sineWaveObject.name) | ||||
|     }); | ||||
|     test('items in a display layout can be removed with object tree context menu when viewing another item', async ({ page }) => { | ||||
|         test.info().annotations.push({ | ||||
|             type: 'issue', | ||||
|             description: 'https://github.com/nasa/openmct/issues/3117' | ||||
|         }); | ||||
|         // Create a Display Layout | ||||
|         const displayLayout = await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Display Layout' | ||||
|         }); | ||||
|         // Edit Display Layout | ||||
|         await page.locator('[title="Edit"]').click(); | ||||
|     const layoutGridHolder = page.locator('.l-layout__grid-holder'); | ||||
|     await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder); | ||||
|     await page.locator('button[title="Save"]').click(); | ||||
|     await page.locator('text=Save and Finish Editing').click(); | ||||
|  | ||||
|         // Expand the 'My Items' folder in the left tree | ||||
|         await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click(); | ||||
|         // Add the Sine Wave Generator to the Display Layout and save changes | ||||
|         const treePane = page.getByRole('tree', { | ||||
|             name: 'Main Tree' | ||||
|         }); | ||||
|         const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', { | ||||
|             name: new RegExp(sineWaveObject.name) | ||||
|         }); | ||||
|         const layoutGridHolder = page.locator('.l-layout__grid-holder'); | ||||
|         await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder); | ||||
|         await page.locator('button[title="Save"]').click(); | ||||
|         await page.locator('text=Save and Finish Editing').click(); | ||||
|     // Subscribe to the Sine Wave Generator data | ||||
|     // On getting data, check if the value found in the  Display Layout is the most recent value | ||||
|     // from the Sine Wave Generator | ||||
|     const getTelemValuePromise = await subscribeToTelemetry(page, sineWaveObject.uuid); | ||||
|     const formattedTelemetryValue = getTelemValuePromise; | ||||
|     const displayLayoutValuePromise = await page.waitForSelector( | ||||
|       `text="${formattedTelemetryValue}"` | ||||
|     ); | ||||
|     const displayLayoutValue = await displayLayoutValuePromise.textContent(); | ||||
|     const trimmedDisplayValue = displayLayoutValue.trim(); | ||||
|  | ||||
|         expect.soft(await page.locator('.l-layout .l-layout__frame').count()).toEqual(1); | ||||
|  | ||||
|         // Expand the Display Layout so we can remove the sine wave generator | ||||
|         await page.locator('.c-tree__item.is-navigated-object .c-disclosure-triangle').click(); | ||||
|  | ||||
|         // Go to the original Sine Wave Generator to navigate away from the Display Layout | ||||
|         await page.goto(sineWaveObject.url); | ||||
|  | ||||
|         // Bring up context menu and remove | ||||
|         await sineWaveGeneratorTreeItem.first().click({ button: 'right' }); | ||||
|         await page.locator('li[role="menuitem"]:has-text("Remove")').click(); | ||||
|         await page.locator('button:has-text("OK")').click(); | ||||
|  | ||||
|         // navigate back to the display layout to confirm it has been removed | ||||
|         await page.goto(displayLayout.url); | ||||
|  | ||||
|         expect(await page.locator('.l-layout .l-layout__frame').count()).toEqual(0); | ||||
|     expect(trimmedDisplayValue).toBe(formattedTelemetryValue); | ||||
|   }); | ||||
|   test('alpha-numeric widget telemetry value exactly matches latest telemetry value received in fixed time', async ({ | ||||
|     page | ||||
|   }) => { | ||||
|     // Create a Display Layout | ||||
|     await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Display Layout', | ||||
|       name: 'Test Display Layout' | ||||
|     }); | ||||
|     // Edit Display Layout | ||||
|     await page.locator('[title="Edit"]').click(); | ||||
|  | ||||
|     // Expand the 'My Items' folder in the left tree | ||||
|     await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click(); | ||||
|     // Add the Sine Wave Generator to the Display Layout and save changes | ||||
|     const treePane = page.getByRole('tree', { | ||||
|       name: 'Main Tree' | ||||
|     }); | ||||
|     const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', { | ||||
|       name: new RegExp(sineWaveObject.name) | ||||
|     }); | ||||
|     const layoutGridHolder = page.locator('.l-layout__grid-holder'); | ||||
|     await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder); | ||||
|     await page.locator('button[title="Save"]').click(); | ||||
|     await page.locator('text=Save and Finish Editing').click(); | ||||
|  | ||||
|     // Subscribe to the Sine Wave Generator data | ||||
|     const getTelemValuePromise = await subscribeToTelemetry(page, sineWaveObject.uuid); | ||||
|     // Set an offset of 1 minute and then change the time mode to fixed to set a 1 minute historical window | ||||
|     await setStartOffset(page, { mins: '1' }); | ||||
|     await setFixedTimeMode(page); | ||||
|  | ||||
|     // On getting data, check if the value found in the Display Layout is the most recent value | ||||
|     // from the Sine Wave Generator | ||||
|     const formattedTelemetryValue = getTelemValuePromise; | ||||
|     const displayLayoutValuePromise = await page.waitForSelector( | ||||
|       `text="${formattedTelemetryValue}"` | ||||
|     ); | ||||
|     const displayLayoutValue = await displayLayoutValuePromise.textContent(); | ||||
|     const trimmedDisplayValue = displayLayoutValue.trim(); | ||||
|  | ||||
|     expect(trimmedDisplayValue).toBe(formattedTelemetryValue); | ||||
|   }); | ||||
|   test('items in a display layout can be removed with object tree context menu when viewing the display layout', async ({ | ||||
|     page | ||||
|   }) => { | ||||
|     // Create a Display Layout | ||||
|     await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Display Layout', | ||||
|       name: 'Test Display Layout' | ||||
|     }); | ||||
|     // Edit Display Layout | ||||
|     await page.locator('[title="Edit"]').click(); | ||||
|  | ||||
|     // Expand the 'My Items' folder in the left tree | ||||
|     await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click(); | ||||
|     // Add the Sine Wave Generator to the Display Layout and save changes | ||||
|     const treePane = page.getByRole('tree', { | ||||
|       name: 'Main Tree' | ||||
|     }); | ||||
|     const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', { | ||||
|       name: new RegExp(sineWaveObject.name) | ||||
|     }); | ||||
|     const layoutGridHolder = page.locator('.l-layout__grid-holder'); | ||||
|     await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder); | ||||
|     await page.locator('button[title="Save"]').click(); | ||||
|     await page.locator('text=Save and Finish Editing').click(); | ||||
|  | ||||
|     expect.soft(await page.locator('.l-layout .l-layout__frame').count()).toEqual(1); | ||||
|  | ||||
|     // Expand the Display Layout so we can remove the sine wave generator | ||||
|     await page.locator('.c-tree__item.is-navigated-object .c-disclosure-triangle').click(); | ||||
|  | ||||
|     // Bring up context menu and remove | ||||
|     await sineWaveGeneratorTreeItem.nth(1).click({ button: 'right' }); | ||||
|     await page.locator('li[role="menuitem"]:has-text("Remove")').click(); | ||||
|     await page.locator('button:has-text("OK")').click(); | ||||
|  | ||||
|     // delete | ||||
|  | ||||
|     expect(await page.locator('.l-layout .l-layout__frame').count()).toEqual(0); | ||||
|   }); | ||||
|   test('items in a display layout can be removed with object tree context menu when viewing another item', async ({ | ||||
|     page | ||||
|   }) => { | ||||
|     test.info().annotations.push({ | ||||
|       type: 'issue', | ||||
|       description: 'https://github.com/nasa/openmct/issues/3117' | ||||
|     }); | ||||
|     // Create a Display Layout | ||||
|     const displayLayout = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Display Layout' | ||||
|     }); | ||||
|     // Edit Display Layout | ||||
|     await page.locator('[title="Edit"]').click(); | ||||
|  | ||||
|     // Expand the 'My Items' folder in the left tree | ||||
|     await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click(); | ||||
|     // Add the Sine Wave Generator to the Display Layout and save changes | ||||
|     const treePane = page.getByRole('tree', { | ||||
|       name: 'Main Tree' | ||||
|     }); | ||||
|     const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', { | ||||
|       name: new RegExp(sineWaveObject.name) | ||||
|     }); | ||||
|     const layoutGridHolder = page.locator('.l-layout__grid-holder'); | ||||
|     await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder); | ||||
|     await page.locator('button[title="Save"]').click(); | ||||
|     await page.locator('text=Save and Finish Editing').click(); | ||||
|  | ||||
|     expect.soft(await page.locator('.l-layout .l-layout__frame').count()).toEqual(1); | ||||
|  | ||||
|     // Expand the Display Layout so we can remove the sine wave generator | ||||
|     await page.locator('.c-tree__item.is-navigated-object .c-disclosure-triangle').click(); | ||||
|  | ||||
|     // Go to the original Sine Wave Generator to navigate away from the Display Layout | ||||
|     await page.goto(sineWaveObject.url); | ||||
|  | ||||
|     // Bring up context menu and remove | ||||
|     await sineWaveGeneratorTreeItem.first().click({ button: 'right' }); | ||||
|     await page.locator('li[role="menuitem"]:has-text("Remove")').click(); | ||||
|     await page.locator('button:has-text("OK")').click(); | ||||
|  | ||||
|     // navigate back to the display layout to confirm it has been removed | ||||
|     await page.goto(displayLayout.url); | ||||
|  | ||||
|     expect(await page.locator('.l-layout .l-layout__frame').count()).toEqual(0); | ||||
|   }); | ||||
|  | ||||
|   test('When multiple plots are contained in a layout, we only ask for annotations once @couchdb', async ({ | ||||
|     page | ||||
|   }) => { | ||||
|     // Create another Sine Wave Generator | ||||
|     const anotherSineWaveObject = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Sine Wave Generator' | ||||
|     }); | ||||
|     // Create a Display Layout | ||||
|     await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Display Layout', | ||||
|       name: 'Test Display Layout' | ||||
|     }); | ||||
|     // Edit Display Layout | ||||
|     await page.locator('[title="Edit"]').click(); | ||||
|  | ||||
|     // Expand the 'My Items' folder in the left tree | ||||
|     await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click(); | ||||
|     // Add the Sine Wave Generator to the Display Layout and save changes | ||||
|     const treePane = page.getByRole('tree', { | ||||
|       name: 'Main Tree' | ||||
|     }); | ||||
|     const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', { | ||||
|       name: new RegExp(sineWaveObject.name) | ||||
|     }); | ||||
|  | ||||
|     let layoutGridHolder = page.locator('.l-layout__grid-holder'); | ||||
|     // eslint-disable-next-line playwright/no-force-option | ||||
|     await sineWaveGeneratorTreeItem.dragTo(layoutGridHolder, { force: true }); | ||||
|  | ||||
|     await page.getByText('View type').click(); | ||||
|     await page.getByText('Overlay Plot').click(); | ||||
|  | ||||
|     const anotherSineWaveGeneratorTreeItem = treePane.getByRole('treeitem', { | ||||
|       name: new RegExp(anotherSineWaveObject.name) | ||||
|     }); | ||||
|     layoutGridHolder = page.locator('.l-layout__grid-holder'); | ||||
|     // eslint-disable-next-line playwright/no-force-option | ||||
|     await anotherSineWaveGeneratorTreeItem.dragTo(layoutGridHolder, { force: true }); | ||||
|  | ||||
|     await page.getByText('View type').click(); | ||||
|     await page.getByText('Overlay Plot').click(); | ||||
|  | ||||
|     await page.locator('button[title="Save"]').click(); | ||||
|     await page.locator('text=Save and Finish Editing').click(); | ||||
|  | ||||
|     // Time to inspect some network traffic | ||||
|     let networkRequests = []; | ||||
|     page.on('request', (request) => { | ||||
|       const searchRequest = request.url().endsWith('_find'); | ||||
|       const fetchRequest = request.resourceType() === 'fetch'; | ||||
|       if (searchRequest && fetchRequest) { | ||||
|         networkRequests.push(request); | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     await page.reload(); | ||||
|  | ||||
|     // wait for annotations requests to be batched and requested | ||||
|     await page.waitForLoadState('networkidle'); | ||||
|  | ||||
|     // Network requests for the composite telemetry with multiple items should be: | ||||
|     // 1.  a single batched request for annotations | ||||
|     expect(networkRequests.length).toBe(1); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| /** | ||||
| @@ -200,18 +282,20 @@ test.describe('Display Layout', () => { | ||||
|  * @returns {Promise<string>} the formatted sin telemetry value | ||||
|  */ | ||||
| async function subscribeToTelemetry(page, objectIdentifier) { | ||||
|     const getTelemValuePromise = new Promise(resolve => page.exposeFunction('getTelemValue', resolve)); | ||||
|   const getTelemValuePromise = new Promise((resolve) => | ||||
|     page.exposeFunction('getTelemValue', resolve) | ||||
|   ); | ||||
|  | ||||
|     await page.evaluate(async (telemetryIdentifier) => { | ||||
|         const telemetryObject = await window.openmct.objects.get(telemetryIdentifier); | ||||
|         const metadata = window.openmct.telemetry.getMetadata(telemetryObject); | ||||
|         const formats = await window.openmct.telemetry.getFormatMap(metadata); | ||||
|         window.openmct.telemetry.subscribe(telemetryObject, (obj) => { | ||||
|             const sinVal = obj.sin; | ||||
|             const formattedSinVal = formats.sin.format(sinVal); | ||||
|             window.getTelemValue(formattedSinVal); | ||||
|         }); | ||||
|     }, objectIdentifier); | ||||
|   await page.evaluate(async (telemetryIdentifier) => { | ||||
|     const telemetryObject = await window.openmct.objects.get(telemetryIdentifier); | ||||
|     const metadata = window.openmct.telemetry.getMetadata(telemetryObject); | ||||
|     const formats = await window.openmct.telemetry.getFormatMap(metadata); | ||||
|     window.openmct.telemetry.subscribe(telemetryObject, (obj) => { | ||||
|       const sinVal = obj.sin; | ||||
|       const formattedSinVal = formats.sin.format(sinVal); | ||||
|       window.getTelemValue(formattedSinVal); | ||||
|     }); | ||||
|   }, objectIdentifier); | ||||
|  | ||||
|     return getTelemValuePromise; | ||||
|   return getTelemValuePromise; | ||||
| } | ||||
|   | ||||
| @@ -25,216 +25,231 @@ const utils = require('../../../../helper/faultUtils'); | ||||
| const { selectInspectorTab } = require('../../../../appActions'); | ||||
|  | ||||
| test.describe('The Fault Management Plugin using example faults', () => { | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         await utils.navigateToFaultManagementWithExample(page); | ||||
|     }); | ||||
|   test.beforeEach(async ({ page }) => { | ||||
|     await utils.navigateToFaultManagementWithExample(page); | ||||
|   }); | ||||
|  | ||||
|     test('Shows a criticality icon for every fault @unstable', async ({ page }) => { | ||||
|         const faultCount = await page.locator('c-fault-mgmt__list').count(); | ||||
|         const criticalityIconCount = await page.locator('c-fault-mgmt__list-severity').count(); | ||||
|   test('Shows a criticality icon for every fault @unstable', async ({ page }) => { | ||||
|     const faultCount = await page.locator('c-fault-mgmt__list').count(); | ||||
|     const criticalityIconCount = await page.locator('c-fault-mgmt__list-severity').count(); | ||||
|  | ||||
|         expect.soft(faultCount).toEqual(criticalityIconCount); | ||||
|     }); | ||||
|     expect.soft(faultCount).toEqual(criticalityIconCount); | ||||
|   }); | ||||
|  | ||||
|     test('When selecting a fault, it has an "is-selected" class and it\'s information shows in the inspector @unstable', async ({ page }) => { | ||||
|         await utils.selectFaultItem(page, 1); | ||||
|   test('When selecting a fault, it has an "is-selected" class and it\'s information shows in the inspector @unstable', async ({ | ||||
|     page | ||||
|   }) => { | ||||
|     await utils.selectFaultItem(page, 1); | ||||
|  | ||||
|         await selectInspectorTab(page, 'Fault Management Configuration'); | ||||
|         const selectedFaultName = await page.locator('.c-fault-mgmt__list.is-selected .c-fault-mgmt__list-faultname').textContent(); | ||||
|         const inspectorFaultNameCount = await page.locator(`.c-inspector__properties >> :text("${selectedFaultName}")`).count(); | ||||
|     await selectInspectorTab(page, 'Fault Management Configuration'); | ||||
|     const selectedFaultName = await page | ||||
|       .locator('.c-fault-mgmt__list.is-selected .c-fault-mgmt__list-faultname') | ||||
|       .textContent(); | ||||
|     const inspectorFaultNameCount = await page | ||||
|       .locator(`.c-inspector__properties >> :text("${selectedFaultName}")`) | ||||
|       .count(); | ||||
|  | ||||
|         await expect.soft(page.locator('.c-faults-list-view-item-body > .c-fault-mgmt__list').first()).toHaveClass(/is-selected/); | ||||
|         expect.soft(inspectorFaultNameCount).toEqual(1); | ||||
|     }); | ||||
|     await expect | ||||
|       .soft(page.locator('.c-faults-list-view-item-body > .c-fault-mgmt__list').first()) | ||||
|       .toHaveClass(/is-selected/); | ||||
|     expect.soft(inspectorFaultNameCount).toEqual(1); | ||||
|   }); | ||||
|  | ||||
|     test('When selecting multiple faults, no specific fault information is shown in the inspector @unstable', async ({ page }) => { | ||||
|         await utils.selectFaultItem(page, 1); | ||||
|         await utils.selectFaultItem(page, 2); | ||||
|   test('When selecting multiple faults, no specific fault information is shown in the inspector @unstable', async ({ | ||||
|     page | ||||
|   }) => { | ||||
|     await utils.selectFaultItem(page, 1); | ||||
|     await utils.selectFaultItem(page, 2); | ||||
|  | ||||
|         const selectedRows = page.locator('.c-fault-mgmt__list.is-selected .c-fault-mgmt__list-faultname'); | ||||
|         expect.soft(await selectedRows.count()).toEqual(2); | ||||
|     const selectedRows = page.locator( | ||||
|       '.c-fault-mgmt__list.is-selected .c-fault-mgmt__list-faultname' | ||||
|     ); | ||||
|     expect.soft(await selectedRows.count()).toEqual(2); | ||||
|  | ||||
|         await selectInspectorTab(page, 'Fault Management Configuration'); | ||||
|         const firstSelectedFaultName = await selectedRows.nth(0).textContent(); | ||||
|         const secondSelectedFaultName = await selectedRows.nth(1).textContent(); | ||||
|         const firstNameInInspectorCount = await page.locator(`.c-inspector__properties >> :text("${firstSelectedFaultName}")`).count(); | ||||
|         const secondNameInInspectorCount = await page.locator(`.c-inspector__properties >> :text("${secondSelectedFaultName}")`).count(); | ||||
|     await selectInspectorTab(page, 'Fault Management Configuration'); | ||||
|     const firstSelectedFaultName = await selectedRows.nth(0).textContent(); | ||||
|     const secondSelectedFaultName = await selectedRows.nth(1).textContent(); | ||||
|     const firstNameInInspectorCount = await page | ||||
|       .locator(`.c-inspector__properties >> :text("${firstSelectedFaultName}")`) | ||||
|       .count(); | ||||
|     const secondNameInInspectorCount = await page | ||||
|       .locator(`.c-inspector__properties >> :text("${secondSelectedFaultName}")`) | ||||
|       .count(); | ||||
|  | ||||
|         expect.soft(firstNameInInspectorCount).toEqual(0); | ||||
|         expect.soft(secondNameInInspectorCount).toEqual(0); | ||||
|     }); | ||||
|     expect.soft(firstNameInInspectorCount).toEqual(0); | ||||
|     expect.soft(secondNameInInspectorCount).toEqual(0); | ||||
|   }); | ||||
|  | ||||
|     test('Allows you to shelve a fault @unstable', async ({ page }) => { | ||||
|         const shelvedFaultName = await utils.getFaultName(page, 2); | ||||
|         const beforeShelvedFault = utils.getFaultByName(page, shelvedFaultName); | ||||
|   test('Allows you to shelve a fault @unstable', async ({ page }) => { | ||||
|     const shelvedFaultName = await utils.getFaultName(page, 2); | ||||
|     const beforeShelvedFault = utils.getFaultByName(page, shelvedFaultName); | ||||
|  | ||||
|         expect.soft(await beforeShelvedFault.count()).toBe(1); | ||||
|     expect.soft(await beforeShelvedFault.count()).toBe(1); | ||||
|  | ||||
|         await utils.shelveFault(page, 2); | ||||
|     await utils.shelveFault(page, 2); | ||||
|  | ||||
|         // check it is removed from standard view | ||||
|         const afterShelvedFault = utils.getFaultByName(page, shelvedFaultName); | ||||
|         expect.soft(await afterShelvedFault.count()).toBe(0); | ||||
|     // check it is removed from standard view | ||||
|     const afterShelvedFault = utils.getFaultByName(page, shelvedFaultName); | ||||
|     expect.soft(await afterShelvedFault.count()).toBe(0); | ||||
|  | ||||
|         await utils.changeViewTo(page, 'shelved'); | ||||
|     await utils.changeViewTo(page, 'shelved'); | ||||
|  | ||||
|         const shelvedViewFault = utils.getFaultByName(page, shelvedFaultName); | ||||
|     const shelvedViewFault = utils.getFaultByName(page, shelvedFaultName); | ||||
|  | ||||
|         expect.soft(await shelvedViewFault.count()).toBe(1); | ||||
|     }); | ||||
|     expect.soft(await shelvedViewFault.count()).toBe(1); | ||||
|   }); | ||||
|  | ||||
|     test('Allows you to acknowledge a fault @unstable', async ({ page }) => { | ||||
|         const acknowledgedFaultName = await utils.getFaultName(page, 3); | ||||
|   test('Allows you to acknowledge a fault @unstable', async ({ page }) => { | ||||
|     const acknowledgedFaultName = await utils.getFaultName(page, 3); | ||||
|  | ||||
|         await utils.acknowledgeFault(page, 3); | ||||
|     await utils.acknowledgeFault(page, 3); | ||||
|  | ||||
|         const fault = utils.getFault(page, 3); | ||||
|         await expect.soft(fault).toHaveClass(/is-acknowledged/); | ||||
|     const fault = utils.getFault(page, 3); | ||||
|     await expect.soft(fault).toHaveClass(/is-acknowledged/); | ||||
|  | ||||
|         await utils.changeViewTo(page, 'acknowledged'); | ||||
|     await utils.changeViewTo(page, 'acknowledged'); | ||||
|  | ||||
|         const acknowledgedViewFaultName = await utils.getFaultName(page, 1); | ||||
|         expect.soft(acknowledgedFaultName).toEqual(acknowledgedViewFaultName); | ||||
|     }); | ||||
|     const acknowledgedViewFaultName = await utils.getFaultName(page, 1); | ||||
|     expect.soft(acknowledgedFaultName).toEqual(acknowledgedViewFaultName); | ||||
|   }); | ||||
|  | ||||
|     test('Allows you to shelve multiple faults @unstable', async ({ page }) => { | ||||
|         const shelvedFaultNameOne = await utils.getFaultName(page, 1); | ||||
|         const shelvedFaultNameFour = await utils.getFaultName(page, 4); | ||||
|   test('Allows you to shelve multiple faults @unstable', async ({ page }) => { | ||||
|     const shelvedFaultNameOne = await utils.getFaultName(page, 1); | ||||
|     const shelvedFaultNameFour = await utils.getFaultName(page, 4); | ||||
|  | ||||
|         const beforeShelvedFaultOne = utils.getFaultByName(page, shelvedFaultNameOne); | ||||
|         const beforeShelvedFaultFour = utils.getFaultByName(page, shelvedFaultNameFour); | ||||
|     const beforeShelvedFaultOne = utils.getFaultByName(page, shelvedFaultNameOne); | ||||
|     const beforeShelvedFaultFour = utils.getFaultByName(page, shelvedFaultNameFour); | ||||
|  | ||||
|         expect.soft(await beforeShelvedFaultOne.count()).toBe(1); | ||||
|         expect.soft(await beforeShelvedFaultFour.count()).toBe(1); | ||||
|     expect.soft(await beforeShelvedFaultOne.count()).toBe(1); | ||||
|     expect.soft(await beforeShelvedFaultFour.count()).toBe(1); | ||||
|  | ||||
|         await utils.shelveMultipleFaults(page, 1, 4); | ||||
|     await utils.shelveMultipleFaults(page, 1, 4); | ||||
|  | ||||
|         // check it is removed from standard view | ||||
|         const afterShelvedFaultOne = utils.getFaultByName(page, shelvedFaultNameOne); | ||||
|         const afterShelvedFaultFour = utils.getFaultByName(page, shelvedFaultNameFour); | ||||
|         expect.soft(await afterShelvedFaultOne.count()).toBe(0); | ||||
|         expect.soft(await afterShelvedFaultFour.count()).toBe(0); | ||||
|     // check it is removed from standard view | ||||
|     const afterShelvedFaultOne = utils.getFaultByName(page, shelvedFaultNameOne); | ||||
|     const afterShelvedFaultFour = utils.getFaultByName(page, shelvedFaultNameFour); | ||||
|     expect.soft(await afterShelvedFaultOne.count()).toBe(0); | ||||
|     expect.soft(await afterShelvedFaultFour.count()).toBe(0); | ||||
|  | ||||
|         await utils.changeViewTo(page, 'shelved'); | ||||
|     await utils.changeViewTo(page, 'shelved'); | ||||
|  | ||||
|         const shelvedViewFaultOne = utils.getFaultByName(page, shelvedFaultNameOne); | ||||
|         const shelvedViewFaultFour = utils.getFaultByName(page, shelvedFaultNameFour); | ||||
|     const shelvedViewFaultOne = utils.getFaultByName(page, shelvedFaultNameOne); | ||||
|     const shelvedViewFaultFour = utils.getFaultByName(page, shelvedFaultNameFour); | ||||
|  | ||||
|         expect.soft(await shelvedViewFaultOne.count()).toBe(1); | ||||
|         expect.soft(await shelvedViewFaultFour.count()).toBe(1); | ||||
|     }); | ||||
|     expect.soft(await shelvedViewFaultOne.count()).toBe(1); | ||||
|     expect.soft(await shelvedViewFaultFour.count()).toBe(1); | ||||
|   }); | ||||
|  | ||||
|     test('Allows you to acknowledge multiple faults @unstable', async ({ page }) => { | ||||
|         const acknowledgedFaultNameTwo = await utils.getFaultName(page, 2); | ||||
|         const acknowledgedFaultNameFive = await utils.getFaultName(page, 5); | ||||
|   test('Allows you to acknowledge multiple faults @unstable', async ({ page }) => { | ||||
|     const acknowledgedFaultNameTwo = await utils.getFaultName(page, 2); | ||||
|     const acknowledgedFaultNameFive = await utils.getFaultName(page, 5); | ||||
|  | ||||
|         await utils.acknowledgeMultipleFaults(page, 2, 5); | ||||
|     await utils.acknowledgeMultipleFaults(page, 2, 5); | ||||
|  | ||||
|         const faultTwo = utils.getFault(page, 2); | ||||
|         const faultFive = utils.getFault(page, 5); | ||||
|     const faultTwo = utils.getFault(page, 2); | ||||
|     const faultFive = utils.getFault(page, 5); | ||||
|  | ||||
|         // check they have been acknowledged | ||||
|         await expect.soft(faultTwo).toHaveClass(/is-acknowledged/); | ||||
|         await expect.soft(faultFive).toHaveClass(/is-acknowledged/); | ||||
|     // check they have been acknowledged | ||||
|     await expect.soft(faultTwo).toHaveClass(/is-acknowledged/); | ||||
|     await expect.soft(faultFive).toHaveClass(/is-acknowledged/); | ||||
|  | ||||
|         await utils.changeViewTo(page, 'acknowledged'); | ||||
|     await utils.changeViewTo(page, 'acknowledged'); | ||||
|  | ||||
|         const acknowledgedViewFaultTwo = utils.getFaultByName(page, acknowledgedFaultNameTwo); | ||||
|         const acknowledgedViewFaultFive = utils.getFaultByName(page, acknowledgedFaultNameFive); | ||||
|     const acknowledgedViewFaultTwo = utils.getFaultByName(page, acknowledgedFaultNameTwo); | ||||
|     const acknowledgedViewFaultFive = utils.getFaultByName(page, acknowledgedFaultNameFive); | ||||
|  | ||||
|         expect.soft(await acknowledgedViewFaultTwo.count()).toBe(1); | ||||
|         expect.soft(await acknowledgedViewFaultFive.count()).toBe(1); | ||||
|     }); | ||||
|     expect.soft(await acknowledgedViewFaultTwo.count()).toBe(1); | ||||
|     expect.soft(await acknowledgedViewFaultFive.count()).toBe(1); | ||||
|   }); | ||||
|  | ||||
|     test('Allows you to search faults @unstable', async ({ page }) => { | ||||
|         const faultThreeNamespace = await utils.getFaultNamespace(page, 3); | ||||
|         const faultTwoName = await utils.getFaultName(page, 2); | ||||
|         const faultFiveTriggerTime = await utils.getFaultTriggerTime(page, 5); | ||||
|   test('Allows you to search faults @unstable', async ({ page }) => { | ||||
|     const faultThreeNamespace = await utils.getFaultNamespace(page, 3); | ||||
|     const faultTwoName = await utils.getFaultName(page, 2); | ||||
|     const faultFiveTriggerTime = await utils.getFaultTriggerTime(page, 5); | ||||
|  | ||||
|         // should be all faults (5) | ||||
|         let faultResultCount = await utils.getFaultResultCount(page); | ||||
|         expect.soft(faultResultCount).toEqual(5); | ||||
|     // should be all faults (5) | ||||
|     let faultResultCount = await utils.getFaultResultCount(page); | ||||
|     expect.soft(faultResultCount).toEqual(5); | ||||
|  | ||||
|         // search namespace | ||||
|         await utils.enterSearchTerm(page, faultThreeNamespace); | ||||
|     // search namespace | ||||
|     await utils.enterSearchTerm(page, faultThreeNamespace); | ||||
|  | ||||
|         faultResultCount = await utils.getFaultResultCount(page); | ||||
|         expect.soft(faultResultCount).toEqual(1); | ||||
|         expect.soft(await utils.getFaultNamespace(page, 1)).toEqual(faultThreeNamespace); | ||||
|     faultResultCount = await utils.getFaultResultCount(page); | ||||
|     expect.soft(faultResultCount).toEqual(1); | ||||
|     expect.soft(await utils.getFaultNamespace(page, 1)).toEqual(faultThreeNamespace); | ||||
|  | ||||
|         // all faults | ||||
|         await utils.clearSearch(page); | ||||
|         faultResultCount = await utils.getFaultResultCount(page); | ||||
|         expect.soft(faultResultCount).toEqual(5); | ||||
|     // all faults | ||||
|     await utils.clearSearch(page); | ||||
|     faultResultCount = await utils.getFaultResultCount(page); | ||||
|     expect.soft(faultResultCount).toEqual(5); | ||||
|  | ||||
|         // search name | ||||
|         await utils.enterSearchTerm(page, faultTwoName); | ||||
|     // search name | ||||
|     await utils.enterSearchTerm(page, faultTwoName); | ||||
|  | ||||
|         faultResultCount = await utils.getFaultResultCount(page); | ||||
|         expect.soft(faultResultCount).toEqual(1); | ||||
|         expect.soft(await utils.getFaultName(page, 1)).toEqual(faultTwoName); | ||||
|     faultResultCount = await utils.getFaultResultCount(page); | ||||
|     expect.soft(faultResultCount).toEqual(1); | ||||
|     expect.soft(await utils.getFaultName(page, 1)).toEqual(faultTwoName); | ||||
|  | ||||
|         // all faults | ||||
|         await utils.clearSearch(page); | ||||
|         faultResultCount = await utils.getFaultResultCount(page); | ||||
|         expect.soft(faultResultCount).toEqual(5); | ||||
|     // all faults | ||||
|     await utils.clearSearch(page); | ||||
|     faultResultCount = await utils.getFaultResultCount(page); | ||||
|     expect.soft(faultResultCount).toEqual(5); | ||||
|  | ||||
|         // search triggerTime | ||||
|         await utils.enterSearchTerm(page, faultFiveTriggerTime); | ||||
|     // search triggerTime | ||||
|     await utils.enterSearchTerm(page, faultFiveTriggerTime); | ||||
|  | ||||
|         faultResultCount = await utils.getFaultResultCount(page); | ||||
|         expect.soft(faultResultCount).toEqual(1); | ||||
|         expect.soft(await utils.getFaultTriggerTime(page, 1)).toEqual(faultFiveTriggerTime); | ||||
|     }); | ||||
|     faultResultCount = await utils.getFaultResultCount(page); | ||||
|     expect.soft(faultResultCount).toEqual(1); | ||||
|     expect.soft(await utils.getFaultTriggerTime(page, 1)).toEqual(faultFiveTriggerTime); | ||||
|   }); | ||||
|  | ||||
|     test('Allows you to sort faults @unstable', async ({ page }) => { | ||||
|         const highestSeverity = await utils.getHighestSeverity(page); | ||||
|         const lowestSeverity = await utils.getLowestSeverity(page); | ||||
|         const faultOneName = 'Example Fault 1'; | ||||
|         const faultFiveName = 'Example Fault 5'; | ||||
|         let firstFaultName = await utils.getFaultName(page, 1); | ||||
|   test('Allows you to sort faults @unstable', async ({ page }) => { | ||||
|     const highestSeverity = await utils.getHighestSeverity(page); | ||||
|     const lowestSeverity = await utils.getLowestSeverity(page); | ||||
|     const faultOneName = 'Example Fault 1'; | ||||
|     const faultFiveName = 'Example Fault 5'; | ||||
|     let firstFaultName = await utils.getFaultName(page, 1); | ||||
|  | ||||
|         expect.soft(firstFaultName).toEqual(faultOneName); | ||||
|     expect.soft(firstFaultName).toEqual(faultOneName); | ||||
|  | ||||
|         await utils.sortFaultsBy(page, 'oldest-first'); | ||||
|     await utils.sortFaultsBy(page, 'oldest-first'); | ||||
|  | ||||
|         firstFaultName = await utils.getFaultName(page, 1); | ||||
|         expect.soft(firstFaultName).toEqual(faultFiveName); | ||||
|     firstFaultName = await utils.getFaultName(page, 1); | ||||
|     expect.soft(firstFaultName).toEqual(faultFiveName); | ||||
|  | ||||
|         await utils.sortFaultsBy(page, 'severity'); | ||||
|  | ||||
|         const sortedHighestSeverity = await utils.getFaultSeverity(page, 1); | ||||
|         const sortedLowestSeverity = await utils.getFaultSeverity(page, 5); | ||||
|         expect.soft(sortedHighestSeverity).toEqual(highestSeverity); | ||||
|         expect.soft(sortedLowestSeverity).toEqual(lowestSeverity); | ||||
|     }); | ||||
|     await utils.sortFaultsBy(page, 'severity'); | ||||
|  | ||||
|     const sortedHighestSeverity = await utils.getFaultSeverity(page, 1); | ||||
|     const sortedLowestSeverity = await utils.getFaultSeverity(page, 5); | ||||
|     expect.soft(sortedHighestSeverity).toEqual(highestSeverity); | ||||
|     expect.soft(sortedLowestSeverity).toEqual(lowestSeverity); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| test.describe('The Fault Management Plugin without using example faults', () => { | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         await utils.navigateToFaultManagementWithoutExample(page); | ||||
|     }); | ||||
|   test.beforeEach(async ({ page }) => { | ||||
|     await utils.navigateToFaultManagementWithoutExample(page); | ||||
|   }); | ||||
|  | ||||
|     test('Shows no faults when no faults are provided @unstable', async ({ page }) => { | ||||
|         const faultCount = await page.locator('c-fault-mgmt__list').count(); | ||||
|   test('Shows no faults when no faults are provided @unstable', async ({ page }) => { | ||||
|     const faultCount = await page.locator('c-fault-mgmt__list').count(); | ||||
|  | ||||
|         expect.soft(faultCount).toEqual(0); | ||||
|     expect.soft(faultCount).toEqual(0); | ||||
|  | ||||
|         await utils.changeViewTo(page, 'acknowledged'); | ||||
|         const acknowledgedCount = await page.locator('c-fault-mgmt__list').count(); | ||||
|         expect.soft(acknowledgedCount).toEqual(0); | ||||
|     await utils.changeViewTo(page, 'acknowledged'); | ||||
|     const acknowledgedCount = await page.locator('c-fault-mgmt__list').count(); | ||||
|     expect.soft(acknowledgedCount).toEqual(0); | ||||
|  | ||||
|         await utils.changeViewTo(page, 'shelved'); | ||||
|         const shelvedCount = await page.locator('c-fault-mgmt__list').count(); | ||||
|         expect.soft(shelvedCount).toEqual(0); | ||||
|     }); | ||||
|     await utils.changeViewTo(page, 'shelved'); | ||||
|     const shelvedCount = await page.locator('c-fault-mgmt__list').count(); | ||||
|     expect.soft(shelvedCount).toEqual(0); | ||||
|   }); | ||||
|  | ||||
|     test('Will return no faults when searching @unstable', async ({ page }) => { | ||||
|         await utils.enterSearchTerm(page, 'fault'); | ||||
|   test('Will return no faults when searching @unstable', async ({ page }) => { | ||||
|     await utils.enterSearchTerm(page, 'fault'); | ||||
|  | ||||
|         const faultCount = await page.locator('c-fault-mgmt__list').count(); | ||||
|     const faultCount = await page.locator('c-fault-mgmt__list').count(); | ||||
|  | ||||
|         expect.soft(faultCount).toEqual(0); | ||||
|     }); | ||||
|     expect.soft(faultCount).toEqual(0); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -24,130 +24,138 @@ const { test, expect } = require('../../../../pluginFixtures'); | ||||
| const { createDomainObjectWithDefaults } = require('../../../../appActions'); | ||||
|  | ||||
| test.describe('Flexible Layout', () => { | ||||
|     let sineWaveObject; | ||||
|     let clockObject; | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|   let sineWaveObject; | ||||
|   let clockObject; | ||||
|   test.beforeEach(async ({ page }) => { | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|  | ||||
|         // Create Sine Wave Generator | ||||
|         sineWaveObject = await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Sine Wave Generator' | ||||
|         }); | ||||
|  | ||||
|         // Create Clock Object | ||||
|         clockObject = await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Clock' | ||||
|         }); | ||||
|     // Create Sine Wave Generator | ||||
|     sineWaveObject = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Sine Wave Generator' | ||||
|     }); | ||||
|     test('panes have the appropriate draggable attribute while in Edit and Browse modes', async ({ page }) => { | ||||
|         const treePane = page.getByRole('tree', { | ||||
|             name: 'Main Tree' | ||||
|         }); | ||||
|         const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', { | ||||
|             name: new RegExp(sineWaveObject.name) | ||||
|         }); | ||||
|         const clockTreeItem = treePane.getByRole('treeitem', { | ||||
|             name: new RegExp(clockObject.name) | ||||
|         }); | ||||
|         // Create a Flexible Layout | ||||
|         await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Flexible Layout' | ||||
|         }); | ||||
|         // Edit Flexible Layout | ||||
|         await page.locator('[title="Edit"]').click(); | ||||
|  | ||||
|         // Expand the 'My Items' folder in the left tree | ||||
|         await page.locator('.c-tree__item__view-control.c-disclosure-triangle').first().click(); | ||||
|         // Add the Sine Wave Generator and Clock to the Flexible Layout | ||||
|         await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first()); | ||||
|         await clockTreeItem.dragTo(page.locator('.c-fl__container.is-empty')); | ||||
|         // Check that panes can be dragged while Flexible Layout is in Edit mode | ||||
|         let dragWrapper = page.locator('.c-fl-container__frames-holder .c-fl-frame__drag-wrapper').first(); | ||||
|         await expect(dragWrapper).toHaveAttribute('draggable', 'true'); | ||||
|         // Save Flexible Layout | ||||
|         await page.locator('button[title="Save"]').click(); | ||||
|         await page.locator('text=Save and Finish Editing').click(); | ||||
|         // Check that panes are not draggable while Flexible Layout is in Browse mode | ||||
|         dragWrapper = page.locator('.c-fl-container__frames-holder .c-fl-frame__drag-wrapper').first(); | ||||
|         await expect(dragWrapper).toHaveAttribute('draggable', 'false'); | ||||
|     // Create Clock Object | ||||
|     clockObject = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Clock' | ||||
|     }); | ||||
|     test('items in a flexible layout can be removed with object tree context menu when viewing the flexible layout', async ({ page }) => { | ||||
|         const treePane = page.getByRole('tree', { | ||||
|             name: 'Main Tree' | ||||
|         }); | ||||
|         const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', { | ||||
|             name: new RegExp(sineWaveObject.name) | ||||
|         }); | ||||
|         // Create a Display Layout | ||||
|         await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Flexible Layout' | ||||
|         }); | ||||
|         // Edit Flexible Layout | ||||
|         await page.locator('[title="Edit"]').click(); | ||||
|  | ||||
|         // Expand the 'My Items' folder in the left tree | ||||
|         await page.locator('.c-tree__item__view-control.c-disclosure-triangle').first().click(); | ||||
|         // Add the Sine Wave Generator to the Flexible Layout and save changes | ||||
|         await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first()); | ||||
|         await page.locator('button[title="Save"]').click(); | ||||
|         await page.locator('text=Save and Finish Editing').click(); | ||||
|  | ||||
|         expect.soft(await page.locator('.c-fl-container__frame').count()).toEqual(1); | ||||
|  | ||||
|         // Expand the Flexible Layout so we can remove the sine wave generator | ||||
|         await page.locator('.c-tree__item.is-navigated-object .c-disclosure-triangle').click(); | ||||
|  | ||||
|         // Bring up context menu and remove | ||||
|         await sineWaveGeneratorTreeItem.first().click({ button: 'right' }); | ||||
|         await page.locator('li[role="menuitem"]:has-text("Remove")').click(); | ||||
|         await page.locator('button:has-text("OK")').click(); | ||||
|  | ||||
|         // Verify that the item has been removed from the layout | ||||
|         expect(await page.locator('.c-fl-container__frame').count()).toEqual(0); | ||||
|   }); | ||||
|   test('panes have the appropriate draggable attribute while in Edit and Browse modes', async ({ | ||||
|     page | ||||
|   }) => { | ||||
|     const treePane = page.getByRole('tree', { | ||||
|       name: 'Main Tree' | ||||
|     }); | ||||
|     test('items in a flexible layout can be removed with object tree context menu when viewing another item', async ({ page }) => { | ||||
|         test.info().annotations.push({ | ||||
|             type: 'issue', | ||||
|             description: 'https://github.com/nasa/openmct/issues/3117' | ||||
|         }); | ||||
|         const treePane = page.getByRole('tree', { | ||||
|             name: 'Main Tree' | ||||
|         }); | ||||
|         const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', { | ||||
|             name: new RegExp(sineWaveObject.name) | ||||
|         }); | ||||
|  | ||||
|         // Create a Flexible Layout | ||||
|         const flexibleLayout = await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Flexible Layout' | ||||
|         }); | ||||
|         // Edit Flexible Layout | ||||
|         await page.locator('[title="Edit"]').click(); | ||||
|  | ||||
|         // Expand the 'My Items' folder in the left tree | ||||
|         await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click(); | ||||
|         // Add the Sine Wave Generator to the Flexible Layout and save changes | ||||
|         await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first()); | ||||
|         await page.locator('button[title="Save"]').click(); | ||||
|         await page.locator('text=Save and Finish Editing').click(); | ||||
|  | ||||
|         expect.soft(await page.locator('.c-fl-container__frame').count()).toEqual(1); | ||||
|  | ||||
|         // Expand the Flexible Layout so we can remove the sine wave generator | ||||
|         await page.locator('.c-tree__item.is-navigated-object .c-disclosure-triangle').click(); | ||||
|  | ||||
|         // Go to the original Sine Wave Generator to navigate away from the Flexible Layout | ||||
|         await page.goto(sineWaveObject.url); | ||||
|  | ||||
|         // Bring up context menu and remove | ||||
|         await sineWaveGeneratorTreeItem.first().click({ button: 'right' }); | ||||
|         await page.locator('li[role="menuitem"]:has-text("Remove")').click(); | ||||
|         await page.locator('button:has-text("OK")').click(); | ||||
|  | ||||
|         // navigate back to the display layout to confirm it has been removed | ||||
|         await page.goto(flexibleLayout.url); | ||||
|  | ||||
|         // Verify that the item has been removed from the layout | ||||
|         expect(await page.locator('.c-fl-container__frame').count()).toEqual(0); | ||||
|     const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', { | ||||
|       name: new RegExp(sineWaveObject.name) | ||||
|     }); | ||||
|     const clockTreeItem = treePane.getByRole('treeitem', { | ||||
|       name: new RegExp(clockObject.name) | ||||
|     }); | ||||
|     // Create a Flexible Layout | ||||
|     await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Flexible Layout' | ||||
|     }); | ||||
|     // Edit Flexible Layout | ||||
|     await page.locator('[title="Edit"]').click(); | ||||
|  | ||||
|     // Expand the 'My Items' folder in the left tree | ||||
|     await page.locator('.c-tree__item__view-control.c-disclosure-triangle').first().click(); | ||||
|     // Add the Sine Wave Generator and Clock to the Flexible Layout | ||||
|     await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first()); | ||||
|     await clockTreeItem.dragTo(page.locator('.c-fl__container.is-empty')); | ||||
|     // Check that panes can be dragged while Flexible Layout is in Edit mode | ||||
|     let dragWrapper = page | ||||
|       .locator('.c-fl-container__frames-holder .c-fl-frame__drag-wrapper') | ||||
|       .first(); | ||||
|     await expect(dragWrapper).toHaveAttribute('draggable', 'true'); | ||||
|     // Save Flexible Layout | ||||
|     await page.locator('button[title="Save"]').click(); | ||||
|     await page.locator('text=Save and Finish Editing').click(); | ||||
|     // Check that panes are not draggable while Flexible Layout is in Browse mode | ||||
|     dragWrapper = page.locator('.c-fl-container__frames-holder .c-fl-frame__drag-wrapper').first(); | ||||
|     await expect(dragWrapper).toHaveAttribute('draggable', 'false'); | ||||
|   }); | ||||
|   test('items in a flexible layout can be removed with object tree context menu when viewing the flexible layout', async ({ | ||||
|     page | ||||
|   }) => { | ||||
|     const treePane = page.getByRole('tree', { | ||||
|       name: 'Main Tree' | ||||
|     }); | ||||
|     const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', { | ||||
|       name: new RegExp(sineWaveObject.name) | ||||
|     }); | ||||
|     // Create a Display Layout | ||||
|     await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Flexible Layout' | ||||
|     }); | ||||
|     // Edit Flexible Layout | ||||
|     await page.locator('[title="Edit"]').click(); | ||||
|  | ||||
|     // Expand the 'My Items' folder in the left tree | ||||
|     await page.locator('.c-tree__item__view-control.c-disclosure-triangle').first().click(); | ||||
|     // Add the Sine Wave Generator to the Flexible Layout and save changes | ||||
|     await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first()); | ||||
|     await page.locator('button[title="Save"]').click(); | ||||
|     await page.locator('text=Save and Finish Editing').click(); | ||||
|  | ||||
|     expect.soft(await page.locator('.c-fl-container__frame').count()).toEqual(1); | ||||
|  | ||||
|     // Expand the Flexible Layout so we can remove the sine wave generator | ||||
|     await page.locator('.c-tree__item.is-navigated-object .c-disclosure-triangle').click(); | ||||
|  | ||||
|     // Bring up context menu and remove | ||||
|     await sineWaveGeneratorTreeItem.first().click({ button: 'right' }); | ||||
|     await page.locator('li[role="menuitem"]:has-text("Remove")').click(); | ||||
|     await page.locator('button:has-text("OK")').click(); | ||||
|  | ||||
|     // Verify that the item has been removed from the layout | ||||
|     expect(await page.locator('.c-fl-container__frame').count()).toEqual(0); | ||||
|   }); | ||||
|   test('items in a flexible layout can be removed with object tree context menu when viewing another item', async ({ | ||||
|     page | ||||
|   }) => { | ||||
|     test.info().annotations.push({ | ||||
|       type: 'issue', | ||||
|       description: 'https://github.com/nasa/openmct/issues/3117' | ||||
|     }); | ||||
|     const treePane = page.getByRole('tree', { | ||||
|       name: 'Main Tree' | ||||
|     }); | ||||
|     const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', { | ||||
|       name: new RegExp(sineWaveObject.name) | ||||
|     }); | ||||
|  | ||||
|     // Create a Flexible Layout | ||||
|     const flexibleLayout = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Flexible Layout' | ||||
|     }); | ||||
|     // Edit Flexible Layout | ||||
|     await page.locator('[title="Edit"]').click(); | ||||
|  | ||||
|     // Expand the 'My Items' folder in the left tree | ||||
|     await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click(); | ||||
|     // Add the Sine Wave Generator to the Flexible Layout and save changes | ||||
|     await sineWaveGeneratorTreeItem.dragTo(page.locator('.c-fl__container.is-empty').first()); | ||||
|     await page.locator('button[title="Save"]').click(); | ||||
|     await page.locator('text=Save and Finish Editing').click(); | ||||
|  | ||||
|     expect.soft(await page.locator('.c-fl-container__frame').count()).toEqual(1); | ||||
|  | ||||
|     // Expand the Flexible Layout so we can remove the sine wave generator | ||||
|     await page.locator('.c-tree__item.is-navigated-object .c-disclosure-triangle').click(); | ||||
|  | ||||
|     // Go to the original Sine Wave Generator to navigate away from the Flexible Layout | ||||
|     await page.goto(sineWaveObject.url); | ||||
|  | ||||
|     // Bring up context menu and remove | ||||
|     await sineWaveGeneratorTreeItem.first().click({ button: 'right' }); | ||||
|     await page.locator('li[role="menuitem"]:has-text("Remove")').click(); | ||||
|     await page.locator('button:has-text("OK")').click(); | ||||
|  | ||||
|     // navigate back to the display layout to confirm it has been removed | ||||
|     await page.goto(flexibleLayout.url); | ||||
|  | ||||
|     // Verify that the item has been removed from the layout | ||||
|     expect(await page.locator('.c-fl-container__frame').count()).toEqual(0); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -21,104 +21,116 @@ | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* | ||||
| * This test suite is dedicated to testing the Gauge component. | ||||
| */ | ||||
|  * This test suite is dedicated to testing the Gauge component. | ||||
|  */ | ||||
|  | ||||
| const { test, expect } = require('../../../../pluginFixtures'); | ||||
| const { createDomainObjectWithDefaults } = require('../../../../appActions'); | ||||
| const uuid = require('uuid').v4; | ||||
|  | ||||
| test.describe('Gauge', () => { | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         // Open a browser, navigate to the main page, and wait until all networkevents to resolve | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|   test.beforeEach(async ({ page }) => { | ||||
|     // Open a browser, navigate to the main page, and wait until all networkevents to resolve | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|   }); | ||||
|  | ||||
|   test('Can add and remove telemetry sources @unstable', async ({ page }) => { | ||||
|     // Create the gauge with defaults | ||||
|     const gauge = await createDomainObjectWithDefaults(page, { type: 'Gauge' }); | ||||
|     const editButtonLocator = page.locator('button[title="Edit"]'); | ||||
|     const saveButtonLocator = page.locator('button[title="Save"]'); | ||||
|  | ||||
|     // Create a sine wave generator within the gauge | ||||
|     const swg1 = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Sine Wave Generator', | ||||
|       name: `swg-${uuid()}`, | ||||
|       parent: gauge.uuid | ||||
|     }); | ||||
|  | ||||
|     test('Can add and remove telemetry sources @unstable', async ({ page }) => { | ||||
|         // Create the gauge with defaults | ||||
|         const gauge = await createDomainObjectWithDefaults(page, { type: 'Gauge' }); | ||||
|         const editButtonLocator = page.locator('button[title="Edit"]'); | ||||
|         const saveButtonLocator = page.locator('button[title="Save"]'); | ||||
|     // Navigate to the gauge and verify that | ||||
|     // the SWG appears in the elements pool | ||||
|     await page.goto(gauge.url); | ||||
|     await editButtonLocator.click(); | ||||
|     await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeVisible(); | ||||
|     await saveButtonLocator.click(); | ||||
|     await page.locator('li[title="Save and Finish Editing"]').click(); | ||||
|  | ||||
|         // Create a sine wave generator within the gauge | ||||
|         const swg1 = await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Sine Wave Generator', | ||||
|             name: `swg-${uuid()}`, | ||||
|             parent: gauge.uuid | ||||
|         }); | ||||
|  | ||||
|         // Navigate to the gauge and verify that | ||||
|         // the SWG appears in the elements pool | ||||
|         await page.goto(gauge.url); | ||||
|         await editButtonLocator.click(); | ||||
|         await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeVisible(); | ||||
|         await saveButtonLocator.click(); | ||||
|         await page.locator('li[title="Save and Finish Editing"]').click(); | ||||
|  | ||||
|         // Create another sine wave generator within the gauge | ||||
|         const swg2 = await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Sine Wave Generator', | ||||
|             name: `swg-${uuid()}`, | ||||
|             parent: gauge.uuid | ||||
|         }); | ||||
|  | ||||
|         // Verify that the 'Replace telemetry source' modal appears and accept it | ||||
|         await expect.soft(page.locator('text=This action will replace the current telemetry source. Do you want to continue?')).toBeVisible(); | ||||
|         await page.click('text=Ok'); | ||||
|  | ||||
|         // Navigate to the gauge and verify that the new SWG | ||||
|         // appears in the elements pool and the old one is gone | ||||
|         await page.goto(gauge.url); | ||||
|         await editButtonLocator.click(); | ||||
|         await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeHidden(); | ||||
|         await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg2.name}`)).toBeVisible(); | ||||
|         await saveButtonLocator.click(); | ||||
|  | ||||
|         // Right click on the new SWG in the elements pool and delete it | ||||
|         await page.locator(`#inspector-elements-tree >> text=${swg2.name}`).click({ | ||||
|             button: 'right' | ||||
|         }); | ||||
|         await page.locator('li[title="Remove this object from its containing object."]').click(); | ||||
|  | ||||
|         // Verify that the 'Remove object' confirmation modal appears and accept it | ||||
|         await expect.soft(page.locator('text=Warning! This action will remove this object. Are you sure you want to continue?')).toBeVisible(); | ||||
|         await page.click('text=Ok'); | ||||
|  | ||||
|         // Verify that the elements pool shows no elements | ||||
|         await expect(page.locator('text="No contained elements"')).toBeVisible(); | ||||
|     // Create another sine wave generator within the gauge | ||||
|     const swg2 = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Sine Wave Generator', | ||||
|       name: `swg-${uuid()}`, | ||||
|       parent: gauge.uuid | ||||
|     }); | ||||
|     test('Can create a non-default Gauge', async ({ page }) => { | ||||
|         test.info().annotations.push({ | ||||
|             type: 'issue', | ||||
|             description: 'https://github.com/nasa/openmct/issues/5356' | ||||
|         }); | ||||
|         //Click the Create button | ||||
|         await page.click('button:has-text("Create")'); | ||||
|  | ||||
|         // Click the object specified by 'type' | ||||
|         await page.click(`li[role='menuitem']:text("Gauge")`); | ||||
|         // FIXME: We need better selectors for these custom form controls | ||||
|         const displayCurrentValueSwitch = page.locator('.c-toggle-switch__slider >> nth=0'); | ||||
|         await displayCurrentValueSwitch.setChecked(false); | ||||
|         await page.click('button[aria-label="Save"]'); | ||||
|     // Verify that the 'Replace telemetry source' modal appears and accept it | ||||
|     await expect | ||||
|       .soft( | ||||
|         page.locator( | ||||
|           'text=This action will replace the current telemetry source. Do you want to continue?' | ||||
|         ) | ||||
|       ) | ||||
|       .toBeVisible(); | ||||
|     await page.click('text=Ok'); | ||||
|  | ||||
|         // TODO: Verify changes in the UI | ||||
|     // Navigate to the gauge and verify that the new SWG | ||||
|     // appears in the elements pool and the old one is gone | ||||
|     await page.goto(gauge.url); | ||||
|     await editButtonLocator.click(); | ||||
|     await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeHidden(); | ||||
|     await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg2.name}`)).toBeVisible(); | ||||
|     await saveButtonLocator.click(); | ||||
|  | ||||
|     // Right click on the new SWG in the elements pool and delete it | ||||
|     await page.locator(`#inspector-elements-tree >> text=${swg2.name}`).click({ | ||||
|       button: 'right' | ||||
|     }); | ||||
|     test('Can edit a single Gauge-specific property', async ({ page }) => { | ||||
|         test.info().annotations.push({ | ||||
|             type: 'issue', | ||||
|             description: 'https://github.com/nasa/openmct/issues/5985' | ||||
|         }); | ||||
|     await page.locator('li[title="Remove this object from its containing object."]').click(); | ||||
|  | ||||
|         // Create the gauge with defaults | ||||
|         await createDomainObjectWithDefaults(page, { type: 'Gauge' }); | ||||
|         await page.click('button[title="More options"]'); | ||||
|         await page.click('li[role="menuitem"]:has-text("Edit Properties")'); | ||||
|         // FIXME: We need better selectors for these custom form controls | ||||
|         const displayCurrentValueSwitch = page.locator('.c-toggle-switch__slider >> nth=0'); | ||||
|         await displayCurrentValueSwitch.setChecked(false); | ||||
|         await page.click('button[aria-label="Save"]'); | ||||
|     // Verify that the 'Remove object' confirmation modal appears and accept it | ||||
|     await expect | ||||
|       .soft( | ||||
|         page.locator( | ||||
|           'text=Warning! This action will remove this object. Are you sure you want to continue?' | ||||
|         ) | ||||
|       ) | ||||
|       .toBeVisible(); | ||||
|     await page.click('text=Ok'); | ||||
|  | ||||
|         // TODO: Verify changes in the UI | ||||
|     // Verify that the elements pool shows no elements | ||||
|     await expect(page.locator('text="No contained elements"')).toBeVisible(); | ||||
|   }); | ||||
|   test('Can create a non-default Gauge', async ({ page }) => { | ||||
|     test.info().annotations.push({ | ||||
|       type: 'issue', | ||||
|       description: 'https://github.com/nasa/openmct/issues/5356' | ||||
|     }); | ||||
|     //Click the Create button | ||||
|     await page.click('button:has-text("Create")'); | ||||
|  | ||||
|     // Click the object specified by 'type' | ||||
|     await page.click(`li[role='menuitem']:text("Gauge")`); | ||||
|     // FIXME: We need better selectors for these custom form controls | ||||
|     const displayCurrentValueSwitch = page.locator('.c-toggle-switch__slider >> nth=0'); | ||||
|     await displayCurrentValueSwitch.setChecked(false); | ||||
|     await page.click('button[aria-label="Save"]'); | ||||
|  | ||||
|     // TODO: Verify changes in the UI | ||||
|   }); | ||||
|   test('Can edit a single Gauge-specific property', async ({ page }) => { | ||||
|     test.info().annotations.push({ | ||||
|       type: 'issue', | ||||
|       description: 'https://github.com/nasa/openmct/issues/5985' | ||||
|     }); | ||||
|  | ||||
|     // Create the gauge with defaults | ||||
|     await createDomainObjectWithDefaults(page, { type: 'Gauge' }); | ||||
|     await page.click('button[title="More options"]'); | ||||
|     await page.click('li[role="menuitem"]:has-text("Edit Properties")'); | ||||
|     // FIXME: We need better selectors for these custom form controls | ||||
|     const displayCurrentValueSwitch = page.locator('.c-toggle-switch__slider >> nth=0'); | ||||
|     await displayCurrentValueSwitch.setChecked(false); | ||||
|     await page.click('button[aria-label="Save"]'); | ||||
|  | ||||
|     // TODO: Verify changes in the UI | ||||
|   }); | ||||
| }); | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -29,22 +29,31 @@ This test suite is dedicated to tests which verify the basic operations surround | ||||
| const { test, expect } = require('../../../../baseFixtures'); | ||||
|  | ||||
| test.describe('ExportAsJSON', () => { | ||||
|     test.fixme('Create a basic object and verify that it can be exported as JSON from Tree', async ({ page }) => { | ||||
|         //Create domain object | ||||
|         //Save Domain Object | ||||
|         //Verify that the newly created domain object can be exported as JSON from the Tree | ||||
|     }); | ||||
|     test.fixme('Create a basic object and verify that it can be exported as JSON from 3 dot menu', async ({ page }) => { | ||||
|         //Create domain object | ||||
|         //Save Domain Object | ||||
|         //Verify that the newly created domain object can be exported as JSON from the 3 dot menu | ||||
|     }); | ||||
|     test.fixme('Verify that a nested Object can be exported as JSON', async ({ page }) => { | ||||
|         // Create 2 objects with hierarchy | ||||
|         // Export as JSON | ||||
|         // Verify Hiearchy | ||||
|     }); | ||||
|     test.fixme('Verify that the ExportAsJSON dropdown does not appear for the item X', async ({ page }) => { | ||||
|         // Other than non-persistible objects | ||||
|     }); | ||||
|   test.fixme( | ||||
|     'Create a basic object and verify that it can be exported as JSON from Tree', | ||||
|     async ({ page }) => { | ||||
|       //Create domain object | ||||
|       //Save Domain Object | ||||
|       //Verify that the newly created domain object can be exported as JSON from the Tree | ||||
|     } | ||||
|   ); | ||||
|   test.fixme( | ||||
|     'Create a basic object and verify that it can be exported as JSON from 3 dot menu', | ||||
|     async ({ page }) => { | ||||
|       //Create domain object | ||||
|       //Save Domain Object | ||||
|       //Verify that the newly created domain object can be exported as JSON from the 3 dot menu | ||||
|     } | ||||
|   ); | ||||
|   test.fixme('Verify that a nested Object can be exported as JSON', async ({ page }) => { | ||||
|     // Create 2 objects with hierarchy | ||||
|     // Export as JSON | ||||
|     // Verify Hiearchy | ||||
|   }); | ||||
|   test.fixme( | ||||
|     'Verify that the ExportAsJSON dropdown does not appear for the item X', | ||||
|     async ({ page }) => { | ||||
|       // Other than non-persistible objects | ||||
|     } | ||||
|   ); | ||||
| }); | ||||
|   | ||||
| @@ -1,48 +1,54 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2023, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* | ||||
| This test suite is dedicated to tests which verify the basic operations surrounding importAsJSON. | ||||
| */ | ||||
|  | ||||
| // FIXME: Remove this eslint exception once tests are implemented | ||||
| // eslint-disable-next-line no-unused-vars | ||||
| const { test, expect } = require('../../../../baseFixtures'); | ||||
|  | ||||
| test.describe('ExportAsJSON', () => { | ||||
|     test.fixme('Verify that domain object can be importAsJSON from Tree', async ({ page }) => { | ||||
|         //Verify that an testdata JSON file can be imported from Tree | ||||
|         //Verify correctness of imported domain object | ||||
|     }); | ||||
|     test.fixme('Verify that domain object can be importAsJSON from 3 dot menu on folder', async ({ page }) => { | ||||
|         //Verify that an testdata JSON file can be imported from 3 dot menu on folder domain object | ||||
|         //Verify correctness of imported domain object | ||||
|     }); | ||||
|     test.fixme('Verify that a nested Objects can be importAsJSON', async ({ page }) => { | ||||
|         // Testdata with hierarchy | ||||
|         // ImportAsJSON on Tree | ||||
|         // Verify Hierarchy | ||||
|     }); | ||||
|     test.fixme('Verify that the ImportAsJSON dropdown does not appear for the item X', async ({ page }) => { | ||||
|         // Other than non-persistible objects | ||||
|     }); | ||||
| }); | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2023, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* | ||||
| This test suite is dedicated to tests which verify the basic operations surrounding importAsJSON. | ||||
| */ | ||||
|  | ||||
| // FIXME: Remove this eslint exception once tests are implemented | ||||
| // eslint-disable-next-line no-unused-vars | ||||
| const { test, expect } = require('../../../../baseFixtures'); | ||||
|  | ||||
| test.describe('ExportAsJSON', () => { | ||||
|   test.fixme('Verify that domain object can be importAsJSON from Tree', async ({ page }) => { | ||||
|     //Verify that an testdata JSON file can be imported from Tree | ||||
|     //Verify correctness of imported domain object | ||||
|   }); | ||||
|   test.fixme( | ||||
|     'Verify that domain object can be importAsJSON from 3 dot menu on folder', | ||||
|     async ({ page }) => { | ||||
|       //Verify that an testdata JSON file can be imported from 3 dot menu on folder domain object | ||||
|       //Verify correctness of imported domain object | ||||
|     } | ||||
|   ); | ||||
|   test.fixme('Verify that a nested Objects can be importAsJSON', async ({ page }) => { | ||||
|     // Testdata with hierarchy | ||||
|     // ImportAsJSON on Tree | ||||
|     // Verify Hierarchy | ||||
|   }); | ||||
|   test.fixme( | ||||
|     'Verify that the ImportAsJSON dropdown does not appear for the item X', | ||||
|     async ({ page }) => { | ||||
|       // Other than non-persistible objects | ||||
|     } | ||||
|   ); | ||||
| }); | ||||
|   | ||||
| @@ -21,189 +21,201 @@ | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| const { test, expect } = require('../../../../pluginFixtures'); | ||||
| const { createDomainObjectWithDefaults, setStartOffset, setFixedTimeMode, setRealTimeMode, selectInspectorTab } = require('../../../../appActions'); | ||||
| const { | ||||
|   createDomainObjectWithDefaults, | ||||
|   setStartOffset, | ||||
|   setFixedTimeMode, | ||||
|   setRealTimeMode, | ||||
|   selectInspectorTab | ||||
| } = require('../../../../appActions'); | ||||
|  | ||||
| test.describe('Testing LAD table configuration', () => { | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|   test.beforeEach(async ({ page }) => { | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|  | ||||
|         // Create LAD table | ||||
|         const ladTable = await createDomainObjectWithDefaults(page, { | ||||
|             type: 'LAD Table', | ||||
|             name: "Test LAD Table" | ||||
|         }); | ||||
|  | ||||
|         // Create Sine Wave Generator | ||||
|         await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Sine Wave Generator', | ||||
|             name: "Test Sine Wave Generator", | ||||
|             parent: ladTable.uuid | ||||
|         }); | ||||
|  | ||||
|         await page.goto(ladTable.url); | ||||
|     }); | ||||
|     test('in edit mode, LAD Tables provide ability to hide columns', async ({ page }) => { | ||||
|         // Edit LAD table | ||||
|         await page.locator('[title="Edit"]').click(); | ||||
|  | ||||
|         // // Expand the 'My Items' folder in the left tree | ||||
|         // await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click(); | ||||
|         // // Add the Sine Wave Generator to the LAD table and save changes | ||||
|         // await page.dragAndDrop('role=treeitem[name=/Test Sine Wave Generator/]', '.c-lad-table-wrapper'); | ||||
|         // select configuration tab in inspector | ||||
|         await selectInspectorTab(page, 'LAD Table Configuration'); | ||||
|  | ||||
|         // make sure headers are visible initially | ||||
|         await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible(); | ||||
|         await expect(page.getByRole('cell', { name: 'Units' })).toBeVisible(); | ||||
|         await expect(page.getByRole('cell', { name: 'Type' })).toBeVisible(); | ||||
|  | ||||
|         // hide timestamp column | ||||
|         await page.getByLabel('Timestamp').uncheck(); | ||||
|         await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeHidden(); | ||||
|         await expect(page.getByRole('cell', { name: 'Units' })).toBeVisible(); | ||||
|         await expect(page.getByRole('cell', { name: 'Type' })).toBeVisible(); | ||||
|  | ||||
|         // hide units & type column | ||||
|         await page.getByLabel('Units').uncheck(); | ||||
|         await page.getByLabel('Type').uncheck(); | ||||
|         await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeHidden(); | ||||
|         await expect(page.getByRole('cell', { name: 'Units' })).toBeHidden(); | ||||
|         await expect(page.getByRole('cell', { name: 'Type' })).toBeHidden(); | ||||
|  | ||||
|         // save and reload and verify they columns are still hidden | ||||
|         await page.locator('button[title="Save"]').click(); | ||||
|         await page.locator('text=Save and Finish Editing').click(); | ||||
|         await page.reload(); | ||||
|         await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeHidden(); | ||||
|         await expect(page.getByRole('cell', { name: 'Units' })).toBeHidden(); | ||||
|         await expect(page.getByRole('cell', { name: 'Type' })).toBeHidden(); | ||||
|  | ||||
|         // Edit LAD table | ||||
|         await page.locator('[title="Edit"]').click(); | ||||
|         await selectInspectorTab(page, 'LAD Table Configuration'); | ||||
|  | ||||
|         // show timestamp column | ||||
|         await page.getByLabel('Timestamp').check(); | ||||
|         await expect(page.getByRole('cell', { name: 'Units' })).toBeHidden(); | ||||
|         await expect(page.getByRole('cell', { name: 'Type' })).toBeHidden(); | ||||
|         await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible(); | ||||
|  | ||||
|         // save and reload and make sure only timestamp is still visible | ||||
|         await page.locator('button[title="Save"]').click(); | ||||
|         await page.locator('text=Save and Finish Editing').click(); | ||||
|         await page.reload(); | ||||
|         await expect(page.getByRole('cell', { name: 'Units' })).toBeHidden(); | ||||
|         await expect(page.getByRole('cell', { name: 'Type' })).toBeHidden(); | ||||
|         await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible(); | ||||
|  | ||||
|         // Edit LAD table | ||||
|         await page.locator('[title="Edit"]').click(); | ||||
|         await selectInspectorTab(page, 'LAD Table Configuration'); | ||||
|  | ||||
|         // show units and type columns | ||||
|         await page.getByLabel('Units').check(); | ||||
|         await page.getByLabel('Type').check(); | ||||
|         await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible(); | ||||
|         await expect(page.getByRole('cell', { name: 'Units' })).toBeVisible(); | ||||
|         await expect(page.getByRole('cell', { name: 'Type' })).toBeVisible(); | ||||
|  | ||||
|         // save and reload and make sure all columns are still visible | ||||
|         await page.locator('button[title="Save"]').click(); | ||||
|         await page.locator('text=Save and Finish Editing').click(); | ||||
|         await page.reload(); | ||||
|         await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible(); | ||||
|         await expect(page.getByRole('cell', { name: 'Units' })).toBeVisible(); | ||||
|         await expect(page.getByRole('cell', { name: 'Type' })).toBeVisible(); | ||||
|     // Create LAD table | ||||
|     const ladTable = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'LAD Table', | ||||
|       name: 'Test LAD Table' | ||||
|     }); | ||||
|  | ||||
|     test('LAD Tables don\'t allow selection of rows but does show context click menus', async ({ page }) => { | ||||
|         const cell = await page.locator('.js-first-data'); | ||||
|         const userSelectable = await cell.evaluate((el) => { | ||||
|             return window.getComputedStyle(el).getPropertyValue('user-select'); | ||||
|         }); | ||||
|  | ||||
|         expect(userSelectable).toBe('none'); | ||||
|         // Right-click on the LAD table row | ||||
|         await cell.click({ | ||||
|             button: 'right' | ||||
|         }); | ||||
|         const menuOptions = page.locator('.c-menu ul'); | ||||
|         await expect.soft(menuOptions).toContainText('View Full Datum'); | ||||
|         await expect.soft(menuOptions).toContainText('View Historical Data'); | ||||
|         await expect.soft(menuOptions).toContainText('Remove'); | ||||
|         // await page.locator('li[title="Remove this object from its containing object."]').click(); | ||||
|     // Create Sine Wave Generator | ||||
|     await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Sine Wave Generator', | ||||
|       name: 'Test Sine Wave Generator', | ||||
|       parent: ladTable.uuid | ||||
|     }); | ||||
|  | ||||
|     await page.goto(ladTable.url); | ||||
|   }); | ||||
|   test('in edit mode, LAD Tables provide ability to hide columns', async ({ page }) => { | ||||
|     // Edit LAD table | ||||
|     await page.locator('[title="Edit"]').click(); | ||||
|  | ||||
|     // // Expand the 'My Items' folder in the left tree | ||||
|     // await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click(); | ||||
|     // // Add the Sine Wave Generator to the LAD table and save changes | ||||
|     // await page.dragAndDrop('role=treeitem[name=/Test Sine Wave Generator/]', '.c-lad-table-wrapper'); | ||||
|     // select configuration tab in inspector | ||||
|     await selectInspectorTab(page, 'LAD Table Configuration'); | ||||
|  | ||||
|     // make sure headers are visible initially | ||||
|     await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible(); | ||||
|     await expect(page.getByRole('cell', { name: 'Units' })).toBeVisible(); | ||||
|     await expect(page.getByRole('cell', { name: 'Type' })).toBeVisible(); | ||||
|  | ||||
|     // hide timestamp column | ||||
|     await page.getByLabel('Timestamp').uncheck(); | ||||
|     await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeHidden(); | ||||
|     await expect(page.getByRole('cell', { name: 'Units' })).toBeVisible(); | ||||
|     await expect(page.getByRole('cell', { name: 'Type' })).toBeVisible(); | ||||
|  | ||||
|     // hide units & type column | ||||
|     await page.getByLabel('Units').uncheck(); | ||||
|     await page.getByLabel('Type').uncheck(); | ||||
|     await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeHidden(); | ||||
|     await expect(page.getByRole('cell', { name: 'Units' })).toBeHidden(); | ||||
|     await expect(page.getByRole('cell', { name: 'Type' })).toBeHidden(); | ||||
|  | ||||
|     // save and reload and verify they columns are still hidden | ||||
|     await page.locator('button[title="Save"]').click(); | ||||
|     await page.locator('text=Save and Finish Editing').click(); | ||||
|     await page.reload(); | ||||
|     await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeHidden(); | ||||
|     await expect(page.getByRole('cell', { name: 'Units' })).toBeHidden(); | ||||
|     await expect(page.getByRole('cell', { name: 'Type' })).toBeHidden(); | ||||
|  | ||||
|     // Edit LAD table | ||||
|     await page.locator('[title="Edit"]').click(); | ||||
|     await selectInspectorTab(page, 'LAD Table Configuration'); | ||||
|  | ||||
|     // show timestamp column | ||||
|     await page.getByLabel('Timestamp').check(); | ||||
|     await expect(page.getByRole('cell', { name: 'Units' })).toBeHidden(); | ||||
|     await expect(page.getByRole('cell', { name: 'Type' })).toBeHidden(); | ||||
|     await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible(); | ||||
|  | ||||
|     // save and reload and make sure only timestamp is still visible | ||||
|     await page.locator('button[title="Save"]').click(); | ||||
|     await page.locator('text=Save and Finish Editing').click(); | ||||
|     await page.reload(); | ||||
|     await expect(page.getByRole('cell', { name: 'Units' })).toBeHidden(); | ||||
|     await expect(page.getByRole('cell', { name: 'Type' })).toBeHidden(); | ||||
|     await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible(); | ||||
|  | ||||
|     // Edit LAD table | ||||
|     await page.locator('[title="Edit"]').click(); | ||||
|     await selectInspectorTab(page, 'LAD Table Configuration'); | ||||
|  | ||||
|     // show units and type columns | ||||
|     await page.getByLabel('Units').check(); | ||||
|     await page.getByLabel('Type').check(); | ||||
|     await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible(); | ||||
|     await expect(page.getByRole('cell', { name: 'Units' })).toBeVisible(); | ||||
|     await expect(page.getByRole('cell', { name: 'Type' })).toBeVisible(); | ||||
|  | ||||
|     // save and reload and make sure all columns are still visible | ||||
|     await page.locator('button[title="Save"]').click(); | ||||
|     await page.locator('text=Save and Finish Editing').click(); | ||||
|     await page.reload(); | ||||
|     await expect(page.getByRole('cell', { name: 'Timestamp' })).toBeVisible(); | ||||
|     await expect(page.getByRole('cell', { name: 'Units' })).toBeVisible(); | ||||
|     await expect(page.getByRole('cell', { name: 'Type' })).toBeVisible(); | ||||
|   }); | ||||
|  | ||||
|   test("LAD Tables don't allow selection of rows but does show context click menus", async ({ | ||||
|     page | ||||
|   }) => { | ||||
|     const cell = await page.locator('.js-first-data'); | ||||
|     const userSelectable = await cell.evaluate((el) => { | ||||
|       return window.getComputedStyle(el).getPropertyValue('user-select'); | ||||
|     }); | ||||
|  | ||||
|     expect(userSelectable).toBe('none'); | ||||
|     // Right-click on the LAD table row | ||||
|     await cell.click({ | ||||
|       button: 'right' | ||||
|     }); | ||||
|     const menuOptions = page.locator('.c-menu ul'); | ||||
|     await expect.soft(menuOptions).toContainText('View Full Datum'); | ||||
|     await expect.soft(menuOptions).toContainText('View Historical Data'); | ||||
|     await expect.soft(menuOptions).toContainText('Remove'); | ||||
|     // await page.locator('li[title="Remove this object from its containing object."]').click(); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| test.describe('Testing LAD table @unstable', () => { | ||||
|     let sineWaveObject; | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|         await setRealTimeMode(page); | ||||
|   let sineWaveObject; | ||||
|   test.beforeEach(async ({ page }) => { | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|     await setRealTimeMode(page); | ||||
|  | ||||
|         // Create Sine Wave Generator | ||||
|         sineWaveObject = await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Sine Wave Generator', | ||||
|             name: "Test Sine Wave Generator" | ||||
|         }); | ||||
|     // Create Sine Wave Generator | ||||
|     sineWaveObject = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Sine Wave Generator', | ||||
|       name: 'Test Sine Wave Generator' | ||||
|     }); | ||||
|     test('telemetry value exactly matches latest telemetry value received in real time', async ({ page }) => { | ||||
|         // Create LAD table | ||||
|         await createDomainObjectWithDefaults(page, { | ||||
|             type: 'LAD Table', | ||||
|             name: "Test LAD Table" | ||||
|         }); | ||||
|         // Edit LAD table | ||||
|         await page.locator('[title="Edit"]').click(); | ||||
|  | ||||
|         // Expand the 'My Items' folder in the left tree | ||||
|         await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click(); | ||||
|         // Add the Sine Wave Generator to the LAD table and save changes | ||||
|         await page.dragAndDrop('text=Test Sine Wave Generator', '.c-lad-table-wrapper'); | ||||
|         await page.locator('button[title="Save"]').click(); | ||||
|         await page.locator('text=Save and Finish Editing').click(); | ||||
|  | ||||
|         // Subscribe to the Sine Wave Generator data | ||||
|         // On getting data, check if the value found in the LAD table is the most recent value | ||||
|         // from the Sine Wave Generator | ||||
|         const getTelemValuePromise = await subscribeToTelemetry(page, sineWaveObject.uuid); | ||||
|         const subscribeTelemValue = await getTelemValuePromise; | ||||
|         const ladTableValuePromise = await page.waitForSelector(`text="${subscribeTelemValue}"`); | ||||
|         const ladTableValue = await ladTableValuePromise.textContent(); | ||||
|  | ||||
|         expect(ladTableValue).toBe(subscribeTelemValue); | ||||
|   }); | ||||
|   test('telemetry value exactly matches latest telemetry value received in real time', async ({ | ||||
|     page | ||||
|   }) => { | ||||
|     // Create LAD table | ||||
|     await createDomainObjectWithDefaults(page, { | ||||
|       type: 'LAD Table', | ||||
|       name: 'Test LAD Table' | ||||
|     }); | ||||
|     test('telemetry value exactly matches latest telemetry value received in fixed time', async ({ page }) => { | ||||
|         // Create LAD table | ||||
|         await createDomainObjectWithDefaults(page, { | ||||
|             type: 'LAD Table', | ||||
|             name: "Test LAD Table" | ||||
|         }); | ||||
|         // Edit LAD table | ||||
|         await page.locator('[title="Edit"]').click(); | ||||
|     // Edit LAD table | ||||
|     await page.locator('[title="Edit"]').click(); | ||||
|  | ||||
|         // Expand the 'My Items' folder in the left tree | ||||
|         await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click(); | ||||
|         // Add the Sine Wave Generator to the LAD table and save changes | ||||
|         await page.dragAndDrop('text=Test Sine Wave Generator', '.c-lad-table-wrapper'); | ||||
|         await page.locator('button[title="Save"]').click(); | ||||
|         await page.locator('text=Save and Finish Editing').click(); | ||||
|     // Expand the 'My Items' folder in the left tree | ||||
|     await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click(); | ||||
|     // Add the Sine Wave Generator to the LAD table and save changes | ||||
|     await page.dragAndDrop('text=Test Sine Wave Generator', '.c-lad-table-wrapper'); | ||||
|     await page.locator('button[title="Save"]').click(); | ||||
|     await page.locator('text=Save and Finish Editing').click(); | ||||
|  | ||||
|         // Subscribe to the Sine Wave Generator data | ||||
|         const getTelemValuePromise = await subscribeToTelemetry(page, sineWaveObject.uuid); | ||||
|         // Set an offset of 1 minute and then change the time mode to fixed to set a 1 minute historical window | ||||
|         await setStartOffset(page, { mins: '1' }); | ||||
|         await setFixedTimeMode(page); | ||||
|     // Subscribe to the Sine Wave Generator data | ||||
|     // On getting data, check if the value found in the LAD table is the most recent value | ||||
|     // from the Sine Wave Generator | ||||
|     const getTelemValuePromise = await subscribeToTelemetry(page, sineWaveObject.uuid); | ||||
|     const subscribeTelemValue = await getTelemValuePromise; | ||||
|     const ladTableValuePromise = await page.waitForSelector(`text="${subscribeTelemValue}"`); | ||||
|     const ladTableValue = await ladTableValuePromise.textContent(); | ||||
|  | ||||
|         // On getting data, check if the value found in the LAD table is the most recent value | ||||
|         // from the Sine Wave Generator | ||||
|         const subscribeTelemValue = await getTelemValuePromise; | ||||
|         const ladTableValuePromise = await page.waitForSelector(`text="${subscribeTelemValue}"`); | ||||
|         const ladTableValue = await ladTableValuePromise.textContent(); | ||||
|  | ||||
|         expect(ladTableValue).toBe(subscribeTelemValue); | ||||
|     expect(ladTableValue).toBe(subscribeTelemValue); | ||||
|   }); | ||||
|   test('telemetry value exactly matches latest telemetry value received in fixed time', async ({ | ||||
|     page | ||||
|   }) => { | ||||
|     // Create LAD table | ||||
|     await createDomainObjectWithDefaults(page, { | ||||
|       type: 'LAD Table', | ||||
|       name: 'Test LAD Table' | ||||
|     }); | ||||
|     // Edit LAD table | ||||
|     await page.locator('[title="Edit"]').click(); | ||||
|  | ||||
|     // Expand the 'My Items' folder in the left tree | ||||
|     await page.locator('.c-tree__item__view-control.c-disclosure-triangle').click(); | ||||
|     // Add the Sine Wave Generator to the LAD table and save changes | ||||
|     await page.dragAndDrop('text=Test Sine Wave Generator', '.c-lad-table-wrapper'); | ||||
|     await page.locator('button[title="Save"]').click(); | ||||
|     await page.locator('text=Save and Finish Editing').click(); | ||||
|  | ||||
|     // Subscribe to the Sine Wave Generator data | ||||
|     const getTelemValuePromise = await subscribeToTelemetry(page, sineWaveObject.uuid); | ||||
|     // Set an offset of 1 minute and then change the time mode to fixed to set a 1 minute historical window | ||||
|     await setStartOffset(page, { mins: '1' }); | ||||
|     await setFixedTimeMode(page); | ||||
|  | ||||
|     // On getting data, check if the value found in the LAD table is the most recent value | ||||
|     // from the Sine Wave Generator | ||||
|     const subscribeTelemValue = await getTelemValuePromise; | ||||
|     const ladTableValuePromise = await page.waitForSelector(`text="${subscribeTelemValue}"`); | ||||
|     const ladTableValue = await ladTableValuePromise.textContent(); | ||||
|  | ||||
|     expect(ladTableValue).toBe(subscribeTelemValue); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| /** | ||||
| @@ -216,18 +228,20 @@ test.describe('Testing LAD table @unstable', () => { | ||||
|  * @returns {Promise<string>} the formatted sin telemetry value | ||||
|  */ | ||||
| async function subscribeToTelemetry(page, objectIdentifier) { | ||||
|     const getTelemValuePromise = new Promise(resolve => page.exposeFunction('getTelemValue', resolve)); | ||||
|   const getTelemValuePromise = new Promise((resolve) => | ||||
|     page.exposeFunction('getTelemValue', resolve) | ||||
|   ); | ||||
|  | ||||
|     await page.evaluate(async (telemetryIdentifier) => { | ||||
|         const telemetryObject = await window.openmct.objects.get(telemetryIdentifier); | ||||
|         const metadata = window.openmct.telemetry.getMetadata(telemetryObject); | ||||
|         const formats = await window.openmct.telemetry.getFormatMap(metadata); | ||||
|         window.openmct.telemetry.subscribe(telemetryObject, (obj) => { | ||||
|             const sinVal = obj.sin; | ||||
|             const formattedSinVal = formats.sin.format(sinVal); | ||||
|             window.getTelemValue(formattedSinVal); | ||||
|         }); | ||||
|     }, objectIdentifier); | ||||
|   await page.evaluate(async (telemetryIdentifier) => { | ||||
|     const telemetryObject = await window.openmct.objects.get(telemetryIdentifier); | ||||
|     const metadata = window.openmct.telemetry.getMetadata(telemetryObject); | ||||
|     const formats = await window.openmct.telemetry.getFormatMap(metadata); | ||||
|     window.openmct.telemetry.subscribe(telemetryObject, (obj) => { | ||||
|       const sinVal = obj.sin; | ||||
|       const formattedSinVal = formats.sin.format(sinVal); | ||||
|       window.getTelemValue(formattedSinVal); | ||||
|     }); | ||||
|   }, objectIdentifier); | ||||
|  | ||||
|     return getTelemValuePromise; | ||||
|   return getTelemValuePromise; | ||||
| } | ||||
|   | ||||
| @@ -19,7 +19,7 @@ | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* global __dirname */ | ||||
| /* | ||||
| This test suite is dedicated to tests which verify the basic operations surrounding Notebooks. | ||||
| */ | ||||
| @@ -32,405 +32,454 @@ const path = require('path'); | ||||
| const NOTEBOOK_NAME = 'Notebook'; | ||||
|  | ||||
| test.describe('Notebook CRUD Operations', () => { | ||||
|     test.fixme('Can create a Notebook Object', async ({ page }) => { | ||||
|         //Create domain object | ||||
|         //Newly created notebook should have one Section and one page, 'Unnamed Section'/'Unnamed Page' | ||||
|     }); | ||||
|     test.fixme('Can update a Notebook Object', async ({ page }) => {}); | ||||
|     test.fixme('Can view a perviously created Notebook Object', async ({ page }) => {}); | ||||
|     test.fixme('Can Delete a Notebook Object', async ({ page }) => { | ||||
|         // Other than non-persistible objects | ||||
|     }); | ||||
|   test.fixme('Can create a Notebook Object', async ({ page }) => { | ||||
|     //Create domain object | ||||
|     //Newly created notebook should have one Section and one page, 'Unnamed Section'/'Unnamed Page' | ||||
|   }); | ||||
|   test.fixme('Can update a Notebook Object', async ({ page }) => {}); | ||||
|   test.fixme('Can view a perviously created Notebook Object', async ({ page }) => {}); | ||||
|   test.fixme('Can Delete a Notebook Object', async ({ page }) => { | ||||
|     // Other than non-persistible objects | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| test.describe('Default Notebook', () => { | ||||
|     // General Default Notebook statements | ||||
|     // ## Useful commands: | ||||
|     // 1.  - To check default notebook: | ||||
|     //     `JSON.parse(localStorage.getItem('notebook-storage'));` | ||||
|     // 1.  - Clear default notebook: | ||||
|     //     `localStorage.setItem('notebook-storage', null);` | ||||
|     test.fixme('A newly created Notebook is automatically set as the default notebook if no other notebooks exist', async ({ page }) => { | ||||
|         //Create new notebook | ||||
|         //Verify Default Notebook Characteristics | ||||
|     }); | ||||
|     test.fixme('A newly created Notebook is automatically set as the default notebook if at least one other notebook exists', async ({ page }) => { | ||||
|         //Create new notebook A | ||||
|         //Create second notebook B | ||||
|         //Verify Non-Default Notebook A Characteristics | ||||
|         //Verify Default Notebook B Characteristics | ||||
|     }); | ||||
|     test.fixme('If a default notebook is deleted, the second most recent notebook becomes the default', async ({ page }) => { | ||||
|         //Create new notebook A | ||||
|         //Create second notebook B | ||||
|         //Delete Notebook B | ||||
|         //Verify Default Notebook A Characteristics | ||||
|     }); | ||||
|   // General Default Notebook statements | ||||
|   // ## Useful commands: | ||||
|   // 1.  - To check default notebook: | ||||
|   //     `JSON.parse(localStorage.getItem('notebook-storage'));` | ||||
|   // 1.  - Clear default notebook: | ||||
|   //     `localStorage.setItem('notebook-storage', null);` | ||||
|   test.fixme( | ||||
|     'A newly created Notebook is automatically set as the default notebook if no other notebooks exist', | ||||
|     async ({ page }) => { | ||||
|       //Create new notebook | ||||
|       //Verify Default Notebook Characteristics | ||||
|     } | ||||
|   ); | ||||
|   test.fixme( | ||||
|     'A newly created Notebook is automatically set as the default notebook if at least one other notebook exists', | ||||
|     async ({ page }) => { | ||||
|       //Create new notebook A | ||||
|       //Create second notebook B | ||||
|       //Verify Non-Default Notebook A Characteristics | ||||
|       //Verify Default Notebook B Characteristics | ||||
|     } | ||||
|   ); | ||||
|   test.fixme( | ||||
|     'If a default notebook is deleted, the second most recent notebook becomes the default', | ||||
|     async ({ page }) => { | ||||
|       //Create new notebook A | ||||
|       //Create second notebook B | ||||
|       //Delete Notebook B | ||||
|       //Verify Default Notebook A Characteristics | ||||
|     } | ||||
|   ); | ||||
| }); | ||||
|  | ||||
| test.describe('Notebook section tests', () => { | ||||
|     //The following test cases are associated with Notebook Sections | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         //Navigate to baseURL | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|   //The following test cases are associated with Notebook Sections | ||||
|   test.beforeEach(async ({ page }) => { | ||||
|     //Navigate to baseURL | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|  | ||||
|         // Create Notebook | ||||
|         await createDomainObjectWithDefaults(page, { | ||||
|             type: NOTEBOOK_NAME | ||||
|         }); | ||||
|     // Create Notebook | ||||
|     await createDomainObjectWithDefaults(page, { | ||||
|       type: NOTEBOOK_NAME | ||||
|     }); | ||||
|     test('Default and new sections are automatically named Unnamed Section with Unnamed Page', async ({ page }) => { | ||||
|         // Check that the default section and page are created and the name matches the defaults | ||||
|         const defaultSectionName = await page.locator('.c-notebook__sections .c-list__item__name').textContent(); | ||||
|         expect(defaultSectionName).toBe('Unnamed Section'); | ||||
|         const defaultPageName = await page.locator('.c-notebook__pages .c-list__item__name').textContent(); | ||||
|         expect(defaultPageName).toBe('Unnamed Page'); | ||||
|   }); | ||||
|   test('Default and new sections are automatically named Unnamed Section with Unnamed Page', async ({ | ||||
|     page | ||||
|   }) => { | ||||
|     const notebookSectionNames = page.locator('.c-notebook__sections .c-list__item__name'); | ||||
|     const notebookPageNames = page.locator('.c-notebook__pages .c-list__item__name'); | ||||
|     await expect(notebookSectionNames).toBeHidden(); | ||||
|     await expect(notebookPageNames).toBeHidden(); | ||||
|     // Expand sidebar | ||||
|     await page.locator('.c-notebook__toggle-nav-button').click(); | ||||
|     // Check that the default section and page are created and the name matches the defaults | ||||
|     const defaultSectionName = await notebookSectionNames.innerText(); | ||||
|     await expect(notebookSectionNames).toBeVisible(); | ||||
|     expect(defaultSectionName).toBe('Unnamed Section'); | ||||
|     const defaultPageName = await notebookPageNames.innerText(); | ||||
|     await expect(notebookPageNames).toBeVisible(); | ||||
|     expect(defaultPageName).toBe('Unnamed Page'); | ||||
|  | ||||
|         // Expand sidebar and add a section | ||||
|         await page.locator('.c-notebook__toggle-nav-button').click(); | ||||
|         await page.locator('.js-sidebar-sections .c-icon-button.icon-plus').click(); | ||||
|     // Add a section | ||||
|     await page.locator('.js-sidebar-sections .c-icon-button.icon-plus').click(); | ||||
|  | ||||
|         // Check that new section and page within the new section match the defaults | ||||
|         const newSectionName = await page.locator('.c-notebook__sections .c-list__item__name').nth(1).textContent(); | ||||
|         expect(newSectionName).toBe('Unnamed Section'); | ||||
|         const newPageName = await page.locator('.c-notebook__pages .c-list__item__name').textContent(); | ||||
|         expect(newPageName).toBe('Unnamed Page'); | ||||
|     }); | ||||
|     test.fixme('Section selection operations and associated behavior', async ({ page }) => { | ||||
|         //Create new notebook A | ||||
|         //Add Sections until 6 total with no default section/page | ||||
|         //Select 3rd section | ||||
|         //Delete 4th section | ||||
|         //3rd section is still selected | ||||
|         //Delete 3rd section | ||||
|         //1st section is selected | ||||
|         //Set 3rd section as default | ||||
|         //Delete 2nd section | ||||
|         //3rd section is still default | ||||
|         //Delete 3rd section | ||||
|         //1st is selected and there is no default notebook | ||||
|     }); | ||||
|     test.fixme('Section rename operations', async ({ page }) => { | ||||
|         // Create a new notebook | ||||
|         // Add a section | ||||
|         // Rename the section but do not confirm | ||||
|         // Keyboard press 'Escape' | ||||
|         // Verify that the section name reverts to the default name | ||||
|         // Rename the section but do not confirm | ||||
|         // Keyboard press 'Enter' | ||||
|         // Verify that the section name is updated | ||||
|         // Rename the section to "" (empty string) | ||||
|         // Keyboard press 'Enter' to confirm | ||||
|         // Verify that the section name reverts to the default name | ||||
|         // Rename the section to something long that overflows the text box | ||||
|         // Verify that the section name is not truncated while input is active | ||||
|         // Confirm the section name edit | ||||
|         // Verify that the section name is truncated now that input is not active | ||||
|     }); | ||||
|     // Check that new section and page within the new section match the defaults | ||||
|     const newSectionName = await notebookSectionNames.nth(1).innerText(); | ||||
|     await expect(notebookSectionNames.nth(1)).toBeVisible(); | ||||
|     expect(newSectionName).toBe('Unnamed Section'); | ||||
|     const newPageName = await notebookPageNames.innerText(); | ||||
|     await expect(notebookPageNames).toBeVisible(); | ||||
|     expect(newPageName).toBe('Unnamed Page'); | ||||
|   }); | ||||
|   test.fixme('Section selection operations and associated behavior', async ({ page }) => { | ||||
|     //Create new notebook A | ||||
|     //Add Sections until 6 total with no default section/page | ||||
|     //Select 3rd section | ||||
|     //Delete 4th section | ||||
|     //3rd section is still selected | ||||
|     //Delete 3rd section | ||||
|     //1st section is selected | ||||
|     //Set 3rd section as default | ||||
|     //Delete 2nd section | ||||
|     //3rd section is still default | ||||
|     //Delete 3rd section | ||||
|     //1st is selected and there is no default notebook | ||||
|   }); | ||||
|   test.fixme('Section rename operations', async ({ page }) => { | ||||
|     // Create a new notebook | ||||
|     // Add a section | ||||
|     // Rename the section but do not confirm | ||||
|     // Keyboard press 'Escape' | ||||
|     // Verify that the section name reverts to the default name | ||||
|     // Rename the section but do not confirm | ||||
|     // Keyboard press 'Enter' | ||||
|     // Verify that the section name is updated | ||||
|     // Rename the section to "" (empty string) | ||||
|     // Keyboard press 'Enter' to confirm | ||||
|     // Verify that the section name reverts to the default name | ||||
|     // Rename the section to something long that overflows the text box | ||||
|     // Verify that the section name is not truncated while input is active | ||||
|     // Confirm the section name edit | ||||
|     // Verify that the section name is truncated now that input is not active | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| test.describe('Notebook page tests', () => { | ||||
|     //The following test cases are associated with Notebook Pages | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         //Navigate to baseURL | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|   //The following test cases are associated with Notebook Pages | ||||
|   test.beforeEach(async ({ page }) => { | ||||
|     //Navigate to baseURL | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|  | ||||
|         // Create Notebook | ||||
|         await createDomainObjectWithDefaults(page, { | ||||
|             type: NOTEBOOK_NAME | ||||
|         }); | ||||
|     // Create Notebook | ||||
|     await createDomainObjectWithDefaults(page, { | ||||
|       type: NOTEBOOK_NAME | ||||
|     }); | ||||
|     //Test will need to be implemented after a refactor in #5713 | ||||
|     // eslint-disable-next-line playwright/no-skipped-test | ||||
|     test.skip('Delete page popup is removed properly on clicking dropdown again', async ({ page }) => { | ||||
|         test.info().annotations.push({ | ||||
|             type: 'issue', | ||||
|             description: 'https://github.com/nasa/openmct/issues/5713' | ||||
|         }); | ||||
|         // Expand sidebar and add a second page | ||||
|         await page.locator('.c-notebook__toggle-nav-button').click(); | ||||
|         await page.locator('text=Page Add >> button').click(); | ||||
|   }); | ||||
|   //Test will need to be implemented after a refactor in #5713 | ||||
|   // eslint-disable-next-line playwright/no-skipped-test | ||||
|   test.skip('Delete page popup is removed properly on clicking dropdown again', async ({ | ||||
|     page | ||||
|   }) => { | ||||
|     test.info().annotations.push({ | ||||
|       type: 'issue', | ||||
|       description: 'https://github.com/nasa/openmct/issues/5713' | ||||
|     }); | ||||
|     // Expand sidebar and add a second page | ||||
|     await page.locator('.c-notebook__toggle-nav-button').click(); | ||||
|     await page.locator('text=Page Add >> button').click(); | ||||
|  | ||||
|         // Click on the 2nd page dropdown button and expect the Delete Page option to appear | ||||
|         await page.locator('button[title="Open context menu"]').nth(2).click(); | ||||
|         await expect(page.locator('text=Delete Page')).toBeEnabled(); | ||||
|         // Clicking on the same page a second time causes the same Delete Page option to recreate | ||||
|         await page.locator('button[title="Open context menu"]').nth(2).click(); | ||||
|         await expect(page.locator('text=Delete Page')).toBeEnabled(); | ||||
|         // Clicking on the first page causes the first delete button to detach and recreate on the first page | ||||
|         await page.locator('button[title="Open context menu"]').nth(1).click(); | ||||
|         const numOfDeletePagePopups = await page.locator('li[title="Delete Page"]').count(); | ||||
|         expect(numOfDeletePagePopups).toBe(1); | ||||
|     }); | ||||
|     test.fixme('Page selection operations and associated behavior', async ({ page }) => { | ||||
|         //Create new notebook A | ||||
|         //Delete existing Page | ||||
|         //New 'Unnamed Page' automatically created | ||||
|         //Create 6 total Pages without a default page | ||||
|         //Select 3rd | ||||
|         //Delete 3rd | ||||
|         //First is now selected | ||||
|         //Set 3rd as default | ||||
|         //Select 2nd page | ||||
|         //Delete 2nd page | ||||
|         //3rd (default) is now selected | ||||
|         //Set 3rd as default page | ||||
|         //Select 3rd (default) page | ||||
|         //Delete 3rd page | ||||
|         //First is now selected and there is no default notebook | ||||
|     }); | ||||
|     test.fixme('Page rename operations', async ({ page }) => { | ||||
|         // Create a new notebook | ||||
|         // Add a page | ||||
|         // Rename the page but do not confirm | ||||
|         // Keyboard press 'Escape' | ||||
|         // Verify that the page name reverts to the default name | ||||
|         // Rename the page but do not confirm | ||||
|         // Keyboard press 'Enter' | ||||
|         // Verify that the page name is updated | ||||
|         // Rename the page to "" (empty string) | ||||
|         // Keyboard press 'Enter' to confirm | ||||
|         // Verify that the page name reverts to the default name | ||||
|         // Rename the page to something long that overflows the text box | ||||
|         // Verify that the page name is not truncated while input is active | ||||
|         // Confirm the page name edit | ||||
|         // Verify that the page name is truncated now that input is not active | ||||
|     }); | ||||
|     // Click on the 2nd page dropdown button and expect the Delete Page option to appear | ||||
|     await page.locator('button[title="Open context menu"]').nth(2).click(); | ||||
|     await expect(page.locator('text=Delete Page')).toBeEnabled(); | ||||
|     // Clicking on the same page a second time causes the same Delete Page option to recreate | ||||
|     await page.locator('button[title="Open context menu"]').nth(2).click(); | ||||
|     await expect(page.locator('text=Delete Page')).toBeEnabled(); | ||||
|     // Clicking on the first page causes the first delete button to detach and recreate on the first page | ||||
|     await page.locator('button[title="Open context menu"]').nth(1).click(); | ||||
|     const numOfDeletePagePopups = await page.locator('li[title="Delete Page"]').count(); | ||||
|     expect(numOfDeletePagePopups).toBe(1); | ||||
|   }); | ||||
|   test.fixme('Page selection operations and associated behavior', async ({ page }) => { | ||||
|     //Create new notebook A | ||||
|     //Delete existing Page | ||||
|     //New 'Unnamed Page' automatically created | ||||
|     //Create 6 total Pages without a default page | ||||
|     //Select 3rd | ||||
|     //Delete 3rd | ||||
|     //First is now selected | ||||
|     //Set 3rd as default | ||||
|     //Select 2nd page | ||||
|     //Delete 2nd page | ||||
|     //3rd (default) is now selected | ||||
|     //Set 3rd as default page | ||||
|     //Select 3rd (default) page | ||||
|     //Delete 3rd page | ||||
|     //First is now selected and there is no default notebook | ||||
|   }); | ||||
|   test.fixme('Page rename operations', async ({ page }) => { | ||||
|     // Create a new notebook | ||||
|     // Add a page | ||||
|     // Rename the page but do not confirm | ||||
|     // Keyboard press 'Escape' | ||||
|     // Verify that the page name reverts to the default name | ||||
|     // Rename the page but do not confirm | ||||
|     // Keyboard press 'Enter' | ||||
|     // Verify that the page name is updated | ||||
|     // Rename the page to "" (empty string) | ||||
|     // Keyboard press 'Enter' to confirm | ||||
|     // Verify that the page name reverts to the default name | ||||
|     // Rename the page to something long that overflows the text box | ||||
|     // Verify that the page name is not truncated while input is active | ||||
|     // Confirm the page name edit | ||||
|     // Verify that the page name is truncated now that input is not active | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| test.describe('Notebook export tests', () => { | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         //Navigate to baseURL | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|   test.beforeEach(async ({ page }) => { | ||||
|     //Navigate to baseURL | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|  | ||||
|         // Create Notebook | ||||
|         await createDomainObjectWithDefaults(page, { | ||||
|             type: NOTEBOOK_NAME | ||||
|         }); | ||||
|     // Create Notebook | ||||
|     await createDomainObjectWithDefaults(page, { | ||||
|       type: NOTEBOOK_NAME | ||||
|     }); | ||||
|     test('can export notebook as text', async ({ page }) => { | ||||
|         await nbUtils.enterTextEntry(page, `Foo bar entry`); | ||||
|         // Click on 3 Dot Menu | ||||
|         await page.locator('button[title="More options"]').click(); | ||||
|         const downloadPromise = page.waitForEvent('download'); | ||||
|   }); | ||||
|   test('can export notebook as text', async ({ page }) => { | ||||
|     await nbUtils.enterTextEntry(page, `Foo bar entry`); | ||||
|     // Click on 3 Dot Menu | ||||
|     await page.locator('button[title="More options"]').click(); | ||||
|     const downloadPromise = page.waitForEvent('download'); | ||||
|  | ||||
|         await page.getByRole('menuitem', { name: /Export Notebook as Text/ }).click(); | ||||
|     await page.getByRole('menuitem', { name: /Export Notebook as Text/ }).click(); | ||||
|  | ||||
|         await page.getByRole('button', { name: 'Save' }).click(); | ||||
|         const download = await downloadPromise; | ||||
|         const readStream = await download.createReadStream(); | ||||
|         const exportedText = await streamToString(readStream); | ||||
|         expect(exportedText).toContain('Foo bar entry'); | ||||
|     }); | ||||
|     test.fixme('can export multiple notebook entries as text ', async ({ page }) => {}); | ||||
|     test.fixme('can export all notebook entry metdata', async ({ page }) => {}); | ||||
|     test.fixme('can export all notebook tags', async ({ page }) => {}); | ||||
|     test.fixme('can export all notebook snapshots', async ({ page }) => {}); | ||||
|     await page.getByRole('button', { name: 'Save' }).click(); | ||||
|     const download = await downloadPromise; | ||||
|     const readStream = await download.createReadStream(); | ||||
|     const exportedText = await streamToString(readStream); | ||||
|     expect(exportedText).toContain('Foo bar entry'); | ||||
|   }); | ||||
|   test.fixme('can export multiple notebook entries as text ', async ({ page }) => {}); | ||||
|   test.fixme('can export all notebook entry metdata', async ({ page }) => {}); | ||||
|   test.fixme('can export all notebook tags', async ({ page }) => {}); | ||||
|   test.fixme('can export all notebook snapshots', async ({ page }) => {}); | ||||
| }); | ||||
|  | ||||
| test.describe('Notebook search tests', () => { | ||||
|     test.fixme('Can search for a single result', async ({ page }) => {}); | ||||
|     test.fixme('Can search for many results', async ({ page }) => {}); | ||||
|     test.fixme('Can search for new and recently modified entries', async ({ page }) => {}); | ||||
|     test.fixme('Can search for section text', async ({ page }) => {}); | ||||
|     test.fixme('Can search for page text', async ({ page }) => {}); | ||||
|     test.fixme('Can search for entry text', async ({ page }) => {}); | ||||
|   test.fixme('Can search for a single result', async ({ page }) => {}); | ||||
|   test.fixme('Can search for many results', async ({ page }) => {}); | ||||
|   test.fixme('Can search for new and recently modified entries', async ({ page }) => {}); | ||||
|   test.fixme('Can search for section text', async ({ page }) => {}); | ||||
|   test.fixme('Can search for page text', async ({ page }) => {}); | ||||
|   test.fixme('Can search for entry text', async ({ page }) => {}); | ||||
| }); | ||||
|  | ||||
| test.describe('Notebook entry tests', () => { | ||||
|     // Create Notebook with URL Whitelist | ||||
|     let notebookObject; | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         // eslint-disable-next-line no-undef | ||||
|         await page.addInitScript({ path: path.join(__dirname, '../../../../helper/', 'addInitNotebookWithUrls.js') }); | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|  | ||||
|         notebookObject = await createDomainObjectWithDefaults(page, { | ||||
|             type: NOTEBOOK_NAME | ||||
|         }); | ||||
|   // Create Notebook with URL Whitelist | ||||
|   let notebookObject; | ||||
|   test.beforeEach(async ({ page }) => { | ||||
|     // eslint-disable-next-line no-undef | ||||
|     await page.addInitScript({ | ||||
|       path: path.join(__dirname, '../../../../helper/', 'addInitNotebookWithUrls.js') | ||||
|     }); | ||||
|     test('When a new entry is created, it should be focused and selected', async ({ page }) => { | ||||
|         // Navigate to the notebook object | ||||
|         await page.goto(notebookObject.url); | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|  | ||||
|         // Click .c-notebook__drag-area | ||||
|         await page.locator('.c-notebook__drag-area').click(); | ||||
|         await expect(page.locator('[aria-label="Notebook Entry Input"]')).toBeVisible(); | ||||
|         await expect(page.locator('[aria-label="Notebook Entry"]')).toHaveClass(/is-selected/); | ||||
|     notebookObject = await createDomainObjectWithDefaults(page, { | ||||
|       type: NOTEBOOK_NAME | ||||
|     }); | ||||
|     test('When an object is dropped into a notebook, a new entry is created and it should be focused @unstable', async ({ page }) => { | ||||
|         // Create Overlay Plot | ||||
|         const overlayPlot = await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Overlay Plot' | ||||
|         }); | ||||
|   }); | ||||
|   test('When a new entry is created, it should be focused and selected', async ({ page }) => { | ||||
|     // Navigate to the notebook object | ||||
|     await page.goto(notebookObject.url); | ||||
|  | ||||
|         // Navigate to the notebook object | ||||
|         await page.goto(notebookObject.url); | ||||
|  | ||||
|         // Reveal the notebook in the tree | ||||
|         await page.getByTitle('Show selected item in tree').click(); | ||||
|  | ||||
|         await page.dragAndDrop(`role=treeitem[name=/${overlayPlot.name}/]`, '.c-notebook__drag-area'); | ||||
|  | ||||
|         const embed = page.locator('.c-ne__embed__link'); | ||||
|         const embedName = await embed.textContent(); | ||||
|  | ||||
|         await expect(embed).toHaveClass(/icon-plot-overlay/); | ||||
|         expect(embedName).toBe(overlayPlot.name); | ||||
|     // Click .c-notebook__drag-area | ||||
|     await page.locator('.c-notebook__drag-area').click(); | ||||
|     await expect(page.locator('[aria-label="Notebook Entry Input"]')).toBeVisible(); | ||||
|     await expect(page.locator('[aria-label="Notebook Entry"]')).toHaveClass(/is-selected/); | ||||
|   }); | ||||
|   test('When an object is dropped into a notebook, a new entry is created and it should be focused @unstable', async ({ | ||||
|     page | ||||
|   }) => { | ||||
|     // Create Overlay Plot | ||||
|     const overlayPlot = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Overlay Plot' | ||||
|     }); | ||||
|     test('When an object is dropped into a notebooks existing entry, it should be focused @unstable', async ({ page }) => { | ||||
|         // Create Overlay Plot | ||||
|         const overlayPlot = await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Overlay Plot' | ||||
|         }); | ||||
|  | ||||
|         // Navigate to the notebook object | ||||
|         await page.goto(notebookObject.url); | ||||
|     // Navigate to the notebook object | ||||
|     await page.goto(notebookObject.url); | ||||
|  | ||||
|         // Reveal the notebook in the tree | ||||
|         await page.getByTitle('Show selected item in tree').click(); | ||||
|     // Reveal the notebook in the tree | ||||
|     await page.getByTitle('Show selected item in tree').click(); | ||||
|  | ||||
|         await nbUtils.enterTextEntry(page, 'Entry to drop into'); | ||||
|         await page.dragAndDrop(`role=treeitem[name=/${overlayPlot.name}/]`, 'text=Entry to drop into'); | ||||
|     await page | ||||
|       .getByRole('treeitem', { name: overlayPlot.name }) | ||||
|       .dragTo(page.locator('.c-notebook__drag-area')); | ||||
|  | ||||
|         const existingEntry = page.locator('.c-ne__content', { has: page.locator('text="Entry to drop into"') }); | ||||
|         const embed = existingEntry.locator('.c-ne__embed__link'); | ||||
|         const embedName = await embed.textContent(); | ||||
|     const embed = page.locator('.c-ne__embed__link'); | ||||
|     const embedName = await embed.innerText(); | ||||
|  | ||||
|         await expect(embed).toHaveClass(/icon-plot-overlay/); | ||||
|         expect(embedName).toBe(overlayPlot.name); | ||||
|     await expect(embed).toHaveClass(/icon-plot-overlay/); | ||||
|     expect(embedName).toBe(overlayPlot.name); | ||||
|   }); | ||||
|   test('When an object is dropped into a notebooks existing entry, it should be focused @unstable', async ({ | ||||
|     page | ||||
|   }) => { | ||||
|     // Create Overlay Plot | ||||
|     const overlayPlot = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Overlay Plot' | ||||
|     }); | ||||
|     test.fixme('new entries persist through navigation events without save', async ({ page }) => {}); | ||||
|     test('previous and new entries can be deleted', async ({ page }) => { | ||||
|         // Navigate to the notebook object | ||||
|         await page.goto(notebookObject.url); | ||||
|  | ||||
|         await nbUtils.enterTextEntry(page, 'First Entry'); | ||||
|         await page.hover('text="First Entry"'); | ||||
|         await page.click('button[title="Delete this entry"]'); | ||||
|         await page.getByRole('button', { name: 'Ok' }).filter({ hasText: 'Ok' }).click(); | ||||
|         await expect(page.locator('text="First Entry"')).toBeHidden(); | ||||
|         await nbUtils.enterTextEntry(page, 'Another First Entry'); | ||||
|         await nbUtils.enterTextEntry(page, 'Second Entry'); | ||||
|         await nbUtils.enterTextEntry(page, 'Third Entry'); | ||||
|         await page.hover('[aria-label="Notebook Entry"] >> nth=2'); | ||||
|         await page.click('button[title="Delete this entry"] >> nth=2'); | ||||
|         await page.getByRole('button', { name: 'Ok' }).filter({ hasText: 'Ok' }).click(); | ||||
|         await expect(page.locator('text="Third Entry"')).toBeHidden(); | ||||
|         await expect(page.locator('text="Another First Entry"')).toBeVisible(); | ||||
|         await expect(page.locator('text="Second Entry"')).toBeVisible(); | ||||
|     // Navigate to the notebook object | ||||
|     await page.goto(notebookObject.url); | ||||
|  | ||||
|     // Reveal the notebook in the tree | ||||
|     await page.getByTitle('Show selected item in tree').click(); | ||||
|  | ||||
|     await nbUtils.enterTextEntry(page, 'Entry to drop into'); | ||||
|     await page | ||||
|       .getByRole('treeitem', { name: overlayPlot.name }) | ||||
|       .dragTo(page.locator('text=Entry to drop into')); | ||||
|  | ||||
|     const existingEntry = page.locator('.c-ne__content', { | ||||
|       has: page.locator('text="Entry to drop into"') | ||||
|     }); | ||||
|     test('when a valid link is entered into a notebook entry, it becomes clickable when viewing', async ({ page }) => { | ||||
|         const TEST_LINK = 'http://www.google.com'; | ||||
|     const embed = existingEntry.locator('.c-ne__embed__link'); | ||||
|     const embedName = await embed.innerText(); | ||||
|  | ||||
|         // Navigate to the notebook object | ||||
|         await page.goto(notebookObject.url); | ||||
|     await expect(embed).toHaveClass(/icon-plot-overlay/); | ||||
|     expect(embedName).toBe(overlayPlot.name); | ||||
|   }); | ||||
|   test.fixme('new entries persist through navigation events without save', async ({ page }) => {}); | ||||
|   test('previous and new entries can be deleted', async ({ page }) => { | ||||
|     // Navigate to the notebook object | ||||
|     await page.goto(notebookObject.url); | ||||
|  | ||||
|         // Reveal the notebook in the tree | ||||
|         await page.getByTitle('Show selected item in tree').click(); | ||||
|     await nbUtils.enterTextEntry(page, 'First Entry'); | ||||
|     await page.hover('text="First Entry"'); | ||||
|     await page.click('button[title="Delete this entry"]'); | ||||
|     await page.getByRole('button', { name: 'Ok' }).filter({ hasText: 'Ok' }).click(); | ||||
|     await expect(page.locator('text="First Entry"')).toBeHidden(); | ||||
|     await nbUtils.enterTextEntry(page, 'Another First Entry'); | ||||
|     await nbUtils.enterTextEntry(page, 'Second Entry'); | ||||
|     await nbUtils.enterTextEntry(page, 'Third Entry'); | ||||
|     await page.hover('[aria-label="Notebook Entry"] >> nth=2'); | ||||
|     await page.click('button[title="Delete this entry"] >> nth=2'); | ||||
|     await page.getByRole('button', { name: 'Ok' }).filter({ hasText: 'Ok' }).click(); | ||||
|     await expect(page.locator('text="Third Entry"')).toBeHidden(); | ||||
|     await expect(page.locator('text="Another First Entry"')).toBeVisible(); | ||||
|     await expect(page.locator('text="Second Entry"')).toBeVisible(); | ||||
|   }); | ||||
|   test('when a valid link is entered into a notebook entry, it becomes clickable when viewing', async ({ | ||||
|     page | ||||
|   }) => { | ||||
|     const TEST_LINK = 'http://www.google.com'; | ||||
|  | ||||
|         await nbUtils.enterTextEntry(page, `This should be a link: ${TEST_LINK} is it?`); | ||||
|     // Navigate to the notebook object | ||||
|     await page.goto(notebookObject.url); | ||||
|  | ||||
|         const validLink = page.locator(`a[href="${TEST_LINK}"]`); | ||||
|     // Reveal the notebook in the tree | ||||
|     await page.getByTitle('Show selected item in tree').click(); | ||||
|  | ||||
|         // Start waiting for popup before clicking. Note no await. | ||||
|         const popupPromise = page.waitForEvent('popup'); | ||||
|     await nbUtils.enterTextEntry(page, `This should be a link: ${TEST_LINK} is it?`); | ||||
|  | ||||
|         await validLink.click(); | ||||
|         const popup = await popupPromise; | ||||
|     const validLink = page.locator(`a[href="${TEST_LINK}"]`); | ||||
|  | ||||
|         // Wait for the popup to load. | ||||
|         await popup.waitForLoadState(); | ||||
|         expect.soft(popup.url()).toContain('www.google.com'); | ||||
|     // Start waiting for popup before clicking. Note no await. | ||||
|     const popupPromise = page.waitForEvent('popup'); | ||||
|  | ||||
|         expect(await validLink.count()).toBe(1); | ||||
|     }); | ||||
|     test('when an invalid link is entered into a notebook entry, it does not become clickable when viewing', async ({ page }) => { | ||||
|         const TEST_LINK = 'www.google.com'; | ||||
|     await validLink.click(); | ||||
|     const popup = await popupPromise; | ||||
|  | ||||
|         // Navigate to the notebook object | ||||
|         await page.goto(notebookObject.url); | ||||
|     // Wait for the popup to load. | ||||
|     await popup.waitForLoadState(); | ||||
|     expect.soft(popup.url()).toContain('www.google.com'); | ||||
|  | ||||
|         // Reveal the notebook in the tree | ||||
|         await page.getByTitle('Show selected item in tree').click(); | ||||
|     expect(await validLink.count()).toBe(1); | ||||
|   }); | ||||
|   test('when an invalid link is entered into a notebook entry, it does not become clickable when viewing', async ({ | ||||
|     page | ||||
|   }) => { | ||||
|     const TEST_LINK = 'www.google.com'; | ||||
|  | ||||
|         await nbUtils.enterTextEntry(page, `This should NOT be a link: ${TEST_LINK} is it?`); | ||||
|     // Navigate to the notebook object | ||||
|     await page.goto(notebookObject.url); | ||||
|  | ||||
|         const invalidLink = page.locator(`a[href="${TEST_LINK}"]`); | ||||
|     // Reveal the notebook in the tree | ||||
|     await page.getByTitle('Show selected item in tree').click(); | ||||
|  | ||||
|         expect(await invalidLink.count()).toBe(0); | ||||
|     }); | ||||
|     test('when a link is entered, but it is not in the whitelisted urls, it does not become clickable when viewing', async ({ page }) => { | ||||
|         const TEST_LINK = 'http://www.bing.com'; | ||||
|     await nbUtils.enterTextEntry(page, `This should NOT be a link: ${TEST_LINK} is it?`); | ||||
|  | ||||
|         // Navigate to the notebook object | ||||
|         await page.goto(notebookObject.url); | ||||
|     const invalidLink = page.locator(`a[href="${TEST_LINK}"]`); | ||||
|  | ||||
|         // Reveal the notebook in the tree | ||||
|         await page.getByTitle('Show selected item in tree').click(); | ||||
|     expect(await invalidLink.count()).toBe(0); | ||||
|   }); | ||||
|   test('when a link is entered, but it is not in the whitelisted urls, it does not become clickable when viewing', async ({ | ||||
|     page | ||||
|   }) => { | ||||
|     const TEST_LINK = 'http://www.bing.com'; | ||||
|  | ||||
|         await nbUtils.enterTextEntry(page, `This should NOT be a link: ${TEST_LINK} is it?`); | ||||
|     // Navigate to the notebook object | ||||
|     await page.goto(notebookObject.url); | ||||
|  | ||||
|         const invalidLink = page.locator(`a[href="${TEST_LINK}"]`); | ||||
|     // Reveal the notebook in the tree | ||||
|     await page.getByTitle('Show selected item in tree').click(); | ||||
|  | ||||
|         expect(await invalidLink.count()).toBe(0); | ||||
|     }); | ||||
|     test('when a valid link with a subdomain and a valid domain in the whitelisted urls is entered into a notebook entry, it becomes clickable when viewing', async ({ page }) => { | ||||
|         const INVALID_TEST_LINK = 'http://bing.google.com'; | ||||
|     await nbUtils.enterTextEntry(page, `This should NOT be a link: ${TEST_LINK} is it?`); | ||||
|  | ||||
|         // Navigate to the notebook object | ||||
|         await page.goto(notebookObject.url); | ||||
|     const invalidLink = page.locator(`a[href="${TEST_LINK}"]`); | ||||
|  | ||||
|         // Reveal the notebook in the tree | ||||
|         await page.getByTitle('Show selected item in tree').click(); | ||||
|     expect(await invalidLink.count()).toBe(0); | ||||
|   }); | ||||
|   test('when a valid link with a subdomain and a valid domain in the whitelisted urls is entered into a notebook entry, it becomes clickable when viewing', async ({ | ||||
|     page | ||||
|   }) => { | ||||
|     const INVALID_TEST_LINK = 'http://bing.google.com'; | ||||
|  | ||||
|         await nbUtils.enterTextEntry(page, `This should be a link: ${INVALID_TEST_LINK} is it?`); | ||||
|     // Navigate to the notebook object | ||||
|     await page.goto(notebookObject.url); | ||||
|  | ||||
|         const validLink = page.locator(`a[href="${INVALID_TEST_LINK}"]`); | ||||
|     // Reveal the notebook in the tree | ||||
|     await page.getByTitle('Show selected item in tree').click(); | ||||
|  | ||||
|         expect(await validLink.count()).toBe(1); | ||||
|     }); | ||||
|     test('when a valid secure link is entered into a notebook entry, it becomes clickable when viewing', async ({ page }) => { | ||||
|         const TEST_LINK = 'https://www.google.com'; | ||||
|     await nbUtils.enterTextEntry(page, `This should be a link: ${INVALID_TEST_LINK} is it?`); | ||||
|  | ||||
|         // Navigate to the notebook object | ||||
|         await page.goto(notebookObject.url); | ||||
|     const validLink = page.locator(`a[href="${INVALID_TEST_LINK}"]`); | ||||
|  | ||||
|         // Reveal the notebook in the tree | ||||
|         await page.getByTitle('Show selected item in tree').click(); | ||||
|     expect(await validLink.count()).toBe(1); | ||||
|   }); | ||||
|   test('when a valid secure link is entered into a notebook entry, it becomes clickable when viewing', async ({ | ||||
|     page | ||||
|   }) => { | ||||
|     const TEST_LINK = 'https://www.google.com'; | ||||
|  | ||||
|         await nbUtils.enterTextEntry(page, `This should be a link: ${TEST_LINK} is it?`); | ||||
|     // Navigate to the notebook object | ||||
|     await page.goto(notebookObject.url); | ||||
|  | ||||
|         const validLink = page.locator(`a[href="${TEST_LINK}"]`); | ||||
|     // Reveal the notebook in the tree | ||||
|     await page.getByTitle('Show selected item in tree').click(); | ||||
|  | ||||
|         // Start waiting for popup before clicking. Note no await. | ||||
|         const popupPromise = page.waitForEvent('popup'); | ||||
|     await nbUtils.enterTextEntry(page, `This should be a link: ${TEST_LINK} is it?`); | ||||
|  | ||||
|         await validLink.click(); | ||||
|         const popup = await popupPromise; | ||||
|     const validLink = page.locator(`a[href="${TEST_LINK}"]`); | ||||
|  | ||||
|         // Wait for the popup to load. | ||||
|         await popup.waitForLoadState(); | ||||
|         expect.soft(popup.url()).toContain('www.google.com'); | ||||
|     // Start waiting for popup before clicking. Note no await. | ||||
|     const popupPromise = page.waitForEvent('popup'); | ||||
|  | ||||
|         expect(await validLink.count()).toBe(1); | ||||
|     }); | ||||
|     test('when a nefarious link is entered into a notebook entry, it is sanitized when viewing', async ({ page }) => { | ||||
|         const TEST_LINK = 'http://www.google.com?bad='; | ||||
|         const TEST_LINK_BAD = `http://www.google.com?bad=<script>alert('gimme your cookies')</script>`; | ||||
|     await validLink.click(); | ||||
|     const popup = await popupPromise; | ||||
|  | ||||
|         // Navigate to the notebook object | ||||
|         await page.goto(notebookObject.url); | ||||
|     // Wait for the popup to load. | ||||
|     await popup.waitForLoadState(); | ||||
|     expect.soft(popup.url()).toContain('www.google.com'); | ||||
|  | ||||
|         // Reveal the notebook in the tree | ||||
|         await page.getByTitle('Show selected item in tree').click(); | ||||
|     expect(await validLink.count()).toBe(1); | ||||
|   }); | ||||
|   test('when a nefarious link is entered into a notebook entry, it is sanitized when viewing', async ({ | ||||
|     page | ||||
|   }) => { | ||||
|     const TEST_LINK = 'http://www.google.com?bad='; | ||||
|     const TEST_LINK_BAD = `http://www.google.com?bad=<script>alert('gimme your cookies')</script>`; | ||||
|  | ||||
|         await nbUtils.enterTextEntry(page, `This should be a link, BUT not a bad link: ${TEST_LINK_BAD} is it?`); | ||||
|     // Navigate to the notebook object | ||||
|     await page.goto(notebookObject.url); | ||||
|  | ||||
|         const sanitizedLink = page.locator(`a[href="${TEST_LINK}"]`); | ||||
|         const unsanitizedLink = page.locator(`a[href="${TEST_LINK_BAD}"]`); | ||||
|     // Reveal the notebook in the tree | ||||
|     await page.getByTitle('Show selected item in tree').click(); | ||||
|  | ||||
|         expect.soft(await sanitizedLink.count()).toBe(1); | ||||
|         expect(await unsanitizedLink.count()).toBe(0); | ||||
|     }); | ||||
|     await nbUtils.enterTextEntry( | ||||
|       page, | ||||
|       `This should be a link, BUT not a bad link: ${TEST_LINK_BAD} is it?` | ||||
|     ); | ||||
|  | ||||
|     const sanitizedLink = page.locator(`a[href="${TEST_LINK}"]`); | ||||
|     const unsanitizedLink = page.locator(`a[href="${TEST_LINK_BAD}"]`); | ||||
|  | ||||
|     expect.soft(await sanitizedLink.count()).toBe(1); | ||||
|     expect(await unsanitizedLink.count()).toBe(0); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -29,106 +29,135 @@ const { test, expect } = require('../../../../pluginFixtures'); | ||||
| // const nbUtils = require('../../../../helper/notebookUtils'); | ||||
|  | ||||
| test.describe('Snapshot Menu tests', () => { | ||||
|     test.fixme('When no default notebook is selected, Snapshot Menu dropdown should only have a single option', async ({ page }) => { | ||||
|         // There should be no default notebook | ||||
|         // Clear default notebook if exists using `localStorage.setItem('notebook-storage', null);` | ||||
|         // refresh page | ||||
|         // Click on 'Notebook Snaphot Menu' | ||||
|         // 'save to Notebook Snapshots' should be only option there | ||||
|     }); | ||||
|     test.fixme('When default notebook is updated selected, Snapshot Menu dropdown should list it as the newest option', async ({ page }) => { | ||||
|         // Create 2a notebooks | ||||
|         // Set Notebook A as Default | ||||
|         // Open Snapshot Menu and note that Notebook A is listed | ||||
|         // Close Snapshot Menu | ||||
|         // Set Default Notebook to Notebook B | ||||
|         // Open Snapshot Notebook and note that Notebook B is listed | ||||
|         // Select Default Notebook Option and verify that Snapshot is added to Notebook B | ||||
|     }); | ||||
|     test.fixme('Can add Snapshots via Snapshot Menu and details are correct', async ({ page }) => { | ||||
|         //Note this should be a visual test, too | ||||
|         // Create Telemetry object | ||||
|         // Create A notebook with many pages and sections. | ||||
|         // Set page and section defaults to be between first and last of many. i.e. 3 of 5 | ||||
|         // Navigate to Telemetry object | ||||
|         // Select Default Notebook Option and verify that Snapshot is added to Notebook A | ||||
|         // Verify Snapshot Details appear correctly | ||||
|     }); | ||||
|     test.fixme('Snapshots adjust time conductor', async ({ page }) => { | ||||
|         // Create Telemetry object | ||||
|         // Set Telemetry object's timeconductor to Fixed time with Start and Endtimes are recorded | ||||
|         // Embed Telemetry object into notebook | ||||
|         // Set Time Conductor to Local clock | ||||
|         // Click into embedded telemetry object and verify object appears with same fixed time from record | ||||
|     }); | ||||
|   test.fixme( | ||||
|     'When no default notebook is selected, Snapshot Menu dropdown should only have a single option', | ||||
|     async ({ page }) => { | ||||
|       // There should be no default notebook | ||||
|       // Clear default notebook if exists using `localStorage.setItem('notebook-storage', null);` | ||||
|       // refresh page | ||||
|       // Click on 'Notebook Snaphot Menu' | ||||
|       // 'save to Notebook Snapshots' should be only option there | ||||
|     } | ||||
|   ); | ||||
|   test.fixme( | ||||
|     'When default notebook is updated selected, Snapshot Menu dropdown should list it as the newest option', | ||||
|     async ({ page }) => { | ||||
|       // Create 2a notebooks | ||||
|       // Set Notebook A as Default | ||||
|       // Open Snapshot Menu and note that Notebook A is listed | ||||
|       // Close Snapshot Menu | ||||
|       // Set Default Notebook to Notebook B | ||||
|       // Open Snapshot Notebook and note that Notebook B is listed | ||||
|       // Select Default Notebook Option and verify that Snapshot is added to Notebook B | ||||
|     } | ||||
|   ); | ||||
|   test.fixme('Can add Snapshots via Snapshot Menu and details are correct', async ({ page }) => { | ||||
|     //Note this should be a visual test, too | ||||
|     // Create Telemetry object | ||||
|     // Create A notebook with many pages and sections. | ||||
|     // Set page and section defaults to be between first and last of many. i.e. 3 of 5 | ||||
|     // Navigate to Telemetry object | ||||
|     // Select Default Notebook Option and verify that Snapshot is added to Notebook A | ||||
|     // Verify Snapshot Details appear correctly | ||||
|   }); | ||||
|   test.fixme('Snapshots adjust time conductor', async ({ page }) => { | ||||
|     // Create Telemetry object | ||||
|     // Set Telemetry object's timeconductor to Fixed time with Start and Endtimes are recorded | ||||
|     // Embed Telemetry object into notebook | ||||
|     // Set Time Conductor to Local clock | ||||
|     // Click into embedded telemetry object and verify object appears with same fixed time from record | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| test.describe('Snapshot Container tests', () => { | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         //Navigate to baseURL | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|   test.beforeEach(async ({ page }) => { | ||||
|     //Navigate to baseURL | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|  | ||||
|         // Create Notebook | ||||
|         // const notebook = await createDomainObjectWithDefaults(page, { | ||||
|         //     type: 'Notebook', | ||||
|         //     name: "Test Notebook" | ||||
|         // }); | ||||
|         // // Create Overlay Plot | ||||
|         // const snapShotObject = await createDomainObjectWithDefaults(page, { | ||||
|         //     type: 'Overlay Plot', | ||||
|         //     name: "Dropped Overlay Plot" | ||||
|         // }); | ||||
|     // Create Notebook | ||||
|     // const notebook = await createDomainObjectWithDefaults(page, { | ||||
|     //     type: 'Notebook', | ||||
|     //     name: "Test Notebook" | ||||
|     // }); | ||||
|     // // Create Overlay Plot | ||||
|     // const snapShotObject = await createDomainObjectWithDefaults(page, { | ||||
|     //     type: 'Overlay Plot', | ||||
|     //     name: "Dropped Overlay Plot" | ||||
|     // }); | ||||
|  | ||||
|         await page.getByRole('button', { name: ' Snapshot ' }).click(); | ||||
|         await page.getByRole('menuitem', { name: ' Save to Notebook Snapshots' }).click(); | ||||
|         await page.getByRole('button', { name: 'Show' }).click(); | ||||
|  | ||||
|     }); | ||||
|     test.fixme('5 Snapshots can be added to a container', async ({ page }) => {}); | ||||
|     test.fixme('5 Snapshots can be added to a container and Deleted with Delete All action', async ({ page }) => {}); | ||||
|     test.fixme('A snapshot can be Deleted from Container with 3 dot action menu', async ({ page }) => {}); | ||||
|     test.fixme('A snapshot can be Viewed, Annotated, display deleted, and saved from Container with 3 dot action menu', async ({ page }) => { | ||||
|         await page.locator('.c-snapshot.c-ne__embed').first().getByTitle('More options').click(); | ||||
|         await page.getByRole('menuitem', { name: ' View Snapshot' }).click(); | ||||
|         await expect(page.locator('.c-overlay__outer')).toBeVisible(); | ||||
|         await page.getByTitle('Annotate').click(); | ||||
|         await expect(page.locator('#snap-annotation-canvas')).toBeVisible(); | ||||
|         await page.getByRole('button', { name: '' }).click(); | ||||
|         // await expect(page.locator('#snap-annotation-canvas')).not.toBeVisible(); | ||||
|         await page.getByRole('button', { name: 'Save' }).click(); | ||||
|         await page.getByRole('button', { name: 'Done' }).click(); | ||||
|         //await expect(await page.locator) | ||||
|     }); | ||||
|     test('A snapshot can be Quick Viewed from Container with 3 dot action menu', async ({ page }) => { | ||||
|         await page.locator('.c-snapshot.c-ne__embed').first().getByTitle('More options').click(); | ||||
|         await page.getByRole('menuitem', { name: 'Quick View' }).click(); | ||||
|         await expect(page.locator('.c-overlay__outer')).toBeVisible(); | ||||
|     }); | ||||
|     test.fixme('A snapshot can be Navigated To from Container with 3 dot action menu', async ({ page }) => {}); | ||||
|     test.fixme('A snapshot can be Navigated To Item in Time from Container with 3 dot action menu', async ({ page }) => {}); | ||||
|     test.fixme('A snapshot Container can be open and closed', async ({ page }) => {}); | ||||
|     test.fixme('Can add object to Snapshot container and pull into notebook and create a new entry', async ({ page }) => { | ||||
|         //Create Notebook | ||||
|         //Create Telemetry Object | ||||
|         //From Telemetry Object, use 'save to Notebook Snapshots' | ||||
|         //Snapshots indicator should blink, click on it to view snapshots | ||||
|         //Navigate to Notebook | ||||
|         //Drag and Drop onto droppable area for new entry | ||||
|         //New Entry created with given snapshot added | ||||
|         //Snapshot removed from container? | ||||
|     }); | ||||
|     test.fixme('Can add object to Snapshot container and pull into notebook and existing entry', async ({ page }) => { | ||||
|         //Create Notebook | ||||
|         //Create Telemetry Object | ||||
|         //From Telemetry Object, use 'save to Notebook Snapshots' | ||||
|         //Snapshots indicator should blink, click on it to view snapshots | ||||
|         //Navigate to Notebook | ||||
|         //Drag and Drop into exiting entry | ||||
|         //Existing Entry updated with given snapshot | ||||
|         //Snapshot removed from container? | ||||
|     }); | ||||
|     test.fixme('Verify Embedded options for PNG, JPG, and Annotate work correctly', async ({ page }) => { | ||||
|         //Add snapshot to container | ||||
|         //Verify PNG, JPG, and Annotate buttons work correctly | ||||
|     }); | ||||
|     await page.getByRole('button', { name: ' Snapshot ' }).click(); | ||||
|     await page.getByRole('menuitem', { name: ' Save to Notebook Snapshots' }).click(); | ||||
|     await page.getByRole('button', { name: 'Show' }).click(); | ||||
|   }); | ||||
|   test.fixme('5 Snapshots can be added to a container', async ({ page }) => {}); | ||||
|   test.fixme( | ||||
|     '5 Snapshots can be added to a container and Deleted with Delete All action', | ||||
|     async ({ page }) => {} | ||||
|   ); | ||||
|   test.fixme( | ||||
|     'A snapshot can be Deleted from Container with 3 dot action menu', | ||||
|     async ({ page }) => {} | ||||
|   ); | ||||
|   test.fixme( | ||||
|     'A snapshot can be Viewed, Annotated, display deleted, and saved from Container with 3 dot action menu', | ||||
|     async ({ page }) => { | ||||
|       await page.locator('.c-snapshot.c-ne__embed').first().getByTitle('More options').click(); | ||||
|       await page.getByRole('menuitem', { name: ' View Snapshot' }).click(); | ||||
|       await expect(page.locator('.c-overlay__outer')).toBeVisible(); | ||||
|       await page.getByTitle('Annotate').click(); | ||||
|       await expect(page.locator('#snap-annotation-canvas')).toBeVisible(); | ||||
|       await page.getByRole('button', { name: '' }).click(); | ||||
|       // await expect(page.locator('#snap-annotation-canvas')).not.toBeVisible(); | ||||
|       await page.getByRole('button', { name: 'Save' }).click(); | ||||
|       await page.getByRole('button', { name: 'Done' }).click(); | ||||
|       //await expect(await page.locator) | ||||
|     } | ||||
|   ); | ||||
|   test('A snapshot can be Quick Viewed from Container with 3 dot action menu', async ({ page }) => { | ||||
|     await page.locator('.c-snapshot.c-ne__embed').first().getByTitle('More options').click(); | ||||
|     await page.getByRole('menuitem', { name: 'Quick View' }).click(); | ||||
|     await expect(page.locator('.c-overlay__outer')).toBeVisible(); | ||||
|   }); | ||||
|   test.fixme( | ||||
|     'A snapshot can be Navigated To from Container with 3 dot action menu', | ||||
|     async ({ page }) => {} | ||||
|   ); | ||||
|   test.fixme( | ||||
|     'A snapshot can be Navigated To Item in Time from Container with 3 dot action menu', | ||||
|     async ({ page }) => {} | ||||
|   ); | ||||
|   test.fixme('A snapshot Container can be open and closed', async ({ page }) => {}); | ||||
|   test.fixme( | ||||
|     'Can add object to Snapshot container and pull into notebook and create a new entry', | ||||
|     async ({ page }) => { | ||||
|       //Create Notebook | ||||
|       //Create Telemetry Object | ||||
|       //From Telemetry Object, use 'save to Notebook Snapshots' | ||||
|       //Snapshots indicator should blink, click on it to view snapshots | ||||
|       //Navigate to Notebook | ||||
|       //Drag and Drop onto droppable area for new entry | ||||
|       //New Entry created with given snapshot added | ||||
|       //Snapshot removed from container? | ||||
|     } | ||||
|   ); | ||||
|   test.fixme( | ||||
|     'Can add object to Snapshot container and pull into notebook and existing entry', | ||||
|     async ({ page }) => { | ||||
|       //Create Notebook | ||||
|       //Create Telemetry Object | ||||
|       //From Telemetry Object, use 'save to Notebook Snapshots' | ||||
|       //Snapshots indicator should blink, click on it to view snapshots | ||||
|       //Navigate to Notebook | ||||
|       //Drag and Drop into exiting entry | ||||
|       //Existing Entry updated with given snapshot | ||||
|       //Snapshot removed from container? | ||||
|     } | ||||
|   ); | ||||
|   test.fixme( | ||||
|     'Verify Embedded options for PNG, JPG, and Annotate work correctly', | ||||
|     async ({ page }) => { | ||||
|       //Add snapshot to container | ||||
|       //Verify PNG, JPG, and Annotate buttons work correctly | ||||
|     } | ||||
|   ); | ||||
| }); | ||||
|   | ||||
| @@ -29,180 +29,184 @@ const { createDomainObjectWithDefaults } = require('../../../../appActions'); | ||||
| const nbUtils = require('../../../../helper/notebookUtils'); | ||||
|  | ||||
| test.describe('Notebook Tests with CouchDB @couchdb', () => { | ||||
|     let testNotebook; | ||||
|   let testNotebook; | ||||
|  | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         //Navigate to baseURL | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|   test.beforeEach(async ({ page }) => { | ||||
|     //Navigate to baseURL | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|  | ||||
|         // Create Notebook | ||||
|         testNotebook = await createDomainObjectWithDefaults(page, {type: 'Notebook' }); | ||||
|         await page.goto(testNotebook.url, { waitUntil: 'networkidle'}); | ||||
|     // Create Notebook | ||||
|     testNotebook = await createDomainObjectWithDefaults(page, { type: 'Notebook' }); | ||||
|     await page.goto(testNotebook.url, { waitUntil: 'networkidle' }); | ||||
|   }); | ||||
|  | ||||
|   test('Inspect Notebook Entry Network Requests', async ({ page }) => { | ||||
|     //Ensure we're on the annotations Tab in the inspector | ||||
|     await page.getByText('Annotations').click(); | ||||
|     // Expand sidebar | ||||
|     await page.locator('.c-notebook__toggle-nav-button').click(); | ||||
|  | ||||
|     // Collect all request events to count and assert after notebook action | ||||
|     let notebookElementsRequests = []; | ||||
|     page.on('request', (request) => notebookElementsRequests.push(request)); | ||||
|  | ||||
|     //Clicking Add Page generates | ||||
|     let [notebookUrlRequest, allDocsRequest] = await Promise.all([ | ||||
|       // Waits for the next request with the specified url | ||||
|       page.waitForRequest(`**/openmct/${testNotebook.uuid}`), | ||||
|       page.waitForRequest('**/openmct/_all_docs?include_docs=true'), | ||||
|       // Triggers the request | ||||
|       page.click('[aria-label="Add Page"]') | ||||
|     ]); | ||||
|     // Ensures that there are no other network requests | ||||
|     await page.waitForLoadState('networkidle'); | ||||
|  | ||||
|     // Assert that only two requests are made | ||||
|     // Network Requests are: | ||||
|     // 1) The actual POST to create the page | ||||
|     // 2) The shared worker event from 👆 request | ||||
|     expect(notebookElementsRequests.length).toBe(2); | ||||
|  | ||||
|     // Assert on request object | ||||
|     expect(notebookUrlRequest.postDataJSON().metadata.name).toBe(testNotebook.name); | ||||
|     expect(notebookUrlRequest.postDataJSON().model.persisted).toBeGreaterThanOrEqual( | ||||
|       notebookUrlRequest.postDataJSON().model.modified | ||||
|     ); | ||||
|     expect(allDocsRequest.postDataJSON().keys).toContain(testNotebook.uuid); | ||||
|  | ||||
|     // Add an entry | ||||
|     // Network Requests are: | ||||
|     // 1) The actual POST to create the entry | ||||
|     // 2) The shared worker event from 👆 POST request | ||||
|     notebookElementsRequests = []; | ||||
|     await nbUtils.enterTextEntry(page, 'First Entry'); | ||||
|     await page.waitForLoadState('networkidle'); | ||||
|     expect(notebookElementsRequests.length).toBeLessThanOrEqual(2); | ||||
|  | ||||
|     // Add some tags | ||||
|     // Network Requests are for each tag creation are: | ||||
|     // 1) Getting the original path of the parent object | ||||
|     // 2) Getting the original path of the grandparent object (recursive call) | ||||
|     // 3) Creating the annotation/tag object | ||||
|     // 4) The shared worker event from 👆 POST request | ||||
|     // 5) Mutate notebook domain object's annotationModified property | ||||
|     // 6) The shared worker event from 👆 POST request | ||||
|     // 7) Notebooks fetching new annotations due to annotationModified changed | ||||
|     // 8) The update of the notebook domain's object's modified property | ||||
|     // 9) The shared worker event from 👆 POST request | ||||
|     // 10) Entry is timestamped | ||||
|     // 11) The shared worker event from 👆 POST request | ||||
|  | ||||
|     notebookElementsRequests = []; | ||||
|     await addTagAndAwaitNetwork(page, 'Driving'); | ||||
|     expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(11); | ||||
|  | ||||
|     notebookElementsRequests = []; | ||||
|     await addTagAndAwaitNetwork(page, 'Drilling'); | ||||
|     expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(11); | ||||
|  | ||||
|     notebookElementsRequests = []; | ||||
|     await addTagAndAwaitNetwork(page, 'Science'); | ||||
|     expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(11); | ||||
|  | ||||
|     // Delete all the tags | ||||
|     // Network requests are: | ||||
|     // 1) Send POST to mutate _delete property to true on annotation with tag | ||||
|     // 2) The shared worker event from 👆 POST request | ||||
|     // 3) Timestamp update on entry | ||||
|     // 4) The shared worker event from 👆 POST request | ||||
|     // This happens for 3 tags so 12 requests | ||||
|     notebookElementsRequests = []; | ||||
|     await removeTagAndAwaitNetwork(page, 'Driving'); | ||||
|     await removeTagAndAwaitNetwork(page, 'Drilling'); | ||||
|     await removeTagAndAwaitNetwork(page, 'Science'); | ||||
|     expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(12); | ||||
|  | ||||
|     // Add two more pages | ||||
|     await page.click('[aria-label="Add Page"]'); | ||||
|     await page.click('[aria-label="Add Page"]'); | ||||
|  | ||||
|     // Add three entries | ||||
|     await nbUtils.enterTextEntry(page, 'First Entry'); | ||||
|     await nbUtils.enterTextEntry(page, 'Second Entry'); | ||||
|     await nbUtils.enterTextEntry(page, 'Third Entry'); | ||||
|  | ||||
|     // Add three tags | ||||
|     await addTagAndAwaitNetwork(page, 'Science'); | ||||
|     await addTagAndAwaitNetwork(page, 'Drilling'); | ||||
|     await addTagAndAwaitNetwork(page, 'Driving'); | ||||
|  | ||||
|     // Add a fourth entry | ||||
|     // Network requests are: | ||||
|     // 1) Send POST to add new entry | ||||
|     // 2) The shared worker event from 👆 POST request | ||||
|     // 3) Timestamp update on entry | ||||
|     // 4) The shared worker event from 👆 POST request | ||||
|     notebookElementsRequests = []; | ||||
|     await nbUtils.enterTextEntry(page, 'Fourth Entry'); | ||||
|     page.waitForLoadState('networkidle'); | ||||
|  | ||||
|     expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(4); | ||||
|  | ||||
|     // Add a fifth entry | ||||
|     // Network requests are: | ||||
|     // 1) Send POST to add new entry | ||||
|     // 2) The shared worker event from 👆 POST request | ||||
|     // 3) Timestamp update on entry | ||||
|     // 4) The shared worker event from 👆 POST request | ||||
|     notebookElementsRequests = []; | ||||
|     await nbUtils.enterTextEntry(page, 'Fifth Entry'); | ||||
|     page.waitForLoadState('networkidle'); | ||||
|  | ||||
|     expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(4); | ||||
|  | ||||
|     // Add a sixth entry | ||||
|     // 1) Send POST to add new entry | ||||
|     // 2) The shared worker event from 👆 POST request | ||||
|     // 3) Timestamp update on entry | ||||
|     // 4) The shared worker event from 👆 POST request | ||||
|     notebookElementsRequests = []; | ||||
|     await nbUtils.enterTextEntry(page, 'Sixth Entry'); | ||||
|     page.waitForLoadState('networkidle'); | ||||
|  | ||||
|     expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(4); | ||||
|   }); | ||||
|  | ||||
|   test('Search tests', async ({ page }) => { | ||||
|     test.info().annotations.push({ | ||||
|       type: 'issue', | ||||
|       description: 'https://github.com/akhenry/openmct-yamcs/issues/69' | ||||
|     }); | ||||
|     await page.getByText('Annotations').click(); | ||||
|     await nbUtils.enterTextEntry(page, 'First Entry'); | ||||
|  | ||||
|     test('Inspect Notebook Entry Network Requests', async ({ page }) => { | ||||
|         //Ensure we're on the annotations Tab in the inspector | ||||
|         await page.getByText('Annotations').click(); | ||||
|         // Expand sidebar | ||||
|         await page.locator('.c-notebook__toggle-nav-button').click(); | ||||
|     // Add three tags | ||||
|     await addTagAndAwaitNetwork(page, 'Science'); | ||||
|     await addTagAndAwaitNetwork(page, 'Drilling'); | ||||
|     await addTagAndAwaitNetwork(page, 'Driving'); | ||||
|  | ||||
|         // Collect all request events to count and assert after notebook action | ||||
|         let notebookElementsRequests = []; | ||||
|         page.on('request', (request) => notebookElementsRequests.push(request)); | ||||
|     await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); | ||||
|     //Partial match for "Science" should only return Science | ||||
|     await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Sc'); | ||||
|     await expect(page.locator('[aria-label="Search Result"]').first()).toContainText('Science'); | ||||
|     await expect(page.locator('[aria-label="Search Result"]').first()).not.toContainText('Driving'); | ||||
|     await expect(page.locator('[aria-label="Search Result"]').first()).not.toContainText( | ||||
|       'Drilling' | ||||
|     ); | ||||
|  | ||||
|         //Clicking Add Page generates | ||||
|         let [notebookUrlRequest, allDocsRequest] = await Promise.all([ | ||||
|             // Waits for the next request with the specified url | ||||
|             page.waitForRequest(`**/openmct/${testNotebook.uuid}`), | ||||
|             page.waitForRequest('**/openmct/_all_docs?include_docs=true'), | ||||
|             // Triggers the request | ||||
|             page.click('[aria-label="Add Page"]') | ||||
|         ]); | ||||
|         // Ensures that there are no other network requests | ||||
|         await page.waitForLoadState('networkidle'); | ||||
|  | ||||
|         // Assert that only two requests are made | ||||
|         // Network Requests are: | ||||
|         // 1) The actual POST to create the page | ||||
|         // 2) The shared worker event from 👆 request | ||||
|         expect(notebookElementsRequests.length).toBe(2); | ||||
|  | ||||
|         // Assert on request object | ||||
|         expect(notebookUrlRequest.postDataJSON().metadata.name).toBe(testNotebook.name); | ||||
|         expect(notebookUrlRequest.postDataJSON().model.persisted).toBeGreaterThanOrEqual(notebookUrlRequest.postDataJSON().model.modified); | ||||
|         expect(allDocsRequest.postDataJSON().keys).toContain(testNotebook.uuid); | ||||
|  | ||||
|         // Add an entry | ||||
|         // Network Requests are: | ||||
|         // 1) The actual POST to create the entry | ||||
|         // 2) The shared worker event from 👆 POST request | ||||
|         notebookElementsRequests = []; | ||||
|         await nbUtils.enterTextEntry(page, 'First Entry'); | ||||
|         await page.waitForLoadState('networkidle'); | ||||
|         expect(notebookElementsRequests.length).toBeLessThanOrEqual(2); | ||||
|  | ||||
|         // Add some tags | ||||
|         // Network Requests are for each tag creation are: | ||||
|         // 1) Getting the original path of the parent object | ||||
|         // 2) Getting the original path of the grandparent object (recursive call) | ||||
|         // 3) Creating the annotation/tag object | ||||
|         // 4) The shared worker event from 👆 POST request | ||||
|         // 5) Mutate notebook domain object's annotationModified property | ||||
|         // 6) The shared worker event from 👆 POST request | ||||
|         // 7) Notebooks fetching new annotations due to annotationModified changed | ||||
|         // 8) The update of the notebook domain's object's modified property | ||||
|         // 9) The shared worker event from 👆 POST request | ||||
|         // 10) Entry is timestamped | ||||
|         // 11) The shared worker event from 👆 POST request | ||||
|  | ||||
|         notebookElementsRequests = []; | ||||
|         await addTagAndAwaitNetwork(page, 'Driving'); | ||||
|         expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(11); | ||||
|  | ||||
|         notebookElementsRequests = []; | ||||
|         await addTagAndAwaitNetwork(page, 'Drilling'); | ||||
|         expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(11); | ||||
|  | ||||
|         notebookElementsRequests = []; | ||||
|         await addTagAndAwaitNetwork(page, 'Science'); | ||||
|         expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(11); | ||||
|  | ||||
|         // Delete all the tags | ||||
|         // Network requests are: | ||||
|         // 1) Send POST to mutate _delete property to true on annotation with tag | ||||
|         // 2) The shared worker event from 👆 POST request | ||||
|         // 3) Timestamp update on entry | ||||
|         // 4) The shared worker event from 👆 POST request | ||||
|         // This happens for 3 tags so 12 requests | ||||
|         notebookElementsRequests = []; | ||||
|         await removeTagAndAwaitNetwork(page, 'Driving'); | ||||
|         await removeTagAndAwaitNetwork(page, 'Drilling'); | ||||
|         await removeTagAndAwaitNetwork(page, 'Science'); | ||||
|         expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(12); | ||||
|  | ||||
|         // Add two more pages | ||||
|         await page.click('[aria-label="Add Page"]'); | ||||
|         await page.click('[aria-label="Add Page"]'); | ||||
|  | ||||
|         // Add three entries | ||||
|         await nbUtils.enterTextEntry(page, 'First Entry'); | ||||
|         await nbUtils.enterTextEntry(page, 'Second Entry'); | ||||
|         await nbUtils.enterTextEntry(page, 'Third Entry'); | ||||
|  | ||||
|         // Add three tags | ||||
|         await addTagAndAwaitNetwork(page, 'Science'); | ||||
|         await addTagAndAwaitNetwork(page, 'Drilling'); | ||||
|         await addTagAndAwaitNetwork(page, 'Driving'); | ||||
|  | ||||
|         // Add a fourth entry | ||||
|         // Network requests are: | ||||
|         // 1) Send POST to add new entry | ||||
|         // 2) The shared worker event from 👆 POST request | ||||
|         // 3) Timestamp update on entry | ||||
|         // 4) The shared worker event from 👆 POST request | ||||
|         notebookElementsRequests = []; | ||||
|         await nbUtils.enterTextEntry(page, 'Fourth Entry'); | ||||
|         page.waitForLoadState('networkidle'); | ||||
|  | ||||
|         expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(4); | ||||
|  | ||||
|         // Add a fifth entry | ||||
|         // Network requests are: | ||||
|         // 1) Send POST to add new entry | ||||
|         // 2) The shared worker event from 👆 POST request | ||||
|         // 3) Timestamp update on entry | ||||
|         // 4) The shared worker event from 👆 POST request | ||||
|         notebookElementsRequests = []; | ||||
|         await nbUtils.enterTextEntry(page, 'Fifth Entry'); | ||||
|         page.waitForLoadState('networkidle'); | ||||
|  | ||||
|         expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(4); | ||||
|  | ||||
|         // Add a sixth entry | ||||
|         // 1) Send POST to add new entry | ||||
|         // 2) The shared worker event from 👆 POST request | ||||
|         // 3) Timestamp update on entry | ||||
|         // 4) The shared worker event from 👆 POST request | ||||
|         notebookElementsRequests = []; | ||||
|         await nbUtils.enterTextEntry(page, 'Sixth Entry'); | ||||
|         page.waitForLoadState('networkidle'); | ||||
|  | ||||
|         expect(filterNonFetchRequests(notebookElementsRequests).length).toBeLessThanOrEqual(4); | ||||
|     }); | ||||
|  | ||||
|     test('Search tests', async ({ page }) => { | ||||
|         test.info().annotations.push({ | ||||
|             type: 'issue', | ||||
|             description: 'https://github.com/akhenry/openmct-yamcs/issues/69' | ||||
|         }); | ||||
|         await page.getByText('Annotations').click(); | ||||
|         await nbUtils.enterTextEntry(page, 'First Entry'); | ||||
|  | ||||
|         // Add three tags | ||||
|         await addTagAndAwaitNetwork(page, 'Science'); | ||||
|         await addTagAndAwaitNetwork(page, 'Drilling'); | ||||
|         await addTagAndAwaitNetwork(page, 'Driving'); | ||||
|  | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); | ||||
|         //Partial match for "Science" should only return Science | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Sc'); | ||||
|         await expect(page.locator('[aria-label="Search Result"]').first()).toContainText("Science"); | ||||
|         await expect(page.locator('[aria-label="Search Result"]').first()).not.toContainText("Driving"); | ||||
|         await expect(page.locator('[aria-label="Search Result"]').first()).not.toContainText("Drilling"); | ||||
|  | ||||
|         //Searching for a tag which does not exist should return an empty result | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Xq'); | ||||
|         await expect(page.locator('text=No results found')).toBeVisible(); | ||||
|     }); | ||||
|     //Searching for a tag which does not exist should return an empty result | ||||
|     await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); | ||||
|     await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Xq'); | ||||
|     await expect(page.locator('text=No results found')).toBeVisible(); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| // Try to reduce indeterminism of browser requests by only returning fetch requests. | ||||
| // Filter out preflight CORS, fetching stylesheets, page icons, etc. that can occur during tests | ||||
| function filterNonFetchRequests(requests) { | ||||
|     return requests.filter(request => { | ||||
|         return (request.resourceType() === 'fetch'); | ||||
|     }); | ||||
|   return requests.filter((request) => { | ||||
|     return request.resourceType() === 'fetch'; | ||||
|   }); | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -212,17 +216,17 @@ function filterNonFetchRequests(requests) { | ||||
|  * @param {string} tagName | ||||
|  */ | ||||
| async function addTagAndAwaitNetwork(page, tagName) { | ||||
|     await page.hover(`button:has-text("Add Tag")`); | ||||
|     await page.locator(`button:has-text("Add Tag")`).click(); | ||||
|     await page.locator('[placeholder="Type to select tag"]').click(); | ||||
|     await Promise.all([ | ||||
|         // Waits for the next request with the specified url | ||||
|         page.waitForRequest('**/openmct/_all_docs?include_docs=true'), | ||||
|         // Triggers the request | ||||
|         page.locator(`[aria-label="Autocomplete Options"] >> text=${tagName}`).click(), | ||||
|         expect(page.locator(`[aria-label="Tag"]:has-text("${tagName}")`)).toBeVisible() | ||||
|     ]); | ||||
|     await page.waitForLoadState('networkidle'); | ||||
|   await page.hover(`button:has-text("Add Tag")`); | ||||
|   await page.locator(`button:has-text("Add Tag")`).click(); | ||||
|   await page.locator('[placeholder="Type to select tag"]').click(); | ||||
|   await Promise.all([ | ||||
|     // Waits for the next request with the specified url | ||||
|     page.waitForRequest('**/openmct/_all_docs?include_docs=true'), | ||||
|     // Triggers the request | ||||
|     page.locator(`[aria-label="Autocomplete Options"] >> text=${tagName}`).click(), | ||||
|     expect(page.locator(`[aria-label="Tag"]:has-text("${tagName}")`)).toBeVisible() | ||||
|   ]); | ||||
|   await page.waitForLoadState('networkidle'); | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -232,12 +236,14 @@ async function addTagAndAwaitNetwork(page, tagName) { | ||||
|  * @param {string} tagName | ||||
|  */ | ||||
| async function removeTagAndAwaitNetwork(page, tagName) { | ||||
|     await page.hover(`[aria-label="Tag"]:has-text("${tagName}")`); | ||||
|     await Promise.all([ | ||||
|         page.locator(`[aria-label="Remove tag ${tagName}"]`).click(), | ||||
|         //With this pattern, we're awaiting the response but asserting on the request payload. | ||||
|         page.waitForResponse(resp => resp.request().postData().includes(`"_deleted":true`) && resp.status() === 201) | ||||
|     ]); | ||||
|     await expect(page.locator(`[aria-label="Tag"]:has-text("${tagName}")`)).toBeHidden(); | ||||
|     await page.waitForLoadState('networkidle'); | ||||
|   await page.hover(`[aria-label="Tag"]:has-text("${tagName}")`); | ||||
|   await Promise.all([ | ||||
|     page.locator(`[aria-label="Remove tag ${tagName}"]`).click(), | ||||
|     //With this pattern, we're awaiting the response but asserting on the request payload. | ||||
|     page.waitForResponse( | ||||
|       (resp) => resp.request().postData().includes(`"_deleted":true`) && resp.status() === 201 | ||||
|     ) | ||||
|   ]); | ||||
|   await expect(page.locator(`[aria-label="Tag"]:has-text("${tagName}")`)).toBeHidden(); | ||||
|   await page.waitForLoadState('networkidle'); | ||||
| } | ||||
|   | ||||
| @@ -19,9 +19,12 @@ | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* global __dirname */ | ||||
| const { test, expect, streamToString } = require('../../../../pluginFixtures'); | ||||
| const { openObjectTreeContextMenu, createDomainObjectWithDefaults } = require('../../../../appActions'); | ||||
| const { | ||||
|   openObjectTreeContextMenu, | ||||
|   createDomainObjectWithDefaults | ||||
| } = require('../../../../appActions'); | ||||
| const path = require('path'); | ||||
| const nbUtils = require('../../../../helper/notebookUtils'); | ||||
|  | ||||
| @@ -30,190 +33,186 @@ const TEST_TEXT_NAME = 'Test Page'; | ||||
| const CUSTOM_NAME = 'CUSTOM_NAME'; | ||||
|  | ||||
| test.describe('Restricted Notebook', () => { | ||||
|     let notebook; | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         notebook = await startAndAddRestrictedNotebookObject(page); | ||||
|     }); | ||||
|   let notebook; | ||||
|   test.beforeEach(async ({ page }) => { | ||||
|     notebook = await startAndAddRestrictedNotebookObject(page); | ||||
|   }); | ||||
|  | ||||
|     test('Can be renamed @addInit', async ({ page }) => { | ||||
|         await expect(page.locator('.l-browse-bar__object-name')).toContainText(`${notebook.name}`); | ||||
|     }); | ||||
|   test('Can be renamed @addInit', async ({ page }) => { | ||||
|     await expect(page.locator('.l-browse-bar__object-name')).toContainText(`${notebook.name}`); | ||||
|   }); | ||||
|  | ||||
|     test('Can be deleted if there are no locked pages @addInit', async ({ page }) => { | ||||
|         await openObjectTreeContextMenu(page, notebook.url); | ||||
|   test('Can be deleted if there are no locked pages @addInit', async ({ page }) => { | ||||
|     await openObjectTreeContextMenu(page, notebook.url); | ||||
|  | ||||
|         const menuOptions = page.locator('.c-menu ul'); | ||||
|         await expect.soft(menuOptions).toContainText('Remove'); | ||||
|     const menuOptions = page.locator('.c-menu ul'); | ||||
|     await expect.soft(menuOptions).toContainText('Remove'); | ||||
|  | ||||
|         const restrictedNotebookTreeObject = page.locator(`a:has-text("${notebook.name}")`); | ||||
|     const restrictedNotebookTreeObject = page.locator(`a:has-text("${notebook.name}")`); | ||||
|  | ||||
|         // notebook tree object exists | ||||
|         expect.soft(await restrictedNotebookTreeObject.count()).toEqual(1); | ||||
|     // notebook tree object exists | ||||
|     expect.soft(await restrictedNotebookTreeObject.count()).toEqual(1); | ||||
|  | ||||
|         // Click Remove Text | ||||
|         await page.locator('li[role="menuitem"]:has-text("Remove")').click(); | ||||
|     // Click Remove Text | ||||
|     await page.locator('li[role="menuitem"]:has-text("Remove")').click(); | ||||
|  | ||||
|         // Click 'OK' on confirmation window and wait for save banner to appear | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('button:has-text("OK")').click(), | ||||
|             page.waitForSelector('.c-message-banner__message') | ||||
|         ]); | ||||
|     // Click 'OK' on confirmation window and wait for save banner to appear | ||||
|     await Promise.all([ | ||||
|       page.waitForNavigation(), | ||||
|       page.locator('button:has-text("OK")').click(), | ||||
|       page.waitForSelector('.c-message-banner__message') | ||||
|     ]); | ||||
|  | ||||
|         // has been deleted | ||||
|         expect(await restrictedNotebookTreeObject.count()).toEqual(0); | ||||
|     }); | ||||
|     // 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 nbUtils.enterTextEntry(page, TEST_TEXT); | ||||
|  | ||||
|         const commitButton = page.locator('button:has-text("Commit Entries")'); | ||||
|         expect(await commitButton.count()).toEqual(1); | ||||
|     }); | ||||
|   test('Can be locked if at least one page has one entry @addInit', async ({ page }) => { | ||||
|     await nbUtils.enterTextEntry(page, TEST_TEXT); | ||||
|  | ||||
|     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', () => { | ||||
|     let notebook; | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         notebook = await startAndAddRestrictedNotebookObject(page); | ||||
|         await nbUtils.enterTextEntry(page, TEST_TEXT); | ||||
|         await lockPage(page); | ||||
|   let notebook; | ||||
|   test.beforeEach(async ({ page }) => { | ||||
|     notebook = await startAndAddRestrictedNotebookObject(page); | ||||
|     await nbUtils.enterTextEntry(page, TEST_TEXT); | ||||
|     await lockPage(page); | ||||
|  | ||||
|         // open sidebar | ||||
|         await page.locator('button.c-notebook__toggle-nav-button').click(); | ||||
|     }); | ||||
|     // open sidebar | ||||
|     await page.locator('button.c-notebook__toggle-nav-button').click(); | ||||
|   }); | ||||
|  | ||||
|     test('Locked page should now be in a locked state @addInit @unstable', async ({ page }, testInfo) => { | ||||
|         // eslint-disable-next-line playwright/no-skipped-test | ||||
|         test.skip(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); | ||||
|   test('Locked page should now be in a locked state @addInit @unstable', async ({ | ||||
|     page | ||||
|   }, testInfo) => { | ||||
|     // eslint-disable-next-line playwright/no-skipped-test | ||||
|     test.skip(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); | ||||
|     // 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 openObjectTreeContextMenu(page, notebook.url); | ||||
|         const menuOptions = page.locator('.c-menu ul'); | ||||
|     // no way to remove a restricted notebook with a locked page | ||||
|     await openObjectTreeContextMenu(page, notebook.url); | ||||
|     const menuOptions = page.locator('.c-menu ul'); | ||||
|  | ||||
|         await expect(menuOptions).not.toContainText('Remove'); | ||||
|     }); | ||||
|     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); | ||||
|   test('Can still: add page, rename, add entry, delete unlocked pages @addInit', async ({ | ||||
|     page | ||||
|   }) => { | ||||
|     // Add a new page to the section | ||||
|     await page.getByRole('button', { name: 'Add Page' }).click(); | ||||
|     // Focus the new page by clicking it | ||||
|     await page.getByText('Unnamed Page').nth(1).click(); | ||||
|     // Rename the new page | ||||
|     await page.getByText('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); | ||||
|     // expect to be able to rename unlocked pages | ||||
|     const newPageElement = page.getByText(TEST_TEXT_NAME); | ||||
|     const newPageCount = await newPageElement.count(); | ||||
|     await newPageElement.press('Enter'); // exit contenteditable state | ||||
|     expect.soft(newPageCount).toEqual(1); | ||||
|  | ||||
|         // enter test text | ||||
|         await nbUtils.enterTextEntry(page, TEST_TEXT); | ||||
|     // enter test text | ||||
|     await nbUtils.enterTextEntry(page, TEST_TEXT); | ||||
|  | ||||
|         // expect new page to be lockable | ||||
|         const commitButton = page.locator('BUTTON:HAS-TEXT("COMMIT ENTRIES")'); | ||||
|         expect.soft(await commitButton.count()).toEqual(1); | ||||
|     // expect new page to be lockable | ||||
|     const commitButton = page.getByRole('button', { name: ' 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('button:has-text("OK")').click() | ||||
|         ]); | ||||
|     // Click the context menu button for the new page | ||||
|     await page.getByTitle('Open context menu').click(); | ||||
|     // Delete the page | ||||
|     await page.getByRole('listitem', { name: 'Delete Page' }).click(); | ||||
|     // Click OK button | ||||
|     await page.getByRole('button', { name: 'Ok' }).click(); | ||||
|  | ||||
|         // deleted page, should no longer exist | ||||
|         const deletedPageElement = page.locator(`text=${TEST_TEXT_NAME}`); | ||||
|         expect(await deletedPageElement.count()).toEqual(0); | ||||
|     }); | ||||
|     // deleted page, should no longer exist | ||||
|     const deletedPageElement = page.getByText(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 }) => { | ||||
|     const notebook = await startAndAddRestrictedNotebookObject(page); | ||||
|     await nbUtils.dragAndDropEmbed(page, notebook); | ||||
|   }); | ||||
|  | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         const notebook = await startAndAddRestrictedNotebookObject(page); | ||||
|         await nbUtils.dragAndDropEmbed(page, notebook); | ||||
|     }); | ||||
|   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-icon-button').click(); // embed popup menu | ||||
|  | ||||
|     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-icon-button').click(); // embed popup menu | ||||
|     const embedMenu = page.locator('body >> .c-menu'); | ||||
|     await expect(embedMenu).toContainText('Remove This Embed'); | ||||
|   }); | ||||
|  | ||||
|         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-icon-button').click(); // embed popup menu | ||||
|  | ||||
|         const embedMenu = page.locator('body >> .c-menu'); | ||||
|         await expect(embedMenu).not.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-icon-button').click(); // embed popup menu | ||||
|  | ||||
|     const embedMenu = page.locator('body >> .c-menu'); | ||||
|     await expect(embedMenu).not.toContainText('Remove This Embed'); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| test.describe('can export restricted notebook as text', () => { | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         await startAndAddRestrictedNotebookObject(page); | ||||
|     }); | ||||
|   test.beforeEach(async ({ page }) => { | ||||
|     await startAndAddRestrictedNotebookObject(page); | ||||
|   }); | ||||
|  | ||||
|     test('basic functionality ', async ({ page }) => { | ||||
|         await nbUtils.enterTextEntry(page, `Foo bar entry`); | ||||
|         // Click on 3 Dot Menu | ||||
|         await page.locator('button[title="More options"]').click(); | ||||
|         const downloadPromise = page.waitForEvent('download'); | ||||
|   test('basic functionality ', async ({ page }) => { | ||||
|     await nbUtils.enterTextEntry(page, `Foo bar entry`); | ||||
|     // Click on 3 Dot Menu | ||||
|     await page.locator('button[title="More options"]').click(); | ||||
|     const downloadPromise = page.waitForEvent('download'); | ||||
|  | ||||
|         await page.getByRole('menuitem', { name: /Export Notebook as Text/ }).click(); | ||||
|     await page.getByRole('menuitem', { name: /Export Notebook as Text/ }).click(); | ||||
|  | ||||
|         await page.getByRole('button', { name: 'Save' }).click(); | ||||
|         const download = await downloadPromise; | ||||
|         const readStream = await download.createReadStream(); | ||||
|         const exportedText = await streamToString(readStream); | ||||
|         expect(exportedText).toContain('Foo bar entry'); | ||||
|     await page.getByRole('button', { name: 'Save' }).click(); | ||||
|     const download = await downloadPromise; | ||||
|     const readStream = await download.createReadStream(); | ||||
|     const exportedText = await streamToString(readStream); | ||||
|     expect(exportedText).toContain('Foo bar entry'); | ||||
|   }); | ||||
|  | ||||
|     }); | ||||
|  | ||||
|     test.fixme('can export multiple notebook entries as text ', async ({ page }) => {}); | ||||
|     test.fixme('can export all notebook entry metdata', async ({ page }) => {}); | ||||
|     test.fixme('can export all notebook tags', async ({ page }) => {}); | ||||
|     test.fixme('can export all notebook snapshots', async ({ page }) => {}); | ||||
|   test.fixme('can export multiple notebook entries as text ', async ({ page }) => {}); | ||||
|   test.fixme('can export all notebook entry metdata', async ({ page }) => {}); | ||||
|   test.fixme('can export all notebook tags', async ({ page }) => {}); | ||||
|   test.fixme('can export all notebook snapshots', async ({ page }) => {}); | ||||
| }); | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function startAndAddRestrictedNotebookObject(page) { | ||||
|     // eslint-disable-next-line no-undef | ||||
|     await page.addInitScript({ path: path.join(__dirname, '../../../../helper/', 'addInitRestrictedNotebook.js') }); | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|   await page.addInitScript({ | ||||
|     path: path.join(__dirname, '../../../../helper/', 'addInitRestrictedNotebook.js') | ||||
|   }); | ||||
|   await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|  | ||||
|     return createDomainObjectWithDefaults(page, { type: CUSTOM_NAME }); | ||||
|   return createDomainObjectWithDefaults(page, { type: CUSTOM_NAME }); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function lockPage(page) { | ||||
|     const commitButton = page.locator('button:has-text("Commit Entries")'); | ||||
|     await commitButton.click(); | ||||
|   const commitButton = page.locator('button:has-text("Commit Entries")'); | ||||
|   await commitButton.click(); | ||||
|  | ||||
|     //Wait until Lock Banner is visible | ||||
|     await page.locator('text=Lock Page').click(); | ||||
|   //Wait until Lock Banner is visible | ||||
|   await page.locator('text=Lock Page').click(); | ||||
| } | ||||
|   | ||||
| @@ -29,243 +29,247 @@ const { createDomainObjectWithDefaults, selectInspectorTab } = require('../../.. | ||||
| const nbUtils = require('../../../../helper/notebookUtils'); | ||||
|  | ||||
| /** | ||||
|   * 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 | ||||
|   */ | ||||
|  * 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) { | ||||
|     const notebook = createDomainObjectWithDefaults(page, { type: 'Notebook' }); | ||||
|   const notebook = createDomainObjectWithDefaults(page, { type: 'Notebook' }); | ||||
|  | ||||
|     for (let iteration = 0; iteration < iterations; iteration++) { | ||||
|         await nbUtils.enterTextEntry(page, `Entry ${iteration}`); | ||||
|     } | ||||
|   for (let iteration = 0; iteration < iterations; iteration++) { | ||||
|     await nbUtils.enterTextEntry(page, `Entry ${iteration}`); | ||||
|   } | ||||
|  | ||||
|     return notebook; | ||||
|   return notebook; | ||||
| } | ||||
|  | ||||
| /** | ||||
|   * 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 | ||||
|   */ | ||||
|  * 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) { | ||||
|     const notebook = await createNotebookAndEntry(page, iterations); | ||||
|     await selectInspectorTab(page, 'Annotations'); | ||||
|   const notebook = await createNotebookAndEntry(page, iterations); | ||||
|   await selectInspectorTab(page, 'Annotations'); | ||||
|  | ||||
|     for (let iteration = 0; iteration < iterations; iteration++) { | ||||
|         // Hover and click "Add Tag" button | ||||
|         // Hover is needed here to "slow down" the actions while running in headless mode | ||||
|         await page.locator(`[aria-label="Notebook Entry"] >> nth = ${iteration}`).click(); | ||||
|         await page.hover(`button:has-text("Add Tag")`); | ||||
|         await page.locator(`button:has-text("Add Tag")`).click(); | ||||
|   for (let iteration = 0; iteration < iterations; iteration++) { | ||||
|     // Hover and click "Add Tag" button | ||||
|     // Hover is needed here to "slow down" the actions while running in headless mode | ||||
|     await page.locator(`[aria-label="Notebook Entry"] >> nth = ${iteration}`).click(); | ||||
|     await page.hover(`button:has-text("Add Tag")`); | ||||
|     await page.locator(`button:has-text("Add Tag")`).click(); | ||||
|  | ||||
|         // Click inside the tag search input | ||||
|         await page.locator('[placeholder="Type to select tag"]').click(); | ||||
|         // Select the "Driving" tag | ||||
|         await page.locator('[aria-label="Autocomplete Options"] >> text=Driving').click(); | ||||
|     // Click inside the tag search input | ||||
|     await page.locator('[placeholder="Type to select tag"]').click(); | ||||
|     // Select the "Driving" tag | ||||
|     await page.locator('[aria-label="Autocomplete Options"] >> text=Driving').click(); | ||||
|  | ||||
|         // Hover and click "Add Tag" button | ||||
|         // Hover is needed here to "slow down" the actions while running in headless mode | ||||
|         await page.hover(`button:has-text("Add Tag")`); | ||||
|         await page.locator(`button:has-text("Add Tag")`).click(); | ||||
|         // Click inside the tag search input | ||||
|         await page.locator('[placeholder="Type to select tag"]').click(); | ||||
|         // Select the "Science" tag | ||||
|         await page.locator('[aria-label="Autocomplete Options"] >> text=Science').click(); | ||||
|     } | ||||
|     // Hover and click "Add Tag" button | ||||
|     // Hover is needed here to "slow down" the actions while running in headless mode | ||||
|     await page.hover(`button:has-text("Add Tag")`); | ||||
|     await page.locator(`button:has-text("Add Tag")`).click(); | ||||
|     // Click inside the tag search input | ||||
|     await page.locator('[placeholder="Type to select tag"]').click(); | ||||
|     // Select the "Science" tag | ||||
|     await page.locator('[aria-label="Autocomplete Options"] >> text=Science').click(); | ||||
|   } | ||||
|  | ||||
|     return notebook; | ||||
|   return notebook; | ||||
| } | ||||
|  | ||||
| test.describe('Tagging in Notebooks @addInit', () => { | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         //Go to baseURL | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|     }); | ||||
|     test('Can load tags', async ({ page }) => { | ||||
|         await createNotebookAndEntry(page); | ||||
|   test.beforeEach(async ({ page }) => { | ||||
|     //Go to baseURL | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|   }); | ||||
|   test('Can load tags', async ({ page }) => { | ||||
|     await createNotebookAndEntry(page); | ||||
|  | ||||
|         await selectInspectorTab(page, 'Annotations'); | ||||
|     await selectInspectorTab(page, 'Annotations'); | ||||
|  | ||||
|         await page.locator('button:has-text("Add Tag")').click(); | ||||
|     await page.locator('button:has-text("Add Tag")').click(); | ||||
|  | ||||
|         await page.locator('[placeholder="Type to select tag"]').click(); | ||||
|     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="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"); | ||||
|     await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText('Science'); | ||||
|     await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText('Driving'); | ||||
|  | ||||
|         await page.locator('button:has-text("Add Tag")').click(); | ||||
|         await page.locator('[placeholder="Type to select tag"]').click(); | ||||
|     await page.locator('button:has-text("Add Tag")').click(); | ||||
|     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 add tags with blank entry', async ({ page }) => { | ||||
|         await createDomainObjectWithDefaults(page, { type: 'Notebook' }); | ||||
|         await selectInspectorTab(page, 'Annotations'); | ||||
|     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 add tags with blank entry', async ({ page }) => { | ||||
|     await createDomainObjectWithDefaults(page, { type: 'Notebook' }); | ||||
|     await selectInspectorTab(page, 'Annotations'); | ||||
|  | ||||
|         await nbUtils.enterTextEntry(page, ''); | ||||
|         await page.hover(`button:has-text("Add Tag")`); | ||||
|         await page.locator(`button:has-text("Add Tag")`).click(); | ||||
|     await nbUtils.enterTextEntry(page, ''); | ||||
|     await page.hover(`button:has-text("Add Tag")`); | ||||
|     await page.locator(`button:has-text("Add Tag")`).click(); | ||||
|  | ||||
|         // Click inside the tag search input | ||||
|         await page.locator('[placeholder="Type to select tag"]').click(); | ||||
|         // Select the "Driving" tag | ||||
|         await page.locator('[aria-label="Autocomplete Options"] >> text=Driving').click(); | ||||
|     // Click inside the tag search input | ||||
|     await page.locator('[placeholder="Type to select tag"]').click(); | ||||
|     // Select the "Driving" tag | ||||
|     await page.locator('[aria-label="Autocomplete Options"] >> text=Driving').click(); | ||||
|  | ||||
|         await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Driving"); | ||||
|     }); | ||||
|     test('Can cancel adding tags', async ({ page }) => { | ||||
|         await createNotebookAndEntry(page); | ||||
|     await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText('Driving'); | ||||
|   }); | ||||
|   test('Can cancel adding tags', async ({ page }) => { | ||||
|     await createNotebookAndEntry(page); | ||||
|  | ||||
|         await selectInspectorTab(page, 'Annotations'); | ||||
|     await selectInspectorTab(page, 'Annotations'); | ||||
|  | ||||
|         // Test canceling adding a tag after we click "Type to select tag" | ||||
|         await page.locator('button:has-text("Add Tag")').click(); | ||||
|     // Test canceling adding a tag after we click "Type to select tag" | ||||
|     await page.locator('button:has-text("Add Tag")').click(); | ||||
|  | ||||
|         await page.locator('[placeholder="Type to select tag"]').click(); | ||||
|     await page.locator('[placeholder="Type to select tag"]').click(); | ||||
|  | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); | ||||
|     await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); | ||||
|  | ||||
|         await expect(page.locator('button:has-text("Add Tag")')).toBeVisible(); | ||||
|     await expect(page.locator('button:has-text("Add Tag")')).toBeVisible(); | ||||
|  | ||||
|         // Test canceling adding a tag after we just click "Add Tag" | ||||
|         await page.locator('button:has-text("Add Tag")').click(); | ||||
|     // Test canceling adding a tag after we just click "Add Tag" | ||||
|     await page.locator('button:has-text("Add Tag")').click(); | ||||
|  | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); | ||||
|     await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); | ||||
|  | ||||
|         await expect(page.locator('button:has-text("Add Tag")')).toBeVisible(); | ||||
|     }); | ||||
|     test('Can search for tags and preview works properly', async ({ page }) => { | ||||
|         await createNotebookEntryAndTags(page); | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); | ||||
|         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"]')).not.toContainText("Driving"); | ||||
|     await expect(page.locator('button:has-text("Add Tag")')).toBeVisible(); | ||||
|   }); | ||||
|   test('Can search for tags and preview works properly', async ({ page }) => { | ||||
|     await createNotebookEntryAndTags(page); | ||||
|     await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); | ||||
|     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"]')).not.toContainText('Driving'); | ||||
|  | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); | ||||
|         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"]')).not.toContainText("Driving"); | ||||
|     await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); | ||||
|     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"]')).not.toContainText('Driving'); | ||||
|  | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Xq'); | ||||
|         await expect(page.locator('text=No results found')).toBeVisible(); | ||||
|     await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); | ||||
|     await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Xq'); | ||||
|     await expect(page.locator('text=No results found')).toBeVisible(); | ||||
|  | ||||
|         await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Display Layout' | ||||
|         }); | ||||
|  | ||||
|         // Go back into edit mode for the display layout | ||||
|         await page.locator('button[title="Edit"]').click(); | ||||
|  | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Sc'); | ||||
|         await expect(page.locator('[aria-label="Search Result"]')).toContainText("Science"); | ||||
|         await page.getByText('Entry 0').click(); | ||||
|         await expect(page.locator('.js-preview-window')).toBeVisible(); | ||||
|     await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Display Layout' | ||||
|     }); | ||||
|  | ||||
|     test('Can delete tags', async ({ page }) => { | ||||
|         await createNotebookEntryAndTags(page); | ||||
|         // Delete Driving | ||||
|         await page.hover('[aria-label="Tag"]:has-text("Driving")'); | ||||
|         await page.locator('[aria-label="Remove tag Driving"]').click(); | ||||
|     // Go back into edit mode for the display layout | ||||
|     await page.locator('button[title="Edit"]').click(); | ||||
|  | ||||
|         await expect(page.locator('[aria-label="Tags Inspector"]')).toContainText("Science"); | ||||
|         await expect(page.locator('[aria-label="Tags Inspector"]')).not.toContainText("Driving"); | ||||
|     await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); | ||||
|     await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Sc'); | ||||
|     await expect(page.locator('[aria-label="Search Result"]')).toContainText('Science'); | ||||
|     await page.getByText('Entry 0').click(); | ||||
|     await expect(page.locator('.js-preview-window')).toBeVisible(); | ||||
|   }); | ||||
|  | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc'); | ||||
|         await expect(page.locator('[aria-label="Search Result"]')).not.toContainText("Driving"); | ||||
|   test('Can delete tags', async ({ page }) => { | ||||
|     await createNotebookEntryAndTags(page); | ||||
|     // Delete Driving | ||||
|     await page.hover('[aria-label="Tag"]:has-text("Driving")'); | ||||
|     await page.locator('[aria-label="Remove tag Driving"]').click(); | ||||
|  | ||||
|     await expect(page.locator('[aria-label="Tags Inspector"]')).toContainText('Science'); | ||||
|     await expect(page.locator('[aria-label="Tags Inspector"]')).not.toContainText('Driving'); | ||||
|  | ||||
|     await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc'); | ||||
|     await expect(page.locator('[aria-label="Search Result"]')).not.toContainText('Driving'); | ||||
|   }); | ||||
|  | ||||
|   test('Can delete entries without tags', async ({ page }) => { | ||||
|     test.info().annotations.push({ | ||||
|       type: 'issue', | ||||
|       description: 'https://github.com/nasa/openmct/issues/5823' | ||||
|     }); | ||||
|  | ||||
|     test('Can delete entries without tags', async ({ page }) => { | ||||
|         test.info().annotations.push({ | ||||
|             type: 'issue', | ||||
|             description: 'https://github.com/nasa/openmct/issues/5823' | ||||
|         }); | ||||
|     await createNotebookEntryAndTags(page); | ||||
|  | ||||
|         await createNotebookEntryAndTags(page); | ||||
|     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 = 1`; | ||||
|     await page.locator(entryLocator).click(); | ||||
|     await page.locator(entryLocator).fill(`An entry without tags`); | ||||
|     await page.locator('[aria-label="Notebook Entry Input"] >> nth=1').press('Enter'); | ||||
|  | ||||
|         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 = 1`; | ||||
|         await page.locator(entryLocator).click(); | ||||
|         await page.locator(entryLocator).fill(`An entry without tags`); | ||||
|         await page.locator('[aria-label="Notebook Entry Input"] >> nth=1').press('Enter'); | ||||
|     await page.hover('[aria-label="Notebook Entry Input"] >> nth=1'); | ||||
|     await page.locator('button[title="Delete this entry"]').last().click(); | ||||
|     await expect( | ||||
|       page.locator('text=This action will permanently delete this entry. Do you wish to continue?') | ||||
|     ).toBeVisible(); | ||||
|     await page.locator('button:has-text("Ok")').click(); | ||||
|     await expect( | ||||
|       page.locator('text=This action will permanently delete this entry. Do you wish to continue?') | ||||
|     ).toBeHidden(); | ||||
|   }); | ||||
|  | ||||
|         await page.hover('[aria-label="Notebook Entry Input"] >> nth=1'); | ||||
|         await page.locator('button[title="Delete this entry"]').last().click(); | ||||
|         await expect(page.locator('text=This action will permanently delete this entry. Do you wish to continue?')).toBeVisible(); | ||||
|         await page.locator('button:has-text("Ok")').click(); | ||||
|         await expect(page.locator('text=This action will permanently delete this entry. Do you wish to continue?')).toBeHidden(); | ||||
|     }); | ||||
|   test('Can delete objects with tags and neither return in search', async ({ page }) => { | ||||
|     await createNotebookEntryAndTags(page); | ||||
|     // Delete Notebook | ||||
|     await page.locator('button[title="More options"]').click(); | ||||
|     await page.locator('li[title="Remove this object from its containing object."]').click(); | ||||
|     await page.locator('button:has-text("OK")').click(); | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|  | ||||
|     test('Can delete objects with tags and neither return in search', async ({ page }) => { | ||||
|         await createNotebookEntryAndTags(page); | ||||
|         // Delete Notebook | ||||
|         await page.locator('button[title="More options"]').click(); | ||||
|         await page.locator('li[title="Remove this object from its containing object."]').click(); | ||||
|         await page.locator('button:has-text("OK")').click(); | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|     await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Unnamed'); | ||||
|     await expect(page.locator('text=No results found')).toBeVisible(); | ||||
|     await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sci'); | ||||
|     await expect(page.locator('text=No results found')).toBeVisible(); | ||||
|     await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('dri'); | ||||
|     await expect(page.locator('text=No results found')).toBeVisible(); | ||||
|   }); | ||||
|   test('Tags persist across reload', async ({ page }) => { | ||||
|     //Go to baseURL | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|  | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Unnamed'); | ||||
|         await expect(page.locator('text=No results found')).toBeVisible(); | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sci'); | ||||
|         await expect(page.locator('text=No results found')).toBeVisible(); | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('dri'); | ||||
|         await expect(page.locator('text=No results found')).toBeVisible(); | ||||
|     }); | ||||
|     test('Tags persist across reload', async ({ page }) => { | ||||
|         //Go to baseURL | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|     const ITERATIONS = 4; | ||||
|     const notebook = await createNotebookEntryAndTags(page, ITERATIONS); | ||||
|     await page.goto(notebook.url); | ||||
|  | ||||
|         const ITERATIONS = 4; | ||||
|         const notebook = await createNotebookEntryAndTags(page, ITERATIONS); | ||||
|         await page.goto(notebook.url); | ||||
|     // Verify tags are present | ||||
|     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'); | ||||
|     } | ||||
|  | ||||
|         // Verify tags are present | ||||
|         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 page.reload({ waitUntil: 'domcontentloaded' }); | ||||
|  | ||||
|         //Reload Page | ||||
|         await page.reload({ waitUntil: 'domcontentloaded' }); | ||||
|     // Verify tags persist across reload | ||||
|     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'); | ||||
|     } | ||||
|   }); | ||||
|   test('Can cancel adding a tag', async ({ page }) => { | ||||
|     await createNotebookAndEntry(page); | ||||
|  | ||||
|         // Verify tags persist across reload | ||||
|         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"); | ||||
|         } | ||||
|     }); | ||||
|     test('Can cancel adding a tag', async ({ page }) => { | ||||
|         await createNotebookAndEntry(page); | ||||
|     await selectInspectorTab(page, 'Annotations'); | ||||
|  | ||||
|         await selectInspectorTab(page, 'Annotations'); | ||||
|     // Click on the "Add Tag" button | ||||
|     await page.locator('button:has-text("Add Tag")').click(); | ||||
|  | ||||
|         // Click on the "Add Tag" button | ||||
|         await page.locator('button:has-text("Add Tag")').click(); | ||||
|     // Click inside the AutoComplete field | ||||
|     await page.locator('[placeholder="Type to select tag"]').click(); | ||||
|  | ||||
|         // Click inside the AutoComplete field | ||||
|         await page.locator('[placeholder="Type to select tag"]').click(); | ||||
|     // Click on the "Tags" header (simulating a click outside the autocomplete) | ||||
|     await page.locator('div.c-inspect-properties__header:has-text("Tags")').click(); | ||||
|  | ||||
|         // Click on the "Tags" header (simulating a click outside the autocomplete) | ||||
|         await page.locator('div.c-inspect-properties__header:has-text("Tags")').click(); | ||||
|     // Verify there is a button with text "Add Tag" | ||||
|     await expect(page.locator('button:has-text("Add Tag")')).toBeVisible(); | ||||
|  | ||||
|         // Verify there is a button with text "Add Tag" | ||||
|         await expect(page.locator('button:has-text("Add Tag")')).toBeVisible(); | ||||
|  | ||||
|         // Verify the AutoComplete field is hidden | ||||
|         await expect(page.locator('[placeholder="Type to select tag"]')).toBeHidden(); | ||||
|     }); | ||||
|     // Verify the AutoComplete field is hidden | ||||
|     await expect(page.locator('[placeholder="Type to select tag"]')).toBeHidden(); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -19,10 +19,10 @@ | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* global __dirname */ | ||||
| /* | ||||
| * This test suite is dedicated to testing the operator status plugin. | ||||
| */ | ||||
|  * This test suite is dedicated to testing the operator status plugin. | ||||
|  */ | ||||
|  | ||||
| const path = require('path'); | ||||
| const { test, expect } = require('../../../../pluginFixtures'); | ||||
| @@ -38,119 +38,120 @@ STUB (test.fixme) Rolling through each | ||||
| */ | ||||
|  | ||||
| test.describe('Operator Status', () => { | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         // FIXME: determine if plugins will be added to index.html or need to be injected | ||||
|         // eslint-disable-next-line no-undef | ||||
|         await page.addInitScript({ path: path.join(__dirname, '../../../../helper/', 'addInitExampleUser.js')}); | ||||
|         // eslint-disable-next-line no-undef | ||||
|         await page.addInitScript({ path: path.join(__dirname, '../../../../helper/', 'addInitOperatorStatus.js')}); | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|   test.beforeEach(async ({ page }) => { | ||||
|     // FIXME: determine if plugins will be added to index.html or need to be injected | ||||
|     await page.addInitScript({ | ||||
|       path: path.join(__dirname, '../../../../helper/', 'addInitExampleUser.js') | ||||
|     }); | ||||
|  | ||||
|     // verify that operator status is visible | ||||
|     test('operator status is visible and expands when clicked', async ({ page }) => { | ||||
|         await expect(page.locator('div[title="Set my operator status"]')).toBeVisible(); | ||||
|         await page.locator('div[title="Set my operator status"]').click(); | ||||
|  | ||||
|         // expect default status to be 'GO' | ||||
|         await expect(page.locator('.c-status-poll-panel')).toBeVisible(); | ||||
|     await page.addInitScript({ | ||||
|       path: path.join(__dirname, '../../../../helper/', 'addInitOperatorStatus.js') | ||||
|     }); | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|   }); | ||||
|  | ||||
|     test('poll question indicator remains when blank poll set', async ({ page }) => { | ||||
|         await expect(page.locator('div[title="Set the current poll question"]')).toBeVisible(); | ||||
|         await page.locator('div[title="Set the current poll question"]').click(); | ||||
|         // set to blank | ||||
|         await page.getByRole('button', { name: 'Update' }).click(); | ||||
|   // verify that operator status is visible | ||||
|   test('operator status is visible and expands when clicked', async ({ page }) => { | ||||
|     await expect(page.locator('div[title="Set my operator status"]')).toBeVisible(); | ||||
|     await page.locator('div[title="Set my operator status"]').click(); | ||||
|  | ||||
|         // should still be visible | ||||
|         await expect(page.locator('div[title="Set the current poll question"]')).toBeVisible(); | ||||
|     }); | ||||
|     // expect default status to be 'GO' | ||||
|     await expect(page.locator('.c-status-poll-panel')).toBeVisible(); | ||||
|   }); | ||||
|  | ||||
|     // Verify that user 1 sees updates from user/role 2 (Not possible without openmct-yamcs implementation) | ||||
|     test('operator status table reflects answered values', async ({ page }) => { | ||||
|         // user navigates to operator status poll | ||||
|         const statusPollIndicator = page.locator('div[title="Set my operator status"]'); | ||||
|         await statusPollIndicator.click(); | ||||
|   test('poll question indicator remains when blank poll set', async ({ page }) => { | ||||
|     await expect(page.locator('div[title="Set the current poll question"]')).toBeVisible(); | ||||
|     await page.locator('div[title="Set the current poll question"]').click(); | ||||
|     // set to blank | ||||
|     await page.getByRole('button', { name: 'Update' }).click(); | ||||
|  | ||||
|         // get user role value | ||||
|         const userRole = page.locator('.c-status-poll-panel__user-role'); | ||||
|         const userRoleText = await userRole.innerText(); | ||||
|     // should still be visible | ||||
|     await expect(page.locator('div[title="Set the current poll question"]')).toBeVisible(); | ||||
|   }); | ||||
|  | ||||
|         // get selected status value | ||||
|         const selectStatus = page.locator('select[name="setStatus"]'); | ||||
|         await selectStatus.selectOption({ index: 1}); | ||||
|         const initialStatusValue = await selectStatus.inputValue(); | ||||
|   // Verify that user 1 sees updates from user/role 2 (Not possible without openmct-yamcs implementation) | ||||
|   test('operator status table reflects answered values', async ({ page }) => { | ||||
|     // user navigates to operator status poll | ||||
|     const statusPollIndicator = page.locator('div[title="Set my operator status"]'); | ||||
|     await statusPollIndicator.click(); | ||||
|  | ||||
|         // open manage status poll | ||||
|         const manageStatusPollIndicator = page.locator('div[title="Set the current poll question"]'); | ||||
|         await manageStatusPollIndicator.click(); | ||||
|         // parse the table row values | ||||
|         const row = page.locator(`tr:has-text("${userRoleText}")`); | ||||
|         const rowValues = await row.innerText(); | ||||
|         const rowValuesArr = rowValues.split('\t'); | ||||
|         const COLUMN_STATUS_INDEX = 1; | ||||
|         // check initial set value matches status table | ||||
|         expect(rowValuesArr[COLUMN_STATUS_INDEX].toLowerCase()) | ||||
|             .toEqual(initialStatusValue.toLowerCase()); | ||||
|     // get user role value | ||||
|     const userRole = page.locator('.c-status-poll-panel__user-role'); | ||||
|     const userRoleText = await userRole.innerText(); | ||||
|  | ||||
|         // change user status | ||||
|         await statusPollIndicator.click(); | ||||
|         // FIXME: might want to grab a dynamic option instead of arbitrary | ||||
|         await page.locator('select[name="setStatus"]').selectOption({ index: 2}); | ||||
|         const updatedStatusValue = await selectStatus.inputValue(); | ||||
|         // verify user status is reflected in table | ||||
|         await manageStatusPollIndicator.click(); | ||||
|     // get selected status value | ||||
|     const selectStatus = page.locator('select[name="setStatus"]'); | ||||
|     await selectStatus.selectOption({ index: 1 }); | ||||
|     const initialStatusValue = await selectStatus.inputValue(); | ||||
|  | ||||
|         const updatedRow = page.locator(`tr:has-text("${userRoleText}")`); | ||||
|         const updatedRowValues = await updatedRow.innerText(); | ||||
|         const updatedRowValuesArr = updatedRowValues.split('\t'); | ||||
|     // open manage status poll | ||||
|     const manageStatusPollIndicator = page.locator('div[title="Set the current poll question"]'); | ||||
|     await manageStatusPollIndicator.click(); | ||||
|     // parse the table row values | ||||
|     const row = page.locator(`tr:has-text("${userRoleText}")`); | ||||
|     const rowValues = await row.innerText(); | ||||
|     const rowValuesArr = rowValues.split('\t'); | ||||
|     const COLUMN_STATUS_INDEX = 1; | ||||
|     // check initial set value matches status table | ||||
|     expect(rowValuesArr[COLUMN_STATUS_INDEX].toLowerCase()).toEqual( | ||||
|       initialStatusValue.toLowerCase() | ||||
|     ); | ||||
|  | ||||
|         expect(updatedRowValuesArr[COLUMN_STATUS_INDEX].toLowerCase()) | ||||
|             .toEqual(updatedStatusValue.toLowerCase()); | ||||
|     // change user status | ||||
|     await statusPollIndicator.click(); | ||||
|     // FIXME: might want to grab a dynamic option instead of arbitrary | ||||
|     await page.locator('select[name="setStatus"]').selectOption({ index: 2 }); | ||||
|     const updatedStatusValue = await selectStatus.inputValue(); | ||||
|     // verify user status is reflected in table | ||||
|     await manageStatusPollIndicator.click(); | ||||
|  | ||||
|     }); | ||||
|     const updatedRow = page.locator(`tr:has-text("${userRoleText}")`); | ||||
|     const updatedRowValues = await updatedRow.innerText(); | ||||
|     const updatedRowValuesArr = updatedRowValues.split('\t'); | ||||
|  | ||||
|     test('clear poll button removes poll responses', async ({ page }) => { | ||||
|         // user navigates to operator status poll | ||||
|         const statusPollIndicator = page.locator('div[title="Set my operator status"]'); | ||||
|         await statusPollIndicator.click(); | ||||
|     expect(updatedRowValuesArr[COLUMN_STATUS_INDEX].toLowerCase()).toEqual( | ||||
|       updatedStatusValue.toLowerCase() | ||||
|     ); | ||||
|   }); | ||||
|  | ||||
|         // get user role value | ||||
|         const userRole = page.locator('.c-status-poll-panel__user-role'); | ||||
|         const userRoleText = await userRole.innerText(); | ||||
|   test('clear poll button removes poll responses', async ({ page }) => { | ||||
|     // user navigates to operator status poll | ||||
|     const statusPollIndicator = page.locator('div[title="Set my operator status"]'); | ||||
|     await statusPollIndicator.click(); | ||||
|  | ||||
|         // get selected status value | ||||
|         const selectStatus = page.locator('select[name="setStatus"]'); | ||||
|         // FIXME: might want to grab a dynamic option instead of arbitrary | ||||
|         await selectStatus.selectOption({ index: 1}); | ||||
|         const initialStatusValue = await selectStatus.inputValue(); | ||||
|     // get user role value | ||||
|     const userRole = page.locator('.c-status-poll-panel__user-role'); | ||||
|     const userRoleText = await userRole.innerText(); | ||||
|  | ||||
|         // open manage status poll | ||||
|         const manageStatusPollIndicator = page.locator('div[title="Set the current poll question"]'); | ||||
|         await manageStatusPollIndicator.click(); | ||||
|         // parse the table row values | ||||
|         const row = page.locator(`tr:has-text("${userRoleText}")`); | ||||
|         const rowValues = await row.innerText(); | ||||
|         const rowValuesArr = rowValues.split('\t'); | ||||
|         const COLUMN_STATUS_INDEX = 1; | ||||
|         // check initial set value matches status table | ||||
|         expect(rowValuesArr[COLUMN_STATUS_INDEX].toLowerCase()) | ||||
|             .toEqual(initialStatusValue.toLowerCase()); | ||||
|     // get selected status value | ||||
|     const selectStatus = page.locator('select[name="setStatus"]'); | ||||
|     // FIXME: might want to grab a dynamic option instead of arbitrary | ||||
|     await selectStatus.selectOption({ index: 1 }); | ||||
|     const initialStatusValue = await selectStatus.inputValue(); | ||||
|  | ||||
|         // clear the poll | ||||
|         await page.locator('button[title="Clear the previous poll question"]').click(); | ||||
|     // open manage status poll | ||||
|     const manageStatusPollIndicator = page.locator('div[title="Set the current poll question"]'); | ||||
|     await manageStatusPollIndicator.click(); | ||||
|     // parse the table row values | ||||
|     const row = page.locator(`tr:has-text("${userRoleText}")`); | ||||
|     const rowValues = await row.innerText(); | ||||
|     const rowValuesArr = rowValues.split('\t'); | ||||
|     const COLUMN_STATUS_INDEX = 1; | ||||
|     // check initial set value matches status table | ||||
|     expect(rowValuesArr[COLUMN_STATUS_INDEX].toLowerCase()).toEqual( | ||||
|       initialStatusValue.toLowerCase() | ||||
|     ); | ||||
|  | ||||
|         const updatedRow = page.locator(`tr:has-text("${userRoleText}")`); | ||||
|         const updatedRowValues = await updatedRow.innerText(); | ||||
|         const updatedRowValuesArr = updatedRowValues.split('\t'); | ||||
|         const UNSET_VALUE_LABEL = 'Not set'; | ||||
|         expect(updatedRowValuesArr[COLUMN_STATUS_INDEX]) | ||||
|             .toEqual(UNSET_VALUE_LABEL); | ||||
|     // clear the poll | ||||
|     await page.locator('button[title="Clear the previous poll question"]').click(); | ||||
|  | ||||
|     }); | ||||
|  | ||||
|     test.fixme('iterate through all possible response values', async ({ page }) => { | ||||
|         // test all possible respone values for the poll | ||||
|     }); | ||||
|     const updatedRow = page.locator(`tr:has-text("${userRoleText}")`); | ||||
|     const updatedRowValues = await updatedRow.innerText(); | ||||
|     const updatedRowValuesArr = updatedRowValues.split('\t'); | ||||
|     const UNSET_VALUE_LABEL = 'Not set'; | ||||
|     expect(updatedRowValuesArr[COLUMN_STATUS_INDEX]).toEqual(UNSET_VALUE_LABEL); | ||||
|   }); | ||||
|  | ||||
|   test.fixme('iterate through all possible response values', async ({ page }) => { | ||||
|     // test all possible respone values for the poll | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -27,81 +27,95 @@ Testsuite for plot autoscale. | ||||
| const { selectInspectorTab } = require('../../../../appActions'); | ||||
| const { test, expect } = require('../../../../pluginFixtures'); | ||||
| test.use({ | ||||
|     viewport: { | ||||
|         width: 1280, | ||||
|         height: 720 | ||||
|     } | ||||
|   viewport: { | ||||
|     width: 1280, | ||||
|     height: 720 | ||||
|   } | ||||
| }); | ||||
|  | ||||
| test.describe('Autoscale', () => { | ||||
|     test('User can set autoscale with a valid range @snapshot', async ({ page, openmctConfig }) => { | ||||
|         const { myItemsFolderName } = openmctConfig; | ||||
|   test('User can set autoscale with a valid range @snapshot', async ({ page, openmctConfig }) => { | ||||
|     const { myItemsFolderName } = openmctConfig; | ||||
|  | ||||
|         //This is necessary due to the size of the test suite. | ||||
|         test.slow(); | ||||
|     //This is necessary due to the size of the test suite. | ||||
|     test.slow(); | ||||
|  | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|  | ||||
|         await setTimeRange(page); | ||||
|     await setTimeRange(page); | ||||
|  | ||||
|         await createSinewaveOverlayPlot(page, myItemsFolderName); | ||||
|     await createSinewaveOverlayPlot(page, myItemsFolderName); | ||||
|  | ||||
|         await testYTicks(page, ['-1.00', '-0.50', '0.00', '0.50', '1.00']); | ||||
|     await testYTicks(page, ['-1.00', '-0.50', '0.00', '0.50', '1.00']); | ||||
|  | ||||
|         // enter edit mode | ||||
|         await page.click('button[title="Edit"]'); | ||||
|     // enter edit mode | ||||
|     await page.click('button[title="Edit"]'); | ||||
|  | ||||
|         await selectInspectorTab(page, 'Config'); | ||||
|         await turnOffAutoscale(page); | ||||
|     await selectInspectorTab(page, 'Config'); | ||||
|     await turnOffAutoscale(page); | ||||
|  | ||||
|         await setUserDefinedMinAndMax(page, '-2', '2'); | ||||
|     await setUserDefinedMinAndMax(page, '-2', '2'); | ||||
|  | ||||
|         // save | ||||
|         await page.click('button[title="Save"]'); | ||||
|         await Promise.all([ | ||||
|             page.locator('li[title = "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'}); | ||||
|     // save | ||||
|     await page.click('button[title="Save"]'); | ||||
|     await Promise.all([ | ||||
|       page.locator('li[title = "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' }); | ||||
|  | ||||
|         // Make sure that after turning off autoscale, the user entered range values are reflexted in the ticks. | ||||
|         await testYTicks(page, ['-2.00', '-1.50', '-1.00', '-0.50', '0.00', '0.50', '1.00', '1.50', '2.00']); | ||||
|     // Make sure that after turning off autoscale, the user entered range values are reflexted in the ticks. | ||||
|     await testYTicks(page, [ | ||||
|       '-2.00', | ||||
|       '-1.50', | ||||
|       '-1.00', | ||||
|       '-0.50', | ||||
|       '0.00', | ||||
|       '0.50', | ||||
|       '1.00', | ||||
|       '1.50', | ||||
|       '2.00' | ||||
|     ]); | ||||
|  | ||||
|         const canvas = page.locator('canvas').nth(1); | ||||
|     const canvas = page.locator('canvas').nth(1); | ||||
|  | ||||
|         await canvas.hover({trial: true}); | ||||
|         await expect(page.locator('.js-series-data-loaded')).toBeVisible(); | ||||
|     await canvas.hover({ trial: true }); | ||||
|     await expect(page.locator('.js-series-data-loaded')).toBeVisible(); | ||||
|  | ||||
|         expect.soft(await canvas.screenshot()).toMatchSnapshot('autoscale-canvas-prepan.png', { animations: 'disabled' }); | ||||
|     expect | ||||
|       .soft(await canvas.screenshot()) | ||||
|       .toMatchSnapshot('autoscale-canvas-prepan.png', { animations: 'disabled' }); | ||||
|  | ||||
|         //Alt Drag Start | ||||
|         await page.keyboard.down('Alt'); | ||||
|     //Alt Drag Start | ||||
|     await page.keyboard.down('Alt'); | ||||
|  | ||||
|         await canvas.dragTo(canvas, { | ||||
|             sourcePosition: { | ||||
|                 x: 200, | ||||
|                 y: 200 | ||||
|             }, | ||||
|             targetPosition: { | ||||
|                 x: 400, | ||||
|                 y: 400 | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         //Alt Drag End | ||||
|         await page.keyboard.up('Alt'); | ||||
|  | ||||
|         // Ensure the drag worked. | ||||
|         await testYTicks(page, ['0.00', '0.50', '1.00', '1.50', '2.00', '2.50', '3.00', '3.50']); | ||||
|  | ||||
|         //Wait for canvas to stablize. | ||||
|         await canvas.hover({trial: true}); | ||||
|  | ||||
|         expect.soft(await canvas.screenshot()).toMatchSnapshot('autoscale-canvas-panned.png', { animations: 'disabled' }); | ||||
|     await canvas.dragTo(canvas, { | ||||
|       sourcePosition: { | ||||
|         x: 200, | ||||
|         y: 200 | ||||
|       }, | ||||
|       targetPosition: { | ||||
|         x: 400, | ||||
|         y: 400 | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     //Alt Drag End | ||||
|     await page.keyboard.up('Alt'); | ||||
|  | ||||
|     // Ensure the drag worked. | ||||
|     await testYTicks(page, ['0.00', '0.50', '1.00', '1.50', '2.00', '2.50', '3.00', '3.50']); | ||||
|  | ||||
|     //Wait for canvas to stablize. | ||||
|     await canvas.hover({ trial: true }); | ||||
|  | ||||
|     expect | ||||
|       .soft(await canvas.screenshot()) | ||||
|       .toMatchSnapshot('autoscale-canvas-panned.png', { animations: 'disabled' }); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| /** | ||||
| @@ -109,16 +123,20 @@ test.describe('Autoscale', () => { | ||||
|  * @param {string} start | ||||
|  * @param {string} end | ||||
|  */ | ||||
| async function setTimeRange(page, start = '2022-03-29 22:00:00.000Z', end = '2022-03-29 22:00:30.000Z') { | ||||
|     // Set a specific time range for consistency, otherwise it will change | ||||
|     // on every test to a range based on the current time. | ||||
| async function setTimeRange( | ||||
|   page, | ||||
|   start = '2022-03-29 22:00:00.000Z', | ||||
|   end = '2022-03-29 22:00:30.000Z' | ||||
| ) { | ||||
|   // Set a specific time range for consistency, otherwise it will change | ||||
|   // on every test to a range based on the current time. | ||||
|  | ||||
|     const timeInputs = page.locator('input.c-input--datetime'); | ||||
|     await timeInputs.first().click(); | ||||
|     await timeInputs.first().fill(start); | ||||
|   const timeInputs = page.locator('input.c-input--datetime'); | ||||
|   await timeInputs.first().click(); | ||||
|   await timeInputs.first().fill(start); | ||||
|  | ||||
|     await timeInputs.nth(1).click(); | ||||
|     await timeInputs.nth(1).fill(end); | ||||
|   await timeInputs.nth(1).click(); | ||||
|   await timeInputs.nth(1).fill(end); | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -126,54 +144,57 @@ async function setTimeRange(page, start = '2022-03-29 22:00:00.000Z', end = '202 | ||||
|  * @param {string} myItemsFolderName | ||||
|  */ | ||||
| async function createSinewaveOverlayPlot(page, myItemsFolderName) { | ||||
|     // click create button | ||||
|     await page.locator('button:has-text("Create")').click(); | ||||
|   // click create button | ||||
|   await page.locator('button:has-text("Create")').click(); | ||||
|  | ||||
|     // add overlay plot with defaults | ||||
|     await page.locator('li[role="menuitem"]:has-text("Overlay Plot")').click(); | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('button:has-text("OK")').click(), | ||||
|         //Wait for Save Banner to appear1 | ||||
|         page.waitForSelector('.c-message-banner__message') | ||||
|     ]); | ||||
|     //Wait until Save Banner is gone | ||||
|     await page.locator('.c-message-banner__close-button').click(); | ||||
|     await page.waitForSelector('.c-message-banner__message', { state: 'detached'}); | ||||
|   // add overlay plot with defaults | ||||
|   await page.locator('li[role="menuitem"]:has-text("Overlay Plot")').click(); | ||||
|   await Promise.all([ | ||||
|     page.waitForNavigation(), | ||||
|     page.locator('button:has-text("OK")').click(), | ||||
|     //Wait for Save Banner to appear1 | ||||
|     page.waitForSelector('.c-message-banner__message') | ||||
|   ]); | ||||
|   //Wait until Save Banner is gone | ||||
|   await page.locator('.c-message-banner__close-button').click(); | ||||
|   await page.waitForSelector('.c-message-banner__message', { state: 'detached' }); | ||||
|  | ||||
|     // save (exit edit mode) | ||||
|     await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click(); | ||||
|     await page.locator('text=Save and Finish Editing').click(); | ||||
|   // 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(); | ||||
|   // click create button | ||||
|   await page.locator('button:has-text("Create")').click(); | ||||
|  | ||||
|     // add sine wave generator with defaults | ||||
|     await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click(); | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('button:has-text("OK")').click(), | ||||
|         //Wait for Save Banner to appear1 | ||||
|         page.waitForSelector('.c-message-banner__message') | ||||
|     ]); | ||||
|     //Wait until Save Banner is gone | ||||
|     await page.locator('.c-message-banner__close-button').click(); | ||||
|     await page.waitForSelector('.c-message-banner__message', { state: 'detached'}); | ||||
|   // add sine wave generator with defaults | ||||
|   await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click(); | ||||
|   await Promise.all([ | ||||
|     page.waitForNavigation(), | ||||
|     page.locator('button:has-text("OK")').click(), | ||||
|     //Wait for Save Banner to appear1 | ||||
|     page.waitForSelector('.c-message-banner__message') | ||||
|   ]); | ||||
|   //Wait until Save Banner is gone | ||||
|   await page.locator('.c-message-banner__close-button').click(); | ||||
|   await page.waitForSelector('.c-message-banner__message', { state: 'detached' }); | ||||
|  | ||||
|     // focus the overlay plot | ||||
|     await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click(); | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('text=Unnamed Overlay Plot').first().click() | ||||
|     ]); | ||||
|   // focus the overlay plot | ||||
|   await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click(); | ||||
|   await Promise.all([ | ||||
|     page.waitForNavigation(), | ||||
|     page.locator('text=Unnamed Overlay Plot').first().click() | ||||
|   ]); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function turnOffAutoscale(page) { | ||||
|     // uncheck autoscale | ||||
|     await page.getByRole('checkbox', { name: 'Auto scale' }).uncheck(); | ||||
|   // uncheck autoscale | ||||
|   await page.getByRole('checkbox', { name: 'Auto scale' }).uncheck(); | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -182,23 +203,23 @@ async function turnOffAutoscale(page) { | ||||
|  * @param {string} max | ||||
|  */ | ||||
| async function setUserDefinedMinAndMax(page, min, max) { | ||||
|     // set minimum value | ||||
|     await page.getByRole('spinbutton').first().fill(min); | ||||
|     // set maximum value | ||||
|     await page.getByRole('spinbutton').nth(1).fill(max); | ||||
|   // set minimum value | ||||
|   await page.getByRole('spinbutton').first().fill(min); | ||||
|   // set maximum value | ||||
|   await page.getByRole('spinbutton').nth(1).fill(max); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function testYTicks(page, values) { | ||||
|     const yTicks = page.locator('.gl-plot-y-tick-label'); | ||||
|     await page.locator('canvas >> nth=1').hover(); | ||||
|     let promises = [yTicks.count().then(c => expect(c).toBe(values.length))]; | ||||
|   const yTicks = page.locator('.gl-plot-y-tick-label'); | ||||
|   await page.locator('canvas >> nth=1').hover(); | ||||
|   let promises = [yTicks.count().then((c) => expect(c).toBe(values.length))]; | ||||
|  | ||||
|     for (let i = 0, l = values.length; i < l; i += 1) { | ||||
|         promises.push(expect.soft(yTicks.nth(i)).toHaveText(values[i])); // eslint-disable-line | ||||
|     } | ||||
|   for (let i = 0, l = values.length; i < l; i += 1) { | ||||
|     promises.push(expect.soft(yTicks.nth(i)).toHaveText(values[i])); // eslint-disable-line | ||||
|   } | ||||
|  | ||||
|     await Promise.all(promises); | ||||
|   await Promise.all(promises); | ||||
| } | ||||
|   | ||||
| @@ -29,44 +29,50 @@ const { test, expect } = require('../../../../pluginFixtures'); | ||||
| const { selectInspectorTab } = require('../../../../appActions'); | ||||
|  | ||||
| test.describe('Log plot tests', () => { | ||||
|     test('Log Plot ticks are functionally correct in regular and log mode and after refresh', async ({ page, openmctConfig }) => { | ||||
|         const { myItemsFolderName } = openmctConfig; | ||||
|   test('Log Plot ticks are functionally correct in regular and log mode and after refresh', async ({ | ||||
|     page, | ||||
|     openmctConfig | ||||
|   }) => { | ||||
|     const { myItemsFolderName } = openmctConfig; | ||||
|  | ||||
|         //Test.slow decorator is currently broken. Needs to be fixed in https://github.com/nasa/openmct/issues/5374 | ||||
|         test.slow(); | ||||
|     //Test.slow decorator is currently broken. Needs to be fixed in https://github.com/nasa/openmct/issues/5374 | ||||
|     test.slow(); | ||||
|  | ||||
|         await makeOverlayPlot(page, myItemsFolderName); | ||||
|         await testRegularTicks(page); | ||||
|         await enableEditMode(page); | ||||
|         await selectInspectorTab(page, 'Config'); | ||||
|         await enableLogMode(page); | ||||
|         await testLogTicks(page); | ||||
|         await disableLogMode(page); | ||||
|         await testRegularTicks(page); | ||||
|         await enableLogMode(page); | ||||
|         await testLogTicks(page); | ||||
|         await saveOverlayPlot(page); | ||||
|         await testLogTicks(page); | ||||
|     }); | ||||
|     await makeOverlayPlot(page, myItemsFolderName); | ||||
|     await testRegularTicks(page); | ||||
|     await enableEditMode(page); | ||||
|     await selectInspectorTab(page, 'Config'); | ||||
|     await enableLogMode(page); | ||||
|     await testLogTicks(page); | ||||
|     await disableLogMode(page); | ||||
|     await testRegularTicks(page); | ||||
|     await enableLogMode(page); | ||||
|     await testLogTicks(page); | ||||
|     await saveOverlayPlot(page); | ||||
|     await testLogTicks(page); | ||||
|   }); | ||||
|  | ||||
|     // Leaving test as 'TODO' for now. | ||||
|     // NOTE: Not eligible for community contributions. | ||||
|     test.fixme('Verify that log mode option is reflected in import/export JSON', async ({ page, openmctConfig }) => { | ||||
|         const { myItemsFolderName } = openmctConfig; | ||||
|   // Leaving test as 'TODO' for now. | ||||
|   // NOTE: Not eligible for community contributions. | ||||
|   test.fixme( | ||||
|     'Verify that log mode option is reflected in import/export JSON', | ||||
|     async ({ page, openmctConfig }) => { | ||||
|       const { myItemsFolderName } = openmctConfig; | ||||
|  | ||||
|         await makeOverlayPlot(page, myItemsFolderName); | ||||
|         await enableEditMode(page); | ||||
|         await enableLogMode(page); | ||||
|         await saveOverlayPlot(page); | ||||
|       await makeOverlayPlot(page, myItemsFolderName); | ||||
|       await enableEditMode(page); | ||||
|       await enableLogMode(page); | ||||
|       await saveOverlayPlot(page); | ||||
|  | ||||
|         // TODO ...export, delete the overlay, then import it... | ||||
|       // TODO ...export, delete the overlay, then import it... | ||||
|  | ||||
|         //await testLogTicks(page); | ||||
|       //await testLogTicks(page); | ||||
|  | ||||
|         // TODO, the plot is slightly at different position that in the other test, so this fails. | ||||
|         // ...We can fix it by copying all steps from the first test... | ||||
|         // await testLogPlotPixels(page); | ||||
|     }); | ||||
|       // TODO, the plot is slightly at different position that in the other test, so this fails. | ||||
|       // ...We can fix it by copying all steps from the first test... | ||||
|       // await testLogPlotPixels(page); | ||||
|     } | ||||
|   ); | ||||
| }); | ||||
|  | ||||
| /** | ||||
| @@ -75,146 +81,149 @@ test.describe('Log plot tests', () => { | ||||
|  * @param {string} myItemsFolderName | ||||
|  */ | ||||
| async function makeOverlayPlot(page, myItemsFolderName) { | ||||
|     // 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: 'domcontentloaded' }); | ||||
|   // 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: 'domcontentloaded' }); | ||||
|  | ||||
|     // Set a specific time range for consistency, otherwise it will change | ||||
|     // on every test to a range based on the current time. | ||||
|   // Set a specific time range for consistency, otherwise it will change | ||||
|   // on every test to a range based on the current time. | ||||
|  | ||||
|     const timeInputs = page.locator('input.c-input--datetime'); | ||||
|     await timeInputs.first().click(); | ||||
|     await timeInputs.first().fill('2022-03-29 22:00:00.000Z'); | ||||
|   const timeInputs = page.locator('input.c-input--datetime'); | ||||
|   await timeInputs.first().click(); | ||||
|   await timeInputs.first().fill('2022-03-29 22:00:00.000Z'); | ||||
|  | ||||
|     await timeInputs.nth(1).click(); | ||||
|     await timeInputs.nth(1).fill('2022-03-29 22:00:30.000Z'); | ||||
|   await timeInputs.nth(1).click(); | ||||
|   await timeInputs.nth(1).fill('2022-03-29 22:00:30.000Z'); | ||||
|  | ||||
|     // create overlay plot | ||||
|   // create overlay plot | ||||
|  | ||||
|     await page.locator('button.c-create-button').click(); | ||||
|     await page.locator('li[role="menuitem"]:has-text("Overlay Plot")').click(); | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation({ waitUntil: 'networkidle'}), | ||||
|         page.locator('button:has-text("OK")').click(), | ||||
|         //Wait for Save Banner to appear | ||||
|         page.waitForSelector('.c-message-banner__message') | ||||
|     ]); | ||||
|     //Wait until Save Banner is gone | ||||
|     await page.locator('.c-message-banner__close-button').click(); | ||||
|     await page.waitForSelector('.c-message-banner__message', { state: 'detached'}); | ||||
|   await page.locator('button.c-create-button').click(); | ||||
|   await page.locator('li[role="menuitem"]:has-text("Overlay Plot")').click(); | ||||
|   await Promise.all([ | ||||
|     page.waitForNavigation({ waitUntil: 'networkidle' }), | ||||
|     page.locator('button:has-text("OK")').click(), | ||||
|     //Wait for Save Banner to appear | ||||
|     page.waitForSelector('.c-message-banner__message') | ||||
|   ]); | ||||
|   //Wait until Save Banner is gone | ||||
|   await page.locator('.c-message-banner__close-button').click(); | ||||
|   await page.waitForSelector('.c-message-banner__message', { state: 'detached' }); | ||||
|  | ||||
|     // save the overlay plot | ||||
|   // save the overlay plot | ||||
|  | ||||
|     await saveOverlayPlot(page); | ||||
|   await saveOverlayPlot(page); | ||||
|  | ||||
|     // create a sinewave generator | ||||
|   // create a sinewave generator | ||||
|  | ||||
|     await page.locator('button.c-create-button').click(); | ||||
|     await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click(); | ||||
|   await page.locator('button.c-create-button').click(); | ||||
|   await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click(); | ||||
|  | ||||
|     // set amplitude to 6, offset 4, period 2 | ||||
|   // set amplitude to 6, offset 4, period 2 | ||||
|  | ||||
|     await page.locator('div:nth-child(5) .c-form-row__controls .form-control .field input').click(); | ||||
|     await page.locator('div:nth-child(5) .c-form-row__controls .form-control .field input').fill('6'); | ||||
|   await page.locator('div:nth-child(5) .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) .c-form-row__controls .form-control .field input').click(); | ||||
|     await page.locator('div:nth-child(6) .c-form-row__controls .form-control .field input').fill('4'); | ||||
|   await page.locator('div:nth-child(6) .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) .c-form-row__controls .form-control .field input').click(); | ||||
|     await page.locator('div:nth-child(7) .c-form-row__controls .form-control .field input').fill('2'); | ||||
|   await page.locator('div:nth-child(7) .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 | ||||
|   // Click OK to make generator | ||||
|  | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation({ waitUntil: 'networkidle'}), | ||||
|         page.locator('button:has-text("OK")').click(), | ||||
|         //Wait for Save Banner to appear | ||||
|         page.waitForSelector('.c-message-banner__message') | ||||
|     ]); | ||||
|     //Wait until Save Banner is gone | ||||
|     await page.locator('.c-message-banner__close-button').click(); | ||||
|     await page.waitForSelector('.c-message-banner__message', { state: 'detached'}); | ||||
|   await Promise.all([ | ||||
|     page.waitForNavigation({ waitUntil: 'networkidle' }), | ||||
|     page.locator('button:has-text("OK")').click(), | ||||
|     //Wait for Save Banner to appear | ||||
|     page.waitForSelector('.c-message-banner__message') | ||||
|   ]); | ||||
|   //Wait until Save Banner is gone | ||||
|   await page.locator('.c-message-banner__close-button').click(); | ||||
|   await page.waitForSelector('.c-message-banner__message', { state: 'detached' }); | ||||
|  | ||||
|     // click on overlay plot | ||||
|   // click on overlay plot | ||||
|  | ||||
|     await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click(); | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('text=Unnamed Overlay Plot').first().click() | ||||
|     ]); | ||||
|   await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click(); | ||||
|   await Promise.all([ | ||||
|     page.waitForNavigation(), | ||||
|     page.locator('text=Unnamed Overlay Plot').first().click() | ||||
|   ]); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function testRegularTicks(page) { | ||||
|     const yTicks = page.locator('.gl-plot-y-tick-label'); | ||||
|     expect(await yTicks.count()).toBe(7); | ||||
|     await expect(yTicks.nth(0)).toHaveText('-2'); | ||||
|     await expect(yTicks.nth(1)).toHaveText('0'); | ||||
|     await expect(yTicks.nth(2)).toHaveText('2'); | ||||
|     await expect(yTicks.nth(3)).toHaveText('4'); | ||||
|     await expect(yTicks.nth(4)).toHaveText('6'); | ||||
|     await expect(yTicks.nth(5)).toHaveText('8'); | ||||
|     await expect(yTicks.nth(6)).toHaveText('10'); | ||||
|   const yTicks = page.locator('.gl-plot-y-tick-label'); | ||||
|   expect(await yTicks.count()).toBe(7); | ||||
|   await expect(yTicks.nth(0)).toHaveText('-2'); | ||||
|   await expect(yTicks.nth(1)).toHaveText('0'); | ||||
|   await expect(yTicks.nth(2)).toHaveText('2'); | ||||
|   await expect(yTicks.nth(3)).toHaveText('4'); | ||||
|   await expect(yTicks.nth(4)).toHaveText('6'); | ||||
|   await expect(yTicks.nth(5)).toHaveText('8'); | ||||
|   await expect(yTicks.nth(6)).toHaveText('10'); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function testLogTicks(page) { | ||||
|     const yTicks = page.locator('.gl-plot-y-tick-label'); | ||||
|     expect(await yTicks.count()).toBe(9); | ||||
|     await expect(yTicks.nth(0)).toHaveText('-2.98'); | ||||
|     await expect(yTicks.nth(1)).toHaveText('-1.51'); | ||||
|     await expect(yTicks.nth(2)).toHaveText('-0.58'); | ||||
|     await expect(yTicks.nth(3)).toHaveText('-0.00'); | ||||
|     await expect(yTicks.nth(4)).toHaveText('0.58'); | ||||
|     await expect(yTicks.nth(5)).toHaveText('1.51'); | ||||
|     await expect(yTicks.nth(6)).toHaveText('2.98'); | ||||
|     await expect(yTicks.nth(7)).toHaveText('5.31'); | ||||
|     await expect(yTicks.nth(8)).toHaveText('9.00'); | ||||
|   const yTicks = page.locator('.gl-plot-y-tick-label'); | ||||
|   expect(await yTicks.count()).toBe(9); | ||||
|   await expect(yTicks.nth(0)).toHaveText('-2.98'); | ||||
|   await expect(yTicks.nth(1)).toHaveText('-1.51'); | ||||
|   await expect(yTicks.nth(2)).toHaveText('-0.58'); | ||||
|   await expect(yTicks.nth(3)).toHaveText('-0.00'); | ||||
|   await expect(yTicks.nth(4)).toHaveText('0.58'); | ||||
|   await expect(yTicks.nth(5)).toHaveText('1.51'); | ||||
|   await expect(yTicks.nth(6)).toHaveText('2.98'); | ||||
|   await expect(yTicks.nth(7)).toHaveText('5.31'); | ||||
|   await expect(yTicks.nth(8)).toHaveText('9.00'); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function enableEditMode(page) { | ||||
|     // turn on edit mode | ||||
|     await page.getByRole('button', { name: 'Edit' }).click(); | ||||
|     await expect(page.getByRole('button', { name: 'Save' })).toBeVisible(); | ||||
|   // turn on edit mode | ||||
|   await page.getByRole('button', { name: 'Edit' }).click(); | ||||
|   await expect(page.getByRole('button', { name: 'Save' })).toBeVisible(); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function enableLogMode(page) { | ||||
|     await expect(page.getByRole('checkbox', { name: 'Log mode' })).not.toBeChecked(); | ||||
|     await page.getByRole('checkbox', { name: 'Log mode' }).check(); | ||||
|   await expect(page.getByRole('checkbox', { name: 'Log mode' })).not.toBeChecked(); | ||||
|   await page.getByRole('checkbox', { name: 'Log mode' }).check(); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function disableLogMode(page) { | ||||
|     await expect(page.getByRole('checkbox', { name: 'Log mode' })).toBeChecked(); | ||||
|     await page.getByRole('checkbox', { name: 'Log mode' }).uncheck(); | ||||
|   await expect(page.getByRole('checkbox', { name: 'Log mode' })).toBeChecked(); | ||||
|   await page.getByRole('checkbox', { name: 'Log mode' }).uncheck(); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function saveOverlayPlot(page) { | ||||
|     // save overlay plot | ||||
|     await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click(); | ||||
|   // save overlay plot | ||||
|   await page | ||||
|     .locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button') | ||||
|     .nth(1) | ||||
|     .click(); | ||||
|  | ||||
|     await Promise.all([ | ||||
|         page.locator('text=Save and Finish Editing').click(), | ||||
|         //Wait for Save Banner to appear | ||||
|         page.waitForSelector('.c-message-banner__message') | ||||
|     ]); | ||||
|     //Wait until Save Banner is gone | ||||
|     await page.locator('.c-message-banner__close-button').click(); | ||||
|     await page.waitForSelector('.c-message-banner__message', { state: 'detached' }); | ||||
|   await 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' }); | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -223,63 +232,63 @@ async function saveOverlayPlot(page) { | ||||
| // FIXME: Remove this eslint exception once implemented | ||||
| // eslint-disable-next-line no-unused-vars | ||||
| async function testLogPlotPixels(page) { | ||||
|     const pixelsMatch = await page.evaluate(async () => { | ||||
|         // TODO get canvas pixels at a few locations to make sure they're the correct color, to test that the plot comes out as expected. | ||||
|   const pixelsMatch = await page.evaluate(async () => { | ||||
|     // TODO get canvas pixels at a few locations to make sure they're the correct color, to test that the plot comes out as expected. | ||||
|  | ||||
|         await new Promise((r) => setTimeout(r, 5 * 1000)); | ||||
|     await new Promise((r) => setTimeout(r, 5 * 1000)); | ||||
|  | ||||
|         // These are some pixels that should be blue points in the log plot. | ||||
|         // If the plot changes shape to an unexpected shape, this will | ||||
|         // likely fail, which is what we want. | ||||
|         // | ||||
|         // I found these pixels by pausing playwright in debug mode at this | ||||
|         // point, and using similar code as below to output the pixel data, then | ||||
|         // I logged those pixels here. | ||||
|         const expectedBluePixels = [ | ||||
|             // TODO these pixel sets only work with the first test, but not the second test. | ||||
|     // These are some pixels that should be blue points in the log plot. | ||||
|     // If the plot changes shape to an unexpected shape, this will | ||||
|     // likely fail, which is what we want. | ||||
|     // | ||||
|     // I found these pixels by pausing playwright in debug mode at this | ||||
|     // point, and using similar code as below to output the pixel data, then | ||||
|     // I logged those pixels here. | ||||
|     const expectedBluePixels = [ | ||||
|       // TODO these pixel sets only work with the first test, but not the second test. | ||||
|  | ||||
|             // [60, 35], | ||||
|             // [121, 125], | ||||
|             // [156, 377], | ||||
|             // [264, 73], | ||||
|             // [372, 186], | ||||
|             // [576, 73], | ||||
|             // [659, 439], | ||||
|             // [675, 423] | ||||
|       // [60, 35], | ||||
|       // [121, 125], | ||||
|       // [156, 377], | ||||
|       // [264, 73], | ||||
|       // [372, 186], | ||||
|       // [576, 73], | ||||
|       // [659, 439], | ||||
|       // [675, 423] | ||||
|  | ||||
|             [60, 35], | ||||
|             [120, 125], | ||||
|             [156, 375], | ||||
|             [264, 73], | ||||
|             [372, 185], | ||||
|             [575, 72], | ||||
|             [659, 437], | ||||
|             [675, 421] | ||||
|         ]; | ||||
|       [60, 35], | ||||
|       [120, 125], | ||||
|       [156, 375], | ||||
|       [264, 73], | ||||
|       [372, 185], | ||||
|       [575, 72], | ||||
|       [659, 437], | ||||
|       [675, 421] | ||||
|     ]; | ||||
|  | ||||
|         // The first canvas in the DOM is the one that has the plot point | ||||
|         // icons (canvas 2d), which is the one we are testing. The second | ||||
|         // one in the DOM is the WebGL canvas with the line. (Why aren't | ||||
|         // they both WebGL?) | ||||
|         const canvas = document.querySelector('canvas'); | ||||
|     // The first canvas in the DOM is the one that has the plot point | ||||
|     // icons (canvas 2d), which is the one we are testing. The second | ||||
|     // one in the DOM is the WebGL canvas with the line. (Why aren't | ||||
|     // they both WebGL?) | ||||
|     const canvas = document.querySelector('canvas'); | ||||
|  | ||||
|         const ctx = canvas.getContext('2d'); | ||||
|     const ctx = canvas.getContext('2d'); | ||||
|  | ||||
|         for (const pixel of expectedBluePixels) { | ||||
|             // XXX Possible optimization: call getImageData only once with | ||||
|             // area including all pixels to be tested. | ||||
|             const data = ctx.getImageData(pixel[0], pixel[1], 1, 1).data; | ||||
|     for (const pixel of expectedBluePixels) { | ||||
|       // XXX Possible optimization: call getImageData only once with | ||||
|       // area including all pixels to be tested. | ||||
|       const data = ctx.getImageData(pixel[0], pixel[1], 1, 1).data; | ||||
|  | ||||
|             // #43b0ffff <-- openmct cyanish-blue with 100% opacity | ||||
|             // if (data[0] !== 0x43 || data[1] !== 0xb0 || data[2] !== 0xff || data[3] !== 0xff) { | ||||
|             if (data[0] === 0 && data[1] === 0 && data[2] === 0 && data[3] === 0) { | ||||
|                 // If any pixel is empty, it means we didn't hit a plot point. | ||||
|                 return false; | ||||
|             } | ||||
|         } | ||||
|       // #43b0ffff <-- openmct cyanish-blue with 100% opacity | ||||
|       // if (data[0] !== 0x43 || data[1] !== 0xb0 || data[2] !== 0xff || data[3] !== 0xff) { | ||||
|       if (data[0] === 0 && data[1] === 0 && data[2] === 0 && data[3] === 0) { | ||||
|         // If any pixel is empty, it means we didn't hit a plot point. | ||||
|         return false; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|         return true; | ||||
|     }); | ||||
|     return true; | ||||
|   }); | ||||
|  | ||||
|     expect(pixelsMatch).toBe(true); | ||||
|   expect(pixelsMatch).toBe(true); | ||||
| } | ||||
|   | ||||
| @@ -27,55 +27,56 @@ Tests to verify log plot functionality when objects are missing | ||||
| const { test, expect } = require('../../../../pluginFixtures'); | ||||
|  | ||||
| test.describe('Handle missing object for plots', () => { | ||||
|     test('Displays empty div for missing stacked plot item @unstable', async ({ page, browserName, openmctConfig }) => { | ||||
|         // eslint-disable-next-line playwright/no-skipped-test | ||||
|         test.skip(browserName === 'firefox', 'Firefox failing due to console events being missed'); | ||||
|   test('Displays empty div for missing stacked plot item @unstable', async ({ | ||||
|     page, | ||||
|     browserName, | ||||
|     openmctConfig | ||||
|   }) => { | ||||
|     // eslint-disable-next-line playwright/no-skipped-test | ||||
|     test.skip(browserName === 'firefox', 'Firefox failing due to console events being missed'); | ||||
|  | ||||
|         const { myItemsFolderName } = openmctConfig; | ||||
|         const errorLogs = []; | ||||
|     const { myItemsFolderName } = openmctConfig; | ||||
|     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, myItemsFolderName); | ||||
|  | ||||
|         //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 ${myItemsFolderName} >> 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); | ||||
|     page.on('console', (message) => { | ||||
|       if (message.type() === 'warning' && message.text().includes('Missing domain object')) { | ||||
|         errorLogs.push(message.text()); | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     //Make stacked plot | ||||
|     await makeStackedPlot(page, myItemsFolderName); | ||||
|  | ||||
|     //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 ${myItemsFolderName} >> 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); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| /** | ||||
| @@ -83,42 +84,42 @@ test.describe('Handle missing object for plots', () => { | ||||
|  * @private | ||||
|  */ | ||||
| async function makeStackedPlot(page, myItemsFolderName) { | ||||
|     // 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: 'domcontentloaded' }); | ||||
|   // 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: 'domcontentloaded' }); | ||||
|  | ||||
|     // create stacked plot | ||||
|     await page.locator('button.c-create-button').click(); | ||||
|     await page.locator('li[role="menuitem"]:has-text("Stacked Plot")').click(); | ||||
|   // create stacked plot | ||||
|   await page.locator('button.c-create-button').click(); | ||||
|   await page.locator('li[role="menuitem"]:has-text("Stacked Plot")').click(); | ||||
|  | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation({ waitUntil: 'networkidle'}), | ||||
|         page.locator('button:has-text("OK")').click(), | ||||
|         //Wait for Save Banner to appear | ||||
|         page.waitForSelector('.c-message-banner__message') | ||||
|     ]); | ||||
|   await Promise.all([ | ||||
|     page.waitForNavigation({ waitUntil: 'networkidle' }), | ||||
|     page.locator('button:has-text("OK")').click(), | ||||
|     //Wait for Save Banner to appear | ||||
|     page.waitForSelector('.c-message-banner__message') | ||||
|   ]); | ||||
|  | ||||
|     // save the stacked plot | ||||
|     await saveStackedPlot(page); | ||||
|   // save the stacked plot | ||||
|   await saveStackedPlot(page); | ||||
|  | ||||
|     // create a sinewave generator | ||||
|     await createSineWaveGenerator(page); | ||||
|   // create a sinewave generator | ||||
|   await createSineWaveGenerator(page); | ||||
|  | ||||
|     // click on stacked plot | ||||
|     await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click(); | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('text=Unnamed Stacked Plot').first().click() | ||||
|     ]); | ||||
|   // click on stacked plot | ||||
|   await page.locator(`text=Open MCT ${myItemsFolderName} >> 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); | ||||
|   // create a second sinewave generator | ||||
|   await createSineWaveGenerator(page); | ||||
|  | ||||
|     // click on stacked plot | ||||
|     await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click(); | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('text=Unnamed Stacked Plot').first().click() | ||||
|     ]); | ||||
|   // click on stacked plot | ||||
|   await page.locator(`text=Open MCT ${myItemsFolderName} >> span`).nth(3).click(); | ||||
|   await Promise.all([ | ||||
|     page.waitForNavigation(), | ||||
|     page.locator('text=Unnamed Stacked Plot').first().click() | ||||
|   ]); | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -126,17 +127,20 @@ async function makeStackedPlot(page, myItemsFolderName) { | ||||
|  * @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(); | ||||
|   // 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' }); | ||||
|   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' }); | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -144,14 +148,14 @@ async function saveStackedPlot(page) { | ||||
|  * @private | ||||
|  */ | ||||
| async function createSineWaveGenerator(page) { | ||||
|     //Create sine wave generator | ||||
|     await page.locator('button.c-create-button').click(); | ||||
|     await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click(); | ||||
|   //Create sine wave generator | ||||
|   await page.locator('button.c-create-button').click(); | ||||
|   await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click(); | ||||
|  | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation({ waitUntil: 'networkidle'}), | ||||
|         page.locator('button:has-text("OK")').click(), | ||||
|         //Wait for Save Banner to appear | ||||
|         page.waitForSelector('.c-message-banner__message') | ||||
|     ]); | ||||
|   await Promise.all([ | ||||
|     page.waitForNavigation({ waitUntil: 'networkidle' }), | ||||
|     page.locator('button:has-text("OK")').click(), | ||||
|     //Wait for Save Banner to appear | ||||
|     page.waitForSelector('.c-message-banner__message') | ||||
|   ]); | ||||
| } | ||||
|   | ||||
| @@ -26,261 +26,245 @@ necessarily be used for reference when writing new tests in this area. | ||||
| */ | ||||
|  | ||||
| const { test, expect } = require('../../../../pluginFixtures'); | ||||
| const { createDomainObjectWithDefaults, selectInspectorTab } = require('../../../../appActions'); | ||||
| const { | ||||
|   createDomainObjectWithDefaults, | ||||
|   getCanvasPixels, | ||||
|   selectInspectorTab, | ||||
|   waitForPlotsToRender | ||||
| } = require('../../../../appActions'); | ||||
|  | ||||
| test.describe('Overlay Plot', () => { | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|   test.beforeEach(async ({ page }) => { | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|   }); | ||||
|  | ||||
|   test('Plot legend color is in sync with plot series color', async ({ page }) => { | ||||
|     const overlayPlot = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Overlay Plot' | ||||
|     }); | ||||
|  | ||||
|     test('Plot legend color is in sync with plot series color', async ({ page }) => { | ||||
|         const overlayPlot = await createDomainObjectWithDefaults(page, { | ||||
|             type: "Overlay Plot" | ||||
|         }); | ||||
|  | ||||
|         await createDomainObjectWithDefaults(page, { | ||||
|             type: "Sine Wave Generator", | ||||
|             parent: overlayPlot.uuid | ||||
|         }); | ||||
|  | ||||
|         await page.goto(overlayPlot.url); | ||||
|  | ||||
|         await selectInspectorTab(page, 'Config'); | ||||
|  | ||||
|         // navigate to plot series color palette | ||||
|         await page.click('.l-browse-bar__actions__edit'); | ||||
|         await page.locator('li.c-tree__item.menus-to-left .c-disclosure-triangle').click(); | ||||
|         await page.locator('.c-click-swatch--menu').click(); | ||||
|         await page.locator('.c-palette__item[style="background: rgb(255, 166, 61);"]').click(); | ||||
|  | ||||
|         // gets color for swatch located in legend | ||||
|         const element = await page.waitForSelector('.plot-series-color-swatch'); | ||||
|         const color = await element.evaluate((el) => { | ||||
|             return window.getComputedStyle(el).getPropertyValue('background-color'); | ||||
|         }); | ||||
|  | ||||
|         expect(color).toBe('rgb(255, 166, 61)'); | ||||
|     await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Sine Wave Generator', | ||||
|       parent: overlayPlot.uuid | ||||
|     }); | ||||
|  | ||||
|     test('Limit lines persist when series is moved to another Y Axis and on refresh', async ({ page }) => { | ||||
|         test.info().annotations.push({ | ||||
|             type: 'issue', | ||||
|             description: 'https://github.com/nasa/openmct/issues/6338' | ||||
|         }); | ||||
|         // Create an Overlay Plot with a default SWG | ||||
|         const overlayPlot = await createDomainObjectWithDefaults(page, { | ||||
|             type: "Overlay Plot" | ||||
|         }); | ||||
|     await page.goto(overlayPlot.url); | ||||
|  | ||||
|         const swgA = await createDomainObjectWithDefaults(page, { | ||||
|             type: "Sine Wave Generator", | ||||
|             parent: overlayPlot.uuid | ||||
|         }); | ||||
|     await selectInspectorTab(page, 'Config'); | ||||
|  | ||||
|         await page.goto(overlayPlot.url); | ||||
|  | ||||
|         // Assert that no limit lines are shown by default | ||||
|         await page.waitForSelector('.js-limit-area', { state: 'attached' }); | ||||
|         expect(await page.locator('.c-plot-limit-line').count()).toBe(0); | ||||
|  | ||||
|         // Enter edit mode | ||||
|         await page.click('button[title="Edit"]'); | ||||
|  | ||||
|         // Expand the "Sine Wave Generator" plot series options and enable limit lines | ||||
|         await selectInspectorTab(page, 'Config'); | ||||
|         await page.getByRole('list', { name: 'Plot Series Properties' }).locator('span').first().click(); | ||||
|         await page.getByRole('list', { name: 'Plot Series Properties' }).locator('[title="Display limit lines"]~div input').check(); | ||||
|  | ||||
|         await assertLimitLinesExistAndAreVisible(page); | ||||
|  | ||||
|         // Save (exit edit mode) | ||||
|         await page.locator('button[title="Save"]').click(); | ||||
|         await page.locator('li[title="Save and Finish Editing"]').click(); | ||||
|  | ||||
|         await assertLimitLinesExistAndAreVisible(page); | ||||
|  | ||||
|         await page.reload(); | ||||
|  | ||||
|         await assertLimitLinesExistAndAreVisible(page); | ||||
|  | ||||
|         // Enter edit mode | ||||
|         await page.click('button[title="Edit"]'); | ||||
|  | ||||
|         await selectInspectorTab(page, 'Elements'); | ||||
|  | ||||
|         // Drag Sine Wave Generator series from Y Axis 1 into Y Axis 2 | ||||
|         await page.locator(`#inspector-elements-tree >> text=${swgA.name}`).dragTo(page.locator('[aria-label="Element Item Group Y Axis 2"]')); | ||||
|  | ||||
|         await assertLimitLinesExistAndAreVisible(page); | ||||
|  | ||||
|         // Save (exit edit mode) | ||||
|         await page.locator('button[title="Save"]').click(); | ||||
|         await page.locator('li[title="Save and Finish Editing"]').click(); | ||||
|  | ||||
|         await assertLimitLinesExistAndAreVisible(page); | ||||
|  | ||||
|         await page.reload(); | ||||
|  | ||||
|         await assertLimitLinesExistAndAreVisible(page); | ||||
|     // navigate to plot series color palette | ||||
|     await page.click('.l-browse-bar__actions__edit'); | ||||
|     await page.locator('li.c-tree__item.menus-to-left .c-disclosure-triangle').click(); | ||||
|     await page.locator('.c-click-swatch--menu').click(); | ||||
|     await page.locator('.c-palette__item[style="background: rgb(255, 166, 61);"]').click(); | ||||
|     // gets color for swatch located in legend | ||||
|     const seriesColorSwatch = page.locator( | ||||
|       '.gl-plot-y-label-swatch-container > .plot-series-color-swatch' | ||||
|     ); | ||||
|     await expect(seriesColorSwatch).toHaveCSS('background-color', 'rgb(255, 166, 61)'); | ||||
|   }); | ||||
|  | ||||
|   test('Limit lines persist when series is moved to another Y Axis and on refresh', async ({ | ||||
|     page | ||||
|   }) => { | ||||
|     test.info().annotations.push({ | ||||
|       type: 'issue', | ||||
|       description: 'https://github.com/nasa/openmct/issues/6338' | ||||
|     }); | ||||
|     // Create an Overlay Plot with a default SWG | ||||
|     const overlayPlot = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Overlay Plot' | ||||
|     }); | ||||
|  | ||||
|     test('The elements pool supports dragging series into multiple y-axis buckets', async ({ page }) => { | ||||
|         const overlayPlot = await createDomainObjectWithDefaults(page, { | ||||
|             type: "Overlay Plot" | ||||
|         }); | ||||
|  | ||||
|         const swgA = await createDomainObjectWithDefaults(page, { | ||||
|             type: "Sine Wave Generator", | ||||
|             parent: overlayPlot.uuid | ||||
|         }); | ||||
|         const swgB = await createDomainObjectWithDefaults(page, { | ||||
|             type: "Sine Wave Generator", | ||||
|             parent: overlayPlot.uuid | ||||
|         }); | ||||
|         const swgC = await createDomainObjectWithDefaults(page, { | ||||
|             type: "Sine Wave Generator", | ||||
|             parent: overlayPlot.uuid | ||||
|         }); | ||||
|         const swgD = await createDomainObjectWithDefaults(page, { | ||||
|             type: "Sine Wave Generator", | ||||
|             parent: overlayPlot.uuid | ||||
|         }); | ||||
|         const swgE = await createDomainObjectWithDefaults(page, { | ||||
|             type: "Sine Wave Generator", | ||||
|             parent: overlayPlot.uuid | ||||
|         }); | ||||
|  | ||||
|         await page.goto(overlayPlot.url); | ||||
|         await page.click('button[title="Edit"]'); | ||||
|  | ||||
|         await selectInspectorTab(page, 'Elements'); | ||||
|  | ||||
|         // Drag swg a, c, e into Y Axis 2 | ||||
|         await page.locator(`#inspector-elements-tree >> text=${swgA.name}`).dragTo(page.locator('[aria-label="Element Item Group Y Axis 2"]')); | ||||
|         await page.locator(`#inspector-elements-tree >> text=${swgC.name}`).dragTo(page.locator('[aria-label="Element Item Group Y Axis 2"]')); | ||||
|         await page.locator(`#inspector-elements-tree >> text=${swgE.name}`).dragTo(page.locator('[aria-label="Element Item Group Y Axis 2"]')); | ||||
|  | ||||
|         // Assert that Y Axis 1 and Y Axis 2 property groups are visible only | ||||
|         await selectInspectorTab(page, 'Config'); | ||||
|  | ||||
|         const yAxis1PropertyGroup = page.locator('[aria-label="Y Axis Properties"]'); | ||||
|         const yAxis2PropertyGroup = page.locator('[aria-label="Y Axis 2 Properties"]'); | ||||
|         const yAxis3PropertyGroup = page.locator('[aria-label="Y Axis 3 Properties"]'); | ||||
|  | ||||
|         await expect(yAxis1PropertyGroup).toBeVisible(); | ||||
|         await expect(yAxis2PropertyGroup).toBeVisible(); | ||||
|         await expect(yAxis3PropertyGroup).toBeHidden(); | ||||
|  | ||||
|         const yAxis1Group = page.getByLabel("Y Axis 1"); | ||||
|         const yAxis2Group = page.getByLabel("Y Axis 2"); | ||||
|         const yAxis3Group = page.getByLabel("Y Axis 3"); | ||||
|  | ||||
|         await selectInspectorTab(page, 'Elements'); | ||||
|  | ||||
|         // Drag swg b into Y Axis 3 | ||||
|         await page.locator(`#inspector-elements-tree >> text=${swgB.name}`).dragTo(page.locator('[aria-label="Element Item Group Y Axis 3"]')); | ||||
|  | ||||
|         // Assert that all Y Axis property groups are visible | ||||
|         await selectInspectorTab(page, 'Config'); | ||||
|  | ||||
|         await expect(yAxis1PropertyGroup).toBeVisible(); | ||||
|         await expect(yAxis2PropertyGroup).toBeVisible(); | ||||
|         await expect(yAxis3PropertyGroup).toBeVisible(); | ||||
|  | ||||
|         // Verify that the elements are in the correct buckets and in the correct order | ||||
|         await selectInspectorTab(page, 'Elements'); | ||||
|  | ||||
|         expect(yAxis1Group.getByRole('listitem', { name: swgD.name })).toBeTruthy(); | ||||
|         expect(yAxis1Group.getByRole('listitem').nth(0).getByText(swgD.name)).toBeTruthy(); | ||||
|         expect(yAxis2Group.getByRole('listitem', { name: swgE.name })).toBeTruthy(); | ||||
|         expect(yAxis2Group.getByRole('listitem').nth(0).getByText(swgE.name)).toBeTruthy(); | ||||
|         expect(yAxis2Group.getByRole('listitem', { name: swgC.name })).toBeTruthy(); | ||||
|         expect(yAxis2Group.getByRole('listitem').nth(1).getByText(swgC.name)).toBeTruthy(); | ||||
|         expect(yAxis2Group.getByRole('listitem', { name: swgA.name })).toBeTruthy(); | ||||
|         expect(yAxis2Group.getByRole('listitem').nth(2).getByText(swgA.name)).toBeTruthy(); | ||||
|         expect(yAxis3Group.getByRole('listitem', { name: swgB.name })).toBeTruthy(); | ||||
|         expect(yAxis3Group.getByRole('listitem').nth(0).getByText(swgB.name)).toBeTruthy(); | ||||
|     const swgA = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Sine Wave Generator', | ||||
|       parent: overlayPlot.uuid | ||||
|     }); | ||||
|  | ||||
|     test('Clicking on an item in the elements pool brings up the plot preview with data points', async ({ page }) => { | ||||
|         const overlayPlot = await createDomainObjectWithDefaults(page, { | ||||
|             type: "Overlay Plot" | ||||
|         }); | ||||
|     await page.goto(overlayPlot.url); | ||||
|  | ||||
|         const swgA = await createDomainObjectWithDefaults(page, { | ||||
|             type: "Sine Wave Generator", | ||||
|             parent: overlayPlot.uuid | ||||
|         }); | ||||
|     // Assert that no limit lines are shown by default | ||||
|     await page.waitForSelector('.js-limit-area', { state: 'attached' }); | ||||
|     expect(await page.locator('.c-plot-limit-line').count()).toBe(0); | ||||
|  | ||||
|         await page.goto(overlayPlot.url); | ||||
|         // Wait for plot series data to load and be drawn | ||||
|         await expect(page.locator('.js-series-data-loaded')).toBeVisible(); | ||||
|         await page.click('button[title="Edit"]'); | ||||
|     // Enter edit mode | ||||
|     await page.click('button[title="Edit"]'); | ||||
|  | ||||
|         await selectInspectorTab(page, 'Elements'); | ||||
|     // Expand the "Sine Wave Generator" plot series options and enable limit lines | ||||
|     await selectInspectorTab(page, 'Config'); | ||||
|     await page | ||||
|       .getByRole('list', { name: 'Plot Series Properties' }) | ||||
|       .locator('span') | ||||
|       .first() | ||||
|       .click(); | ||||
|     await page | ||||
|       .getByRole('list', { name: 'Plot Series Properties' }) | ||||
|       .locator('[title="Display limit lines"]~div input') | ||||
|       .check(); | ||||
|  | ||||
|         await page.locator(`#inspector-elements-tree >> text=${swgA.name}`).click(); | ||||
|     await assertLimitLinesExistAndAreVisible(page); | ||||
|  | ||||
|         // Wait for "View Large" plot series data to load and be drawn | ||||
|         await expect(page.locator('.c-overlay .js-series-data-loaded')).toBeVisible(); | ||||
|     // Save (exit edit mode) | ||||
|     await page.locator('button[title="Save"]').click(); | ||||
|     await page.locator('li[title="Save and Finish Editing"]').click(); | ||||
|  | ||||
|         const plotPixelSize = await getCanvasPixelsWithData(page); | ||||
|         expect(plotPixelSize).toBeGreaterThan(0); | ||||
|     await assertLimitLinesExistAndAreVisible(page); | ||||
|  | ||||
|     await page.reload(); | ||||
|  | ||||
|     await assertLimitLinesExistAndAreVisible(page); | ||||
|  | ||||
|     // Enter edit mode | ||||
|     await page.click('button[title="Edit"]'); | ||||
|  | ||||
|     await selectInspectorTab(page, 'Elements'); | ||||
|  | ||||
|     // Drag Sine Wave Generator series from Y Axis 1 into Y Axis 2 | ||||
|     await page | ||||
|       .locator(`#inspector-elements-tree >> text=${swgA.name}`) | ||||
|       .dragTo(page.locator('[aria-label="Element Item Group Y Axis 2"]')); | ||||
|  | ||||
|     await assertLimitLinesExistAndAreVisible(page); | ||||
|  | ||||
|     // Save (exit edit mode) | ||||
|     await page.locator('button[title="Save"]').click(); | ||||
|     await page.locator('li[title="Save and Finish Editing"]').click(); | ||||
|  | ||||
|     await assertLimitLinesExistAndAreVisible(page); | ||||
|  | ||||
|     await page.reload(); | ||||
|  | ||||
|     await assertLimitLinesExistAndAreVisible(page); | ||||
|   }); | ||||
|  | ||||
|   test('The elements pool supports dragging series into multiple y-axis buckets', async ({ | ||||
|     page | ||||
|   }) => { | ||||
|     const overlayPlot = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Overlay Plot' | ||||
|     }); | ||||
|  | ||||
|     const swgA = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Sine Wave Generator', | ||||
|       parent: overlayPlot.uuid | ||||
|     }); | ||||
|     const swgB = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Sine Wave Generator', | ||||
|       parent: overlayPlot.uuid | ||||
|     }); | ||||
|     const swgC = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Sine Wave Generator', | ||||
|       parent: overlayPlot.uuid | ||||
|     }); | ||||
|     const swgD = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Sine Wave Generator', | ||||
|       parent: overlayPlot.uuid | ||||
|     }); | ||||
|     const swgE = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Sine Wave Generator', | ||||
|       parent: overlayPlot.uuid | ||||
|     }); | ||||
|  | ||||
|     await page.goto(overlayPlot.url); | ||||
|     await page.click('button[title="Edit"]'); | ||||
|  | ||||
|     await selectInspectorTab(page, 'Elements'); | ||||
|  | ||||
|     // Drag swg a, c, e into Y Axis 2 | ||||
|     await page | ||||
|       .locator(`#inspector-elements-tree >> text=${swgA.name}`) | ||||
|       .dragTo(page.locator('[aria-label="Element Item Group Y Axis 2"]')); | ||||
|     await page | ||||
|       .locator(`#inspector-elements-tree >> text=${swgC.name}`) | ||||
|       .dragTo(page.locator('[aria-label="Element Item Group Y Axis 2"]')); | ||||
|     await page | ||||
|       .locator(`#inspector-elements-tree >> text=${swgE.name}`) | ||||
|       .dragTo(page.locator('[aria-label="Element Item Group Y Axis 2"]')); | ||||
|  | ||||
|     // Assert that Y Axis 1 and Y Axis 2 property groups are visible only | ||||
|     await selectInspectorTab(page, 'Config'); | ||||
|  | ||||
|     const yAxis1PropertyGroup = page.locator('[aria-label="Y Axis Properties"]'); | ||||
|     const yAxis2PropertyGroup = page.locator('[aria-label="Y Axis 2 Properties"]'); | ||||
|     const yAxis3PropertyGroup = page.locator('[aria-label="Y Axis 3 Properties"]'); | ||||
|  | ||||
|     await expect(yAxis1PropertyGroup).toBeVisible(); | ||||
|     await expect(yAxis2PropertyGroup).toBeVisible(); | ||||
|     await expect(yAxis3PropertyGroup).toBeHidden(); | ||||
|  | ||||
|     const yAxis1Group = page.getByLabel('Y Axis 1'); | ||||
|     const yAxis2Group = page.getByLabel('Y Axis 2'); | ||||
|     const yAxis3Group = page.getByLabel('Y Axis 3'); | ||||
|  | ||||
|     await selectInspectorTab(page, 'Elements'); | ||||
|  | ||||
|     // Drag swg b into Y Axis 3 | ||||
|     await page | ||||
|       .locator(`#inspector-elements-tree >> text=${swgB.name}`) | ||||
|       .dragTo(page.locator('[aria-label="Element Item Group Y Axis 3"]')); | ||||
|  | ||||
|     // Assert that all Y Axis property groups are visible | ||||
|     await selectInspectorTab(page, 'Config'); | ||||
|  | ||||
|     await expect(yAxis1PropertyGroup).toBeVisible(); | ||||
|     await expect(yAxis2PropertyGroup).toBeVisible(); | ||||
|     await expect(yAxis3PropertyGroup).toBeVisible(); | ||||
|  | ||||
|     // Verify that the elements are in the correct buckets and in the correct order | ||||
|     await selectInspectorTab(page, 'Elements'); | ||||
|  | ||||
|     expect(yAxis1Group.getByRole('listitem', { name: swgD.name })).toBeTruthy(); | ||||
|     expect(yAxis1Group.getByRole('listitem').nth(0).getByText(swgD.name)).toBeTruthy(); | ||||
|     expect(yAxis2Group.getByRole('listitem', { name: swgE.name })).toBeTruthy(); | ||||
|     expect(yAxis2Group.getByRole('listitem').nth(0).getByText(swgE.name)).toBeTruthy(); | ||||
|     expect(yAxis2Group.getByRole('listitem', { name: swgC.name })).toBeTruthy(); | ||||
|     expect(yAxis2Group.getByRole('listitem').nth(1).getByText(swgC.name)).toBeTruthy(); | ||||
|     expect(yAxis2Group.getByRole('listitem', { name: swgA.name })).toBeTruthy(); | ||||
|     expect(yAxis2Group.getByRole('listitem').nth(2).getByText(swgA.name)).toBeTruthy(); | ||||
|     expect(yAxis3Group.getByRole('listitem', { name: swgB.name })).toBeTruthy(); | ||||
|     expect(yAxis3Group.getByRole('listitem').nth(0).getByText(swgB.name)).toBeTruthy(); | ||||
|   }); | ||||
|  | ||||
|   test('Clicking on an item in the elements pool brings up the plot preview with data points', async ({ | ||||
|     page | ||||
|   }) => { | ||||
|     const overlayPlot = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Overlay Plot' | ||||
|     }); | ||||
|  | ||||
|     const swgA = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Sine Wave Generator', | ||||
|       parent: overlayPlot.uuid | ||||
|     }); | ||||
|  | ||||
|     await page.goto(overlayPlot.url); | ||||
|     // Wait for plot series data to load and be drawn | ||||
|     await waitForPlotsToRender(page); | ||||
|     await page.click('button[title="Edit"]'); | ||||
|  | ||||
|     await selectInspectorTab(page, 'Elements'); | ||||
|  | ||||
|     await page.locator(`#inspector-elements-tree >> text=${swgA.name}`).click(); | ||||
|  | ||||
|     const plotPixels = await getCanvasPixels(page, '.js-overlay canvas'); | ||||
|     const plotPixelSize = plotPixels.length; | ||||
|     expect(plotPixelSize).toBeGreaterThan(0); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function getCanvasPixelsWithData(page) { | ||||
|     const getTelemValuePromise = new Promise(resolve => page.exposeFunction('getCanvasValue', resolve)); | ||||
|  | ||||
|     await page.evaluate(() => { | ||||
|         // The document canvas is where the plot points and lines are drawn. | ||||
|         // The only way to access the canvas is using document (using page.evaluate) | ||||
|         let data; | ||||
|         let canvas; | ||||
|         let ctx; | ||||
|         canvas = document.querySelector('.js-overlay canvas'); | ||||
|         ctx = canvas.getContext('2d'); | ||||
|         data = ctx.getImageData(0, 0, canvas.width, canvas.height).data; | ||||
|         const imageDataValues = Object.values(data); | ||||
|         let plotPixels = []; | ||||
|         // Each pixel consists of four values within the ImageData.data array. The for loop iterates by multiples of four. | ||||
|         // The values associated with each pixel are R (red), G (green), B (blue), and A (alpha), in that order. | ||||
|         for (let i = 0; i < imageDataValues.length;) { | ||||
|             if (imageDataValues[i] > 0) { | ||||
|                 plotPixels.push({ | ||||
|                     startIndex: i, | ||||
|                     endIndex: i + 3, | ||||
|                     value: `rgb(${imageDataValues[i]}, ${imageDataValues[i + 1]}, ${imageDataValues[i + 2]}, ${imageDataValues[i + 3]})` | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|             i = i + 4; | ||||
|  | ||||
|         } | ||||
|  | ||||
|         window.getCanvasValue(plotPixels.length); | ||||
|     }); | ||||
|  | ||||
|     return getTelemValuePromise; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * | ||||
|  * Asserts that limit lines exist and are visible | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function assertLimitLinesExistAndAreVisible(page) { | ||||
|     // Wait for plot series data to load | ||||
|     await expect(page.locator('.js-series-data-loaded')).toBeVisible(); | ||||
|     // Wait for limit lines to be created | ||||
|     await page.waitForSelector('.js-limit-area', { state: 'attached' }); | ||||
|     const limitLineCount = await page.locator('.c-plot-limit-line').count(); | ||||
|     // There should be 10 limit lines created by default | ||||
|     expect(await page.locator('.c-plot-limit-line').count()).toBe(10); | ||||
|     for (let i = 0; i < limitLineCount; i++) { | ||||
|         await expect(page.locator('.c-plot-limit-line').nth(i)).toBeVisible(); | ||||
|     } | ||||
|   // Wait for plot series data to load | ||||
|   await waitForPlotsToRender(page); | ||||
|   // Wait for limit lines to be created | ||||
|   await page.waitForSelector('.js-limit-area', { state: 'attached' }); | ||||
|   const limitLineCount = await page.locator('.c-plot-limit-line').count(); | ||||
|   // There should be 10 limit lines created by default | ||||
|   expect(await page.locator('.c-plot-limit-line').count()).toBe(10); | ||||
|   for (let i = 0; i < limitLineCount; i++) { | ||||
|     await expect(page.locator('.c-plot-limit-line').nth(i)).toBeVisible(); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,113 +0,0 @@ | ||||
| /***************************************************************************** | ||||
|  * Open MCT, Copyright (c) 2014-2023, United States Government | ||||
|  * as represented by the Administrator of the National Aeronautics and Space | ||||
|  * Administration. All rights reserved. | ||||
|  * | ||||
|  * Open MCT is licensed under the Apache License, Version 2.0 (the | ||||
|  * "License"); you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * http://www.apache.org/licenses/LICENSE-2.0. | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||||
|  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||||
|  * License for the specific language governing permissions and limitations | ||||
|  * under the License. | ||||
|  * | ||||
|  * Open MCT includes source code licensed under additional open source | ||||
|  * licenses. See the Open Source Licenses file (LICENSES.md) included with | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* | ||||
| Tests to verify log plot functionality. Note this test suite if very much under active development and should not | ||||
| necessarily be used for reference when writing new tests in this area. | ||||
| */ | ||||
|  | ||||
| const { selectInspectorTab } = require('../../../../appActions'); | ||||
| const { test, expect } = require('../../../../pluginFixtures'); | ||||
|  | ||||
| test.describe('Legend color in sync with plot color', () => { | ||||
|     test('Testing', async ({ page }) => { | ||||
|         await makeOverlayPlot(page); | ||||
|  | ||||
|         // navigate to plot series color palette | ||||
|         await page.click('.l-browse-bar__actions__edit'); | ||||
|         await selectInspectorTab(page, 'Config'); | ||||
|  | ||||
|         await page.locator('li.c-tree__item.menus-to-left .c-disclosure-triangle').click(); | ||||
|         await page.locator('.c-click-swatch--menu').click(); | ||||
|         await page.locator('.c-palette__item[style="background: rgb(255, 166, 61);"]').click(); | ||||
|  | ||||
|         // gets color for swatch located in legend | ||||
|         const element = await page.waitForSelector('.plot-series-color-swatch'); | ||||
|         const color = await element.evaluate((el) => { | ||||
|             return window.getComputedStyle(el).getPropertyValue('background-color'); | ||||
|         }); | ||||
|  | ||||
|         expect(color).toBe('rgb(255, 166, 61)'); | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| async function saveOverlayPlot(page) { | ||||
|     // save overlay plot | ||||
|     await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click(); | ||||
|  | ||||
|     await Promise.all([ | ||||
|         page.locator('text=Save and Finish Editing').click(), | ||||
|         //Wait for Save Banner to appear | ||||
|         page.waitForSelector('.c-message-banner__message') | ||||
|     ]); | ||||
|     //Wait until Save Banner is gone | ||||
|     await page.locator('.c-message-banner__close-button').click(); | ||||
|     await page.waitForSelector('.c-message-banner__message', { state: 'detached' }); | ||||
| } | ||||
|  | ||||
| async function makeOverlayPlot(page) { | ||||
|     // fresh page with time range from 2022-03-29 22:00:00.000Z to 2022-03-29 22:00:30.000Z | ||||
|     await page.goto('/', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|     // create overlay plot | ||||
|  | ||||
|     await page.locator('button.c-create-button').click(); | ||||
|     await page.locator('li[role="menuitem"]:has-text("Overlay Plot")').click(); | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation({ waitUntil: 'networkidle'}), | ||||
|         page.locator('button:has-text("OK")').click(), | ||||
|         //Wait for Save Banner to appear | ||||
|         page.waitForSelector('.c-message-banner__message') | ||||
|     ]); | ||||
|     //Wait until Save Banner is gone | ||||
|     await page.locator('.c-message-banner__close-button').click(); | ||||
|     await page.waitForSelector('.c-message-banner__message', { state: 'detached'}); | ||||
|  | ||||
|     // save the overlay plot | ||||
|  | ||||
|     await saveOverlayPlot(page); | ||||
|  | ||||
|     // create a sinewave generator | ||||
|  | ||||
|     await page.locator('button.c-create-button').click(); | ||||
|     await page.locator('li[role="menuitem"]:has-text("Sine Wave Generator")').click(); | ||||
|  | ||||
|     // Click OK to make generator | ||||
|  | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation({ waitUntil: 'networkidle'}), | ||||
|         page.locator('button:has-text("OK")').click(), | ||||
|         //Wait for Save Banner to appear | ||||
|         page.waitForSelector('.c-message-banner__message') | ||||
|     ]); | ||||
|     //Wait until Save Banner is gone | ||||
|     await page.locator('.c-message-banner__close-button').click(); | ||||
|     await page.waitForSelector('.c-message-banner__message', { state: 'detached'}); | ||||
|  | ||||
|     // click on overlay plot | ||||
|  | ||||
|     await page.locator('text=Open MCT My Items >> span').nth(3).click(); | ||||
|     await Promise.all([ | ||||
|         page.waitForNavigation(), | ||||
|         page.locator('text=Unnamed Overlay Plot').first().click() | ||||
|     ]); | ||||
| } | ||||
| @@ -21,44 +21,46 @@ | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* | ||||
| * This test suite is dedicated to testing the rendering and interaction of plots. | ||||
| * | ||||
| */ | ||||
|  * This test suite is dedicated to testing the rendering and interaction of plots. | ||||
|  * | ||||
|  */ | ||||
|  | ||||
| const { test, expect } = require('../../../../pluginFixtures'); | ||||
| const { createDomainObjectWithDefaults} = require('../../../../appActions'); | ||||
| const { createDomainObjectWithDefaults, getCanvasPixels } = require('../../../../appActions'); | ||||
|  | ||||
| test.describe('Plot Integrity Testing @unstable', () => { | ||||
|     let sineWaveGeneratorObject; | ||||
| test.describe('Plot Rendering', () => { | ||||
|   let sineWaveGeneratorObject; | ||||
|  | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         //Open a browser, navigate to the main page, and wait until all networkevents to resolve | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|         sineWaveGeneratorObject = await createDomainObjectWithDefaults(page, { type: 'Sine Wave Generator' }); | ||||
|   test.beforeEach(async ({ page }) => { | ||||
|     // Open a browser, navigate to the main page, and wait until all networkevents to resolve | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|     sineWaveGeneratorObject = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Sine Wave Generator' | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|     test('Plots do not re-request data when a plot is clicked', async ({ page }) => { | ||||
|         //Navigate to Sine Wave Generator | ||||
|         await page.goto(sineWaveGeneratorObject.url); | ||||
|         //Click on the plot canvas | ||||
|         await page.locator('canvas').nth(1).click(); | ||||
|         //No request was made to get historical data | ||||
|         const createMineFolderRequests = []; | ||||
|         page.on('request', req => { | ||||
|             // eslint-disable-next-line playwright/no-conditional-in-test | ||||
|             createMineFolderRequests.push(req); | ||||
|         }); | ||||
|         expect(createMineFolderRequests.length).toEqual(0); | ||||
|   test('Plots do not re-request data when a plot is clicked', async ({ page }) => { | ||||
|     // Navigate to Sine Wave Generator | ||||
|     await page.goto(sineWaveGeneratorObject.url); | ||||
|     // Click on the plot canvas | ||||
|     await page.locator('canvas').nth(1).click(); | ||||
|     // No request was made to get historical data | ||||
|     const createMineFolderRequests = []; | ||||
|     page.on('request', (req) => { | ||||
|       createMineFolderRequests.push(req); | ||||
|     }); | ||||
|     expect(createMineFolderRequests.length).toEqual(0); | ||||
|   }); | ||||
|  | ||||
|     test('Plot is rendered when infinity values exist', async ({ page }) => { | ||||
|         // Edit Plot | ||||
|         await editSineWaveToUseInfinityOption(page, sineWaveGeneratorObject); | ||||
|   test('Plot is rendered when infinity values exist', async ({ page }) => { | ||||
|     // Edit Plot | ||||
|     await editSineWaveToUseInfinityOption(page, sineWaveGeneratorObject); | ||||
|  | ||||
|         //Get pixel data from Canvas | ||||
|         const plotPixelSize = await getCanvasPixelsWithData(page); | ||||
|         expect(plotPixelSize).toBeGreaterThan(0); | ||||
|     }); | ||||
|     //Get pixel data from Canvas | ||||
|     const plotPixels = await getCanvasPixels(page, 'canvas'); | ||||
|     const plotPixelSize = plotPixels.length; | ||||
|     expect(plotPixelSize).toBeGreaterThan(0); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| /** | ||||
| @@ -69,71 +71,24 @@ test.describe('Plot Integrity Testing @unstable', () => { | ||||
|  * @returns {Promise<CreatedObjectInfo>} An object containing information about the edited domain object. | ||||
|  */ | ||||
| async function editSineWaveToUseInfinityOption(page, sineWaveGeneratorObject) { | ||||
|     await page.goto(sineWaveGeneratorObject.url); | ||||
|     // Edit LAD table | ||||
|     await page.locator('[title="More options"]').click(); | ||||
|     await page.locator('[title="Edit properties of this object."]').click(); | ||||
|     // Modify the infinity option to true | ||||
|     const infinityInput = page.locator('[aria-label="Include Infinity Values"]'); | ||||
|     await infinityInput.click(); | ||||
|   await page.goto(sineWaveGeneratorObject.url); | ||||
|   // Edit SWG properties to include infinity values | ||||
|   await page.locator('[title="More options"]').click(); | ||||
|   await page.locator('[title="Edit properties of this object."]').click(); | ||||
|   await page | ||||
|     .getByRole('switch', { | ||||
|       name: 'Include Infinity Values' | ||||
|     }) | ||||
|     .check(); | ||||
|  | ||||
|     // Click OK button and wait for Navigate event | ||||
|     await Promise.all([ | ||||
|         page.waitForLoadState(), | ||||
|         page.click('[aria-label="Save"]'), | ||||
|         // Wait for Save Banner to appear | ||||
|         page.waitForSelector('.c-message-banner__message') | ||||
|     ]); | ||||
|   await page | ||||
|     .getByRole('button', { | ||||
|       name: 'Save' | ||||
|     }) | ||||
|     .click(); | ||||
|  | ||||
|     // FIXME: Changes to SWG properties should be reflected on save, but they're not? | ||||
|     // Thus, navigate away and back to the object. | ||||
|     await page.goto('./#/browse/mine'); | ||||
|     await page.goto(sineWaveGeneratorObject.url); | ||||
|  | ||||
|     await page.locator('c-progress-bar c-telemetry-table__progress-bar').waitFor({ | ||||
|         state: 'hidden' | ||||
|     }); | ||||
|  | ||||
|     // FIXME: The progress bar disappears on series data load, not on plot render, | ||||
|     // so wait for a half a second before evaluating the canvas. | ||||
|     // eslint-disable-next-line playwright/no-wait-for-timeout | ||||
|     await page.waitForTimeout(500); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function getCanvasPixelsWithData(page) { | ||||
|     const getTelemValuePromise = new Promise(resolve => page.exposeFunction('getCanvasValue', resolve)); | ||||
|  | ||||
|     await page.evaluate(() => { | ||||
|         // The document canvas is where the plot points and lines are drawn. | ||||
|         // The only way to access the canvas is using document (using page.evaluate) | ||||
|         let data; | ||||
|         let canvas; | ||||
|         let ctx; | ||||
|         canvas = document.querySelector('canvas'); | ||||
|         ctx = canvas.getContext('2d'); | ||||
|         data = ctx.getImageData(0, 0, canvas.width, canvas.height).data; | ||||
|         const imageDataValues = Object.values(data); | ||||
|         let plotPixels = []; | ||||
|         // Each pixel consists of four values within the ImageData.data array. The for loop iterates by multiples of four. | ||||
|         // The values associated with each pixel are R (red), G (green), B (blue), and A (alpha), in that order. | ||||
|         for (let i = 0; i < imageDataValues.length;) { | ||||
|             if (imageDataValues[i] > 0) { | ||||
|                 plotPixels.push({ | ||||
|                     startIndex: i, | ||||
|                     endIndex: i + 3, | ||||
|                     value: `rgb(${imageDataValues[i]}, ${imageDataValues[i + 1]}, ${imageDataValues[i + 2]}, ${imageDataValues[i + 3]})` | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|             i = i + 4; | ||||
|  | ||||
|         } | ||||
|  | ||||
|         window.getCanvasValue(plotPixels.length); | ||||
|     }); | ||||
|  | ||||
|     return getTelemValuePromise; | ||||
|   // FIXME: Changes to SWG properties should be reflected on save, but they're not? | ||||
|   // Thus, navigate away and back to the object. | ||||
|   await page.goto('./#/browse/mine'); | ||||
|   await page.goto(sineWaveGeneratorObject.url); | ||||
| } | ||||
|   | ||||
| @@ -21,77 +21,89 @@ | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* | ||||
| * This test suite is dedicated to testing the Scatter Plot component. | ||||
| */ | ||||
|  * This test suite is dedicated to testing the Scatter Plot component. | ||||
|  */ | ||||
|  | ||||
| const { test, expect } = require('../../../../pluginFixtures'); | ||||
| const { createDomainObjectWithDefaults, selectInspectorTab } = require('../../../../appActions'); | ||||
| const uuid = require('uuid').v4; | ||||
|  | ||||
| test.describe('Scatter Plot', () => { | ||||
|     let scatterPlot; | ||||
|   let scatterPlot; | ||||
|  | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         // Open a browser, navigate to the main page, and wait until all networkevents to resolve | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|   test.beforeEach(async ({ page }) => { | ||||
|     // Open a browser, navigate to the main page, and wait until all networkevents to resolve | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|  | ||||
|         // Create the Scatter Plot | ||||
|         scatterPlot = await createDomainObjectWithDefaults(page, { type: 'Scatter Plot' }); | ||||
|     // Create the Scatter Plot | ||||
|     scatterPlot = await createDomainObjectWithDefaults(page, { type: 'Scatter Plot' }); | ||||
|   }); | ||||
|  | ||||
|   test('Can add and remove telemetry sources', async ({ page }) => { | ||||
|     const editButton = page.locator('button[title="Edit"]'); | ||||
|     const saveButton = page.locator('button[title="Save"]'); | ||||
|  | ||||
|     // Create a sine wave generator within the scatter plot | ||||
|     const swg1 = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Sine Wave Generator', | ||||
|       name: `swg-${uuid()}`, | ||||
|       parent: scatterPlot.uuid | ||||
|     }); | ||||
|  | ||||
|     test('Can add and remove telemetry sources', async ({ page }) => { | ||||
|         const editButton = page.locator('button[title="Edit"]'); | ||||
|         const saveButton = page.locator('button[title="Save"]'); | ||||
|     // Navigate to the scatter plot and verify that | ||||
|     // the SWG appears in the elements pool | ||||
|     await page.goto(scatterPlot.url); | ||||
|     await editButton.click(); | ||||
|     await selectInspectorTab(page, 'Elements'); | ||||
|     await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeVisible(); | ||||
|     await saveButton.click(); | ||||
|     await page.locator('li[title="Save and Finish Editing"]').click(); | ||||
|  | ||||
|         // Create a sine wave generator within the scatter plot | ||||
|         const swg1 = await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Sine Wave Generator', | ||||
|             name: `swg-${uuid()}`, | ||||
|             parent: scatterPlot.uuid | ||||
|         }); | ||||
|  | ||||
|         // Navigate to the scatter plot and verify that | ||||
|         // the SWG appears in the elements pool | ||||
|         await page.goto(scatterPlot.url); | ||||
|         await editButton.click(); | ||||
|         await selectInspectorTab(page, 'Elements'); | ||||
|         await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeVisible(); | ||||
|         await saveButton.click(); | ||||
|         await page.locator('li[title="Save and Finish Editing"]').click(); | ||||
|  | ||||
|         // Create another sine wave generator within the scatter plot | ||||
|         const swg2 = await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Sine Wave Generator', | ||||
|             name: `swg-${uuid()}`, | ||||
|             parent: scatterPlot.uuid | ||||
|         }); | ||||
|  | ||||
|         // Verify that the 'Replace telemetry source' modal appears and accept it | ||||
|         await expect.soft(page.locator('text=This action will replace the current telemetry source. Do you want to continue?')).toBeVisible(); | ||||
|         await page.click('text=Ok'); | ||||
|  | ||||
|         // Navigate to the scatter plot and verify that the new SWG | ||||
|         // appears in the elements pool and the old one is gone | ||||
|         await page.goto(scatterPlot.url); | ||||
|         await editButton.click(); | ||||
|  | ||||
|         // Click the "Elements" tab | ||||
|         await selectInspectorTab(page, 'Elements'); | ||||
|         await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeHidden(); | ||||
|         await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg2.name}`)).toBeVisible(); | ||||
|         await saveButton.click(); | ||||
|  | ||||
|         // Right click on the new SWG in the elements pool and delete it | ||||
|         await page.locator(`#inspector-elements-tree >> text=${swg2.name}`).click({ | ||||
|             button: 'right' | ||||
|         }); | ||||
|         await page.locator('li[title="Remove this object from its containing object."]').click(); | ||||
|  | ||||
|         // Verify that the 'Remove object' confirmation modal appears and accept it | ||||
|         await expect.soft(page.locator('text=Warning! This action will remove this object. Are you sure you want to continue?')).toBeVisible(); | ||||
|         await page.click('text=Ok'); | ||||
|  | ||||
|         // Verify that the elements pool shows no elements | ||||
|         await expect(page.locator('text="No contained elements"')).toBeVisible(); | ||||
|     // Create another sine wave generator within the scatter plot | ||||
|     const swg2 = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Sine Wave Generator', | ||||
|       name: `swg-${uuid()}`, | ||||
|       parent: scatterPlot.uuid | ||||
|     }); | ||||
|  | ||||
|     // Verify that the 'Replace telemetry source' modal appears and accept it | ||||
|     await expect | ||||
|       .soft( | ||||
|         page.locator( | ||||
|           'text=This action will replace the current telemetry source. Do you want to continue?' | ||||
|         ) | ||||
|       ) | ||||
|       .toBeVisible(); | ||||
|     await page.click('text=Ok'); | ||||
|  | ||||
|     // Navigate to the scatter plot and verify that the new SWG | ||||
|     // appears in the elements pool and the old one is gone | ||||
|     await page.goto(scatterPlot.url); | ||||
|     await editButton.click(); | ||||
|  | ||||
|     // Click the "Elements" tab | ||||
|     await selectInspectorTab(page, 'Elements'); | ||||
|     await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg1.name}`)).toBeHidden(); | ||||
|     await expect.soft(page.locator(`#inspector-elements-tree >> text=${swg2.name}`)).toBeVisible(); | ||||
|     await saveButton.click(); | ||||
|  | ||||
|     // Right click on the new SWG in the elements pool and delete it | ||||
|     await page.locator(`#inspector-elements-tree >> text=${swg2.name}`).click({ | ||||
|       button: 'right' | ||||
|     }); | ||||
|     await page.locator('li[title="Remove this object from its containing object."]').click(); | ||||
|  | ||||
|     // Verify that the 'Remove object' confirmation modal appears and accept it | ||||
|     await expect | ||||
|       .soft( | ||||
|         page.locator( | ||||
|           'text=Warning! This action will remove this object. Are you sure you want to continue?' | ||||
|         ) | ||||
|       ) | ||||
|       .toBeVisible(); | ||||
|     await page.click('text=Ok'); | ||||
|  | ||||
|     // Verify that the elements pool shows no elements | ||||
|     await expect(page.locator('text="No contained elements"')).toBeVisible(); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -29,161 +29,202 @@ const { test, expect } = require('../../../../pluginFixtures'); | ||||
| const { createDomainObjectWithDefaults, selectInspectorTab } = require('../../../../appActions'); | ||||
|  | ||||
| test.describe('Stacked Plot', () => { | ||||
|     let stackedPlot; | ||||
|     let swgA; | ||||
|     let swgB; | ||||
|     let swgC; | ||||
|   let stackedPlot; | ||||
|   let swgA; | ||||
|   let swgB; | ||||
|   let swgC; | ||||
|  | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         // Open a browser, navigate to the main page, and wait until all networkevents to resolve | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|   test.beforeEach(async ({ page }) => { | ||||
|     // Open a browser, navigate to the main page, and wait until all networkevents to resolve | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|  | ||||
|         stackedPlot = await createDomainObjectWithDefaults(page, { | ||||
|             type: "Stacked Plot" | ||||
|         }); | ||||
|  | ||||
|         swgA = await createDomainObjectWithDefaults(page, { | ||||
|             type: "Sine Wave Generator", | ||||
|             parent: stackedPlot.uuid | ||||
|         }); | ||||
|         swgB = await createDomainObjectWithDefaults(page, { | ||||
|             type: "Sine Wave Generator", | ||||
|             parent: stackedPlot.uuid | ||||
|         }); | ||||
|         swgC = await createDomainObjectWithDefaults(page, { | ||||
|             type: "Sine Wave Generator", | ||||
|             parent: stackedPlot.uuid | ||||
|         }); | ||||
|     stackedPlot = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Stacked Plot' | ||||
|     }); | ||||
|  | ||||
|     test('Using the remove action removes the correct plot', async ({ page }) => { | ||||
|         const swgAElementsPoolItem = page.locator('#inspector-elements-tree').locator('.c-object-label', { hasText: swgA.name }); | ||||
|         const swgBElementsPoolItem = page.locator('#inspector-elements-tree').locator('.c-object-label', { hasText: swgB.name }); | ||||
|         const swgCElementsPoolItem = page.locator('#inspector-elements-tree').locator('.c-object-label', { hasText: swgC.name }); | ||||
|  | ||||
|         await page.goto(stackedPlot.url); | ||||
|  | ||||
|         await page.click('button[title="Edit"]'); | ||||
|  | ||||
|         await selectInspectorTab(page, 'Elements'); | ||||
|  | ||||
|         await swgBElementsPoolItem.click({ button: 'right' }); | ||||
|         await page.getByRole('menuitem').filter({ hasText: /Remove/ }).click(); | ||||
|         await page.getByRole('button').filter({ hasText: "OK" }).click(); | ||||
|  | ||||
|         await expect(page.locator('#inspector-elements-tree .js-elements-pool__item')).toHaveCount(2); | ||||
|  | ||||
|         // Confirm that the elements pool contains the items we expect | ||||
|         await expect(swgAElementsPoolItem).toHaveCount(1); | ||||
|         await expect(swgBElementsPoolItem).toHaveCount(0); | ||||
|         await expect(swgCElementsPoolItem).toHaveCount(1); | ||||
|     swgA = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Sine Wave Generator', | ||||
|       parent: stackedPlot.uuid | ||||
|     }); | ||||
|  | ||||
|     test('Can reorder Stacked Plot items', async ({ page }) => { | ||||
|         const swgAElementsPoolItem = page.locator('#inspector-elements-tree').locator('.c-object-label', { hasText: swgA.name }); | ||||
|         const swgBElementsPoolItem = page.locator('#inspector-elements-tree').locator('.c-object-label', { hasText: swgB.name }); | ||||
|         const swgCElementsPoolItem = page.locator('#inspector-elements-tree').locator('.c-object-label', { hasText: swgC.name }); | ||||
|  | ||||
|         await page.goto(stackedPlot.url); | ||||
|  | ||||
|         await page.click('button[title="Edit"]'); | ||||
|  | ||||
|         await selectInspectorTab(page, 'Elements'); | ||||
|  | ||||
|         const stackedPlotItem1 = page.locator('.c-plot--stacked-container').nth(0); | ||||
|         const stackedPlotItem2 = page.locator('.c-plot--stacked-container').nth(1); | ||||
|         const stackedPlotItem3 = page.locator('.c-plot--stacked-container').nth(2); | ||||
|  | ||||
|         // assert initial plot order - [swgA, swgB, swgC] | ||||
|         await expect(stackedPlotItem1).toHaveAttribute('aria-label', `Stacked Plot Item ${swgA.name}`); | ||||
|         await expect(stackedPlotItem2).toHaveAttribute('aria-label', `Stacked Plot Item ${swgB.name}`); | ||||
|         await expect(stackedPlotItem3).toHaveAttribute('aria-label', `Stacked Plot Item ${swgC.name}`); | ||||
|  | ||||
|         // Drag and drop to reorder - [swgB, swgA, swgC] | ||||
|         await swgBElementsPoolItem.dragTo(swgAElementsPoolItem); | ||||
|  | ||||
|         // assert plot order after reorder - [swgB, swgA, swgC] | ||||
|         await expect(stackedPlotItem1).toHaveAttribute('aria-label', `Stacked Plot Item ${swgB.name}`); | ||||
|         await expect(stackedPlotItem2).toHaveAttribute('aria-label', `Stacked Plot Item ${swgA.name}`); | ||||
|         await expect(stackedPlotItem3).toHaveAttribute('aria-label', `Stacked Plot Item ${swgC.name}`); | ||||
|  | ||||
|         // Drag and drop to reorder - [swgB, swgC, swgA] | ||||
|         await swgCElementsPoolItem.dragTo(swgAElementsPoolItem); | ||||
|  | ||||
|         // assert plot order after second reorder - [swgB, swgC, swgA] | ||||
|         await expect(stackedPlotItem1).toHaveAttribute('aria-label', `Stacked Plot Item ${swgB.name}`); | ||||
|         await expect(stackedPlotItem2).toHaveAttribute('aria-label', `Stacked Plot Item ${swgC.name}`); | ||||
|         await expect(stackedPlotItem3).toHaveAttribute('aria-label', `Stacked Plot Item ${swgA.name}`); | ||||
|  | ||||
|         // collapse inspector | ||||
|         await page.locator('.l-shell__pane-inspector .l-pane__collapse-button').click(); | ||||
|  | ||||
|         // Save (exit edit mode) | ||||
|         await page.locator('button[title="Save"]').click(); | ||||
|         await page.locator('li[title="Save and Finish Editing"]').click(); | ||||
|  | ||||
|         // assert plot order persists after save - [swgB, swgC, swgA] | ||||
|         await expect(stackedPlotItem1).toHaveAttribute('aria-label', `Stacked Plot Item ${swgB.name}`); | ||||
|         await expect(stackedPlotItem2).toHaveAttribute('aria-label', `Stacked Plot Item ${swgC.name}`); | ||||
|         await expect(stackedPlotItem3).toHaveAttribute('aria-label', `Stacked Plot Item ${swgA.name}`); | ||||
|     swgB = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Sine Wave Generator', | ||||
|       parent: stackedPlot.uuid | ||||
|     }); | ||||
|  | ||||
|     test('Selecting a child plot while in browse and edit modes shows its properties in the inspector', async ({ page }) => { | ||||
|         await page.goto(stackedPlot.url); | ||||
|  | ||||
|         await selectInspectorTab(page, 'Config'); | ||||
|  | ||||
|         // Click on the 1st plot | ||||
|         await page.locator(`[aria-label="Stacked Plot Item ${swgA.name}"] canvas`).nth(1).click(); | ||||
|  | ||||
|         // Assert that the inspector shows the Y Axis properties for swgA | ||||
|         await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText("Plot Series"); | ||||
|         await expect(page.getByRole('heading', { name: "Y Axis" })).toBeVisible(); | ||||
|         await expect(page.locator('[aria-label="Plot Series Properties"] .c-object-label')).toContainText(swgA.name); | ||||
|  | ||||
|         // Click on the 2nd plot | ||||
|         await page.locator(`[aria-label="Stacked Plot Item ${swgB.name}"] canvas`).nth(1).click(); | ||||
|  | ||||
|         // Assert that the inspector shows the Y Axis properties for swgB | ||||
|         await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText("Plot Series"); | ||||
|         await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible(); | ||||
|         await expect(page.locator('[aria-label="Plot Series Properties"] .c-object-label')).toContainText(swgB.name); | ||||
|  | ||||
|         // Click on the 3rd plot | ||||
|         await page.locator(`[aria-label="Stacked Plot Item ${swgC.name}"] canvas`).nth(1).click(); | ||||
|  | ||||
|         // Assert that the inspector shows the Y Axis properties for swgC | ||||
|         await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText("Plot Series"); | ||||
|         await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible(); | ||||
|         await expect(page.locator('[aria-label="Plot Series Properties"] .c-object-label')).toContainText(swgC.name); | ||||
|  | ||||
|         // Go into edit mode | ||||
|         await page.click('button[title="Edit"]'); | ||||
|  | ||||
|         await selectInspectorTab(page, 'Config'); | ||||
|  | ||||
|         // Click on canvas for the 1st plot | ||||
|         await page.locator(`[aria-label="Stacked Plot Item ${swgA.name}"]`).click(); | ||||
|  | ||||
|         // Assert that the inspector shows the Y Axis properties for swgA | ||||
|         await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText("Plot Series"); | ||||
|         await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible(); | ||||
|         await expect(page.locator('[aria-label="Plot Series Properties"] .c-object-label')).toContainText(swgA.name); | ||||
|  | ||||
|         //Click on canvas for the 2nd plot | ||||
|         await page.locator(`[aria-label="Stacked Plot Item ${swgB.name}"]`).click(); | ||||
|  | ||||
|         // Assert that the inspector shows the Y Axis properties for swgB | ||||
|         await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText("Plot Series"); | ||||
|         await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible(); | ||||
|         await expect(page.locator('[aria-label="Plot Series Properties"] .c-object-label')).toContainText(swgB.name); | ||||
|  | ||||
|         //Click on canvas for the 3rd plot | ||||
|         await page.locator(`[aria-label="Stacked Plot Item ${swgC.name}"]`).click(); | ||||
|  | ||||
|         // Assert that the inspector shows the Y Axis properties for swgC | ||||
|         await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText("Plot Series"); | ||||
|         await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible(); | ||||
|         await expect(page.locator('[aria-label="Plot Series Properties"] .c-object-label')).toContainText(swgC.name); | ||||
|     swgC = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Sine Wave Generator', | ||||
|       parent: stackedPlot.uuid | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   test('Using the remove action removes the correct plot', async ({ page }) => { | ||||
|     const swgAElementsPoolItem = page | ||||
|       .locator('#inspector-elements-tree') | ||||
|       .locator('.c-object-label', { hasText: swgA.name }); | ||||
|     const swgBElementsPoolItem = page | ||||
|       .locator('#inspector-elements-tree') | ||||
|       .locator('.c-object-label', { hasText: swgB.name }); | ||||
|     const swgCElementsPoolItem = page | ||||
|       .locator('#inspector-elements-tree') | ||||
|       .locator('.c-object-label', { hasText: swgC.name }); | ||||
|  | ||||
|     await page.goto(stackedPlot.url); | ||||
|  | ||||
|     await page.click('button[title="Edit"]'); | ||||
|  | ||||
|     await selectInspectorTab(page, 'Elements'); | ||||
|  | ||||
|     await swgBElementsPoolItem.click({ button: 'right' }); | ||||
|     await page | ||||
|       .getByRole('menuitem') | ||||
|       .filter({ hasText: /Remove/ }) | ||||
|       .click(); | ||||
|     await page.getByRole('button').filter({ hasText: 'OK' }).click(); | ||||
|  | ||||
|     await expect(page.locator('#inspector-elements-tree .js-elements-pool__item')).toHaveCount(2); | ||||
|  | ||||
|     // Confirm that the elements pool contains the items we expect | ||||
|     await expect(swgAElementsPoolItem).toHaveCount(1); | ||||
|     await expect(swgBElementsPoolItem).toHaveCount(0); | ||||
|     await expect(swgCElementsPoolItem).toHaveCount(1); | ||||
|   }); | ||||
|  | ||||
|   test('Can reorder Stacked Plot items', async ({ page }) => { | ||||
|     const swgAElementsPoolItem = page | ||||
|       .locator('#inspector-elements-tree') | ||||
|       .locator('.c-object-label', { hasText: swgA.name }); | ||||
|     const swgBElementsPoolItem = page | ||||
|       .locator('#inspector-elements-tree') | ||||
|       .locator('.c-object-label', { hasText: swgB.name }); | ||||
|     const swgCElementsPoolItem = page | ||||
|       .locator('#inspector-elements-tree') | ||||
|       .locator('.c-object-label', { hasText: swgC.name }); | ||||
|  | ||||
|     await page.goto(stackedPlot.url); | ||||
|  | ||||
|     await page.click('button[title="Edit"]'); | ||||
|  | ||||
|     await selectInspectorTab(page, 'Elements'); | ||||
|  | ||||
|     const stackedPlotItem1 = page.locator('.c-plot--stacked-container').nth(0); | ||||
|     const stackedPlotItem2 = page.locator('.c-plot--stacked-container').nth(1); | ||||
|     const stackedPlotItem3 = page.locator('.c-plot--stacked-container').nth(2); | ||||
|  | ||||
|     // assert initial plot order - [swgA, swgB, swgC] | ||||
|     await expect(stackedPlotItem1).toHaveAttribute('aria-label', `Stacked Plot Item ${swgA.name}`); | ||||
|     await expect(stackedPlotItem2).toHaveAttribute('aria-label', `Stacked Plot Item ${swgB.name}`); | ||||
|     await expect(stackedPlotItem3).toHaveAttribute('aria-label', `Stacked Plot Item ${swgC.name}`); | ||||
|  | ||||
|     // Drag and drop to reorder - [swgB, swgA, swgC] | ||||
|     await swgBElementsPoolItem.dragTo(swgAElementsPoolItem); | ||||
|  | ||||
|     // assert plot order after reorder - [swgB, swgA, swgC] | ||||
|     await expect(stackedPlotItem1).toHaveAttribute('aria-label', `Stacked Plot Item ${swgB.name}`); | ||||
|     await expect(stackedPlotItem2).toHaveAttribute('aria-label', `Stacked Plot Item ${swgA.name}`); | ||||
|     await expect(stackedPlotItem3).toHaveAttribute('aria-label', `Stacked Plot Item ${swgC.name}`); | ||||
|  | ||||
|     // Drag and drop to reorder - [swgB, swgC, swgA] | ||||
|     await swgCElementsPoolItem.dragTo(swgAElementsPoolItem); | ||||
|  | ||||
|     // assert plot order after second reorder - [swgB, swgC, swgA] | ||||
|     await expect(stackedPlotItem1).toHaveAttribute('aria-label', `Stacked Plot Item ${swgB.name}`); | ||||
|     await expect(stackedPlotItem2).toHaveAttribute('aria-label', `Stacked Plot Item ${swgC.name}`); | ||||
|     await expect(stackedPlotItem3).toHaveAttribute('aria-label', `Stacked Plot Item ${swgA.name}`); | ||||
|  | ||||
|     // collapse inspector | ||||
|     await page.locator('.l-shell__pane-inspector .l-pane__collapse-button').click(); | ||||
|  | ||||
|     // Save (exit edit mode) | ||||
|     await page.locator('button[title="Save"]').click(); | ||||
|     await page.locator('li[title="Save and Finish Editing"]').click(); | ||||
|  | ||||
|     // assert plot order persists after save - [swgB, swgC, swgA] | ||||
|     await expect(stackedPlotItem1).toHaveAttribute('aria-label', `Stacked Plot Item ${swgB.name}`); | ||||
|     await expect(stackedPlotItem2).toHaveAttribute('aria-label', `Stacked Plot Item ${swgC.name}`); | ||||
|     await expect(stackedPlotItem3).toHaveAttribute('aria-label', `Stacked Plot Item ${swgA.name}`); | ||||
|   }); | ||||
|  | ||||
|   test('Selecting a child plot while in browse and edit modes shows its properties in the inspector', async ({ | ||||
|     page | ||||
|   }) => { | ||||
|     await page.goto(stackedPlot.url); | ||||
|  | ||||
|     await selectInspectorTab(page, 'Config'); | ||||
|  | ||||
|     // Click on the 1st plot | ||||
|     await page.locator(`[aria-label="Stacked Plot Item ${swgA.name}"] canvas`).nth(1).click(); | ||||
|  | ||||
|     // Assert that the inspector shows the Y Axis properties for swgA | ||||
|     await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText( | ||||
|       'Plot Series' | ||||
|     ); | ||||
|     await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible(); | ||||
|     await expect( | ||||
|       page.locator('[aria-label="Plot Series Properties"] .c-object-label') | ||||
|     ).toContainText(swgA.name); | ||||
|  | ||||
|     // Click on the 2nd plot | ||||
|     await page.locator(`[aria-label="Stacked Plot Item ${swgB.name}"] canvas`).nth(1).click(); | ||||
|  | ||||
|     // Assert that the inspector shows the Y Axis properties for swgB | ||||
|     await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText( | ||||
|       'Plot Series' | ||||
|     ); | ||||
|     await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible(); | ||||
|     await expect( | ||||
|       page.locator('[aria-label="Plot Series Properties"] .c-object-label') | ||||
|     ).toContainText(swgB.name); | ||||
|  | ||||
|     // Click on the 3rd plot | ||||
|     await page.locator(`[aria-label="Stacked Plot Item ${swgC.name}"] canvas`).nth(1).click(); | ||||
|  | ||||
|     // Assert that the inspector shows the Y Axis properties for swgC | ||||
|     await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText( | ||||
|       'Plot Series' | ||||
|     ); | ||||
|     await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible(); | ||||
|     await expect( | ||||
|       page.locator('[aria-label="Plot Series Properties"] .c-object-label') | ||||
|     ).toContainText(swgC.name); | ||||
|  | ||||
|     // Go into edit mode | ||||
|     await page.click('button[title="Edit"]'); | ||||
|  | ||||
|     await selectInspectorTab(page, 'Config'); | ||||
|  | ||||
|     // Click on canvas for the 1st plot | ||||
|     await page.locator(`[aria-label="Stacked Plot Item ${swgA.name}"]`).click(); | ||||
|  | ||||
|     // Assert that the inspector shows the Y Axis properties for swgA | ||||
|     await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText( | ||||
|       'Plot Series' | ||||
|     ); | ||||
|     await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible(); | ||||
|     await expect( | ||||
|       page.locator('[aria-label="Plot Series Properties"] .c-object-label') | ||||
|     ).toContainText(swgA.name); | ||||
|  | ||||
|     //Click on canvas for the 2nd plot | ||||
|     await page.locator(`[aria-label="Stacked Plot Item ${swgB.name}"]`).click(); | ||||
|  | ||||
|     // Assert that the inspector shows the Y Axis properties for swgB | ||||
|     await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText( | ||||
|       'Plot Series' | ||||
|     ); | ||||
|     await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible(); | ||||
|     await expect( | ||||
|       page.locator('[aria-label="Plot Series Properties"] .c-object-label') | ||||
|     ).toContainText(swgB.name); | ||||
|  | ||||
|     //Click on canvas for the 3rd plot | ||||
|     await page.locator(`[aria-label="Stacked Plot Item ${swgC.name}"]`).click(); | ||||
|  | ||||
|     // Assert that the inspector shows the Y Axis properties for swgC | ||||
|     await expect(page.locator('[aria-label="Plot Series Properties"] >> h2')).toContainText( | ||||
|       'Plot Series' | ||||
|     ); | ||||
|     await expect(page.getByRole('heading', { name: 'Y Axis' })).toBeVisible(); | ||||
|     await expect( | ||||
|       page.locator('[aria-label="Plot Series Properties"] .c-object-label') | ||||
|     ).toContainText(swgC.name); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -25,243 +25,250 @@ Tests to verify plot tagging functionality. | ||||
| */ | ||||
|  | ||||
| const { test, expect } = require('../../../../pluginFixtures'); | ||||
| const { createDomainObjectWithDefaults, setRealTimeMode, setFixedTimeMode } = require('../../../../appActions'); | ||||
| const { | ||||
|   createDomainObjectWithDefaults, | ||||
|   setRealTimeMode, | ||||
|   setFixedTimeMode, | ||||
|   waitForPlotsToRender | ||||
| } = require('../../../../appActions'); | ||||
|  | ||||
| test.describe('Plot Tagging', () => { | ||||
|     /** | ||||
|      * Given a canvas and a set of points, tags the points on the canvas. | ||||
|      * @param {import('@playwright/test').Page} page | ||||
|      * @param {HTMLCanvasElement} canvas a telemetry item with a plot | ||||
|      * @param {Number} xEnd a telemetry item with a plot | ||||
|      * @param {Number} yEnd a telemetry item with a plot | ||||
|      * @returns {Promise} | ||||
|      */ | ||||
|     async function createTags({page, canvas, xEnd, yEnd}) { | ||||
|         await canvas.hover({trial: true}); | ||||
|   /** | ||||
|    * Given a canvas and a set of points, tags the points on the canvas. | ||||
|    * @param {import('@playwright/test').Page} page | ||||
|    * @param {HTMLCanvasElement} canvas a telemetry item with a plot | ||||
|    * @param {Number} xEnd a telemetry item with a plot | ||||
|    * @param {Number} yEnd a telemetry item with a plot | ||||
|    * @returns {Promise} | ||||
|    */ | ||||
|   async function createTags({ page, canvas, xEnd, yEnd }) { | ||||
|     await canvas.hover({ trial: true }); | ||||
|  | ||||
|         //Alt+Shift Drag Start to select some points to tag | ||||
|         await page.keyboard.down('Alt'); | ||||
|         await page.keyboard.down('Shift'); | ||||
|     //Alt+Shift Drag Start to select some points to tag | ||||
|     await page.keyboard.down('Alt'); | ||||
|     await page.keyboard.down('Shift'); | ||||
|  | ||||
|         await canvas.dragTo(canvas, { | ||||
|             sourcePosition: { | ||||
|                 x: 1, | ||||
|                 y: 1 | ||||
|             }, | ||||
|             targetPosition: { | ||||
|                 x: xEnd, | ||||
|                 y: yEnd | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         //Alt Drag End | ||||
|         await page.keyboard.up('Alt'); | ||||
|         await page.keyboard.up('Shift'); | ||||
|  | ||||
|         //Wait for canvas to stablize. | ||||
|         await canvas.hover({trial: true}); | ||||
|  | ||||
|         // add some tags | ||||
|         await page.getByText('Annotations').click(); | ||||
|         await page.getByRole('button', { name: /Add Tag/ }).click(); | ||||
|         await page.getByPlaceholder('Type to select tag').click(); | ||||
|         await page.getByText('Driving').click(); | ||||
|  | ||||
|         await page.getByRole('button', { name: /Add Tag/ }).click(); | ||||
|         await page.getByPlaceholder('Type to select tag').click(); | ||||
|         await page.getByText('Science').click(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Given a telemetry item (e.g., a Sine Wave Generator) with a plot, tests that the plot can be tagged. | ||||
|      * @param {import('@playwright/test').Page} page | ||||
|      * @param {import('../../../../appActions').CreatedObjectInfo} telemetryItem a telemetry item with a plot | ||||
|      * @returns {Promise} | ||||
|      */ | ||||
|     async function testTelemetryItem(page, telemetryItem) { | ||||
|         // Check that telemetry item also received the tag | ||||
|         await page.goto(telemetryItem.url); | ||||
|  | ||||
|         await expect(page.getByText('No tags to display for this item')).toBeVisible(); | ||||
|  | ||||
|         const canvas = page.locator('canvas').nth(1); | ||||
|  | ||||
|         //Wait for canvas to stablize. | ||||
|         await canvas.hover({trial: true}); | ||||
|  | ||||
|         // click on the tagged plot point | ||||
|         await canvas.click({ | ||||
|             position: { | ||||
|                 x: 325, | ||||
|                 y: 377 | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         await expect(page.getByText('Science')).toBeVisible(); | ||||
|         await expect(page.getByText('Driving')).toBeHidden(); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Given a page, tests that tags are searchable, deletable, and persist across reloads. | ||||
|      * @param {import('@playwright/test').Page} page | ||||
|      * @returns {Promise} | ||||
|      */ | ||||
|     async function basicTagsTests(page) { | ||||
|         // Search for Driving | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); | ||||
|  | ||||
|         // Clicking elsewhere should cause annotation selection to be cleared | ||||
|         await expect(page.getByText('No tags to display for this item')).toBeVisible(); | ||||
|  | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('driv'); | ||||
|         // click on the search result | ||||
|         await page.getByRole('searchbox', { name: 'OpenMCT Search' }).getByText(/Sine Wave/).first().click(); | ||||
|  | ||||
|         // Delete Driving | ||||
|         await page.hover('[aria-label="Tag"]:has-text("Driving")'); | ||||
|         await page.locator('[aria-label="Remove tag Driving"]').click(); | ||||
|  | ||||
|         // Search for Science | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc'); | ||||
|         await expect(page.locator('[aria-label="Search Result"]').nth(0)).toContainText("Science"); | ||||
|         await expect(page.locator('[aria-label="Search Result"]').nth(0)).not.toContainText("Drilling"); | ||||
|  | ||||
|         // Search for Driving | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('driv'); | ||||
|         await expect(page.getByText('No results found')).toBeVisible(); | ||||
|  | ||||
|         //Reload Page | ||||
|         await Promise.all([ | ||||
|             page.reload(), | ||||
|             page.waitForLoadState('networkidle') | ||||
|         ]); | ||||
|         // wait for plots to load | ||||
|         await expect(page.locator('.js-series-data-loaded')).toBeVisible(); | ||||
|  | ||||
|         await page.getByText('Annotations').click(); | ||||
|         await expect(page.getByText('No tags to display for this item')).toBeVisible(); | ||||
|  | ||||
|         const canvas = page.locator('canvas').nth(1); | ||||
|         // click on the tagged plot point | ||||
|         await canvas.click({ | ||||
|             position: { | ||||
|                 x: 100, | ||||
|                 y: 100 | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         await expect(page.getByText('Science')).toBeVisible(); | ||||
|         await expect(page.getByText('Driving')).toBeHidden(); | ||||
|     } | ||||
|  | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|     await canvas.dragTo(canvas, { | ||||
|       sourcePosition: { | ||||
|         x: 1, | ||||
|         y: 1 | ||||
|       }, | ||||
|       targetPosition: { | ||||
|         x: xEnd, | ||||
|         y: yEnd | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     test('Tags work with Overlay Plots', async ({ page }) => { | ||||
|         //Test.slow decorator is currently broken. Needs to be fixed in https://github.com/nasa/openmct/issues/5374 | ||||
|         test.slow(); | ||||
|     //Alt Drag End | ||||
|     await page.keyboard.up('Alt'); | ||||
|     await page.keyboard.up('Shift'); | ||||
|  | ||||
|         const overlayPlot = await createDomainObjectWithDefaults(page, { | ||||
|             type: "Overlay Plot" | ||||
|         }); | ||||
|     //Wait for canvas to stablize. | ||||
|     await canvas.hover({ trial: true }); | ||||
|  | ||||
|         const alphaSineWave = await createDomainObjectWithDefaults(page, { | ||||
|             type: "Sine Wave Generator", | ||||
|             name: "Alpha Sine Wave", | ||||
|             parent: overlayPlot.uuid | ||||
|         }); | ||||
|     // add some tags | ||||
|     await page.getByText('Annotations').click(); | ||||
|     await page.getByRole('button', { name: /Add Tag/ }).click(); | ||||
|     await page.getByPlaceholder('Type to select tag').click(); | ||||
|     await page.getByText('Driving').click(); | ||||
|  | ||||
|         await createDomainObjectWithDefaults(page, { | ||||
|             type: "Sine Wave Generator", | ||||
|             name: "Beta Sine Wave", | ||||
|             parent: overlayPlot.uuid | ||||
|         }); | ||||
|     await page.getByRole('button', { name: /Add Tag/ }).click(); | ||||
|     await page.getByPlaceholder('Type to select tag').click(); | ||||
|     await page.getByText('Science').click(); | ||||
|   } | ||||
|  | ||||
|         await page.goto(overlayPlot.url); | ||||
|   /** | ||||
|    * Given a telemetry item (e.g., a Sine Wave Generator) with a plot, tests that the plot can be tagged. | ||||
|    * @param {import('@playwright/test').Page} page | ||||
|    * @param {import('../../../../appActions').CreatedObjectInfo} telemetryItem a telemetry item with a plot | ||||
|    * @returns {Promise} | ||||
|    */ | ||||
|   async function testTelemetryItem(page, telemetryItem) { | ||||
|     // Check that telemetry item also received the tag | ||||
|     await page.goto(telemetryItem.url); | ||||
|  | ||||
|         let canvas = page.locator('canvas').nth(1); | ||||
|     await expect(page.getByText('No tags to display for this item')).toBeVisible(); | ||||
|  | ||||
|         // Switch to real-time mode | ||||
|         // Adding tags should pause the plot | ||||
|         await setRealTimeMode(page); | ||||
|     const canvas = page.locator('canvas').nth(1); | ||||
|  | ||||
|         await createTags({ | ||||
|             page, | ||||
|             canvas, | ||||
|             xEnd: 700, | ||||
|             yEnd: 480 | ||||
|         }); | ||||
|     //Wait for canvas to stablize. | ||||
|     await canvas.hover({ trial: true }); | ||||
|  | ||||
|         await setFixedTimeMode(page); | ||||
|  | ||||
|         // changing to fixed time mode rebuilds canvas? | ||||
|         canvas = page.locator('canvas').nth(1); | ||||
|  | ||||
|         await basicTagsTests(page); | ||||
|         await testTelemetryItem(page, alphaSineWave); | ||||
|  | ||||
|         // set to real time mode | ||||
|         await setRealTimeMode(page); | ||||
|  | ||||
|         // Search for Science | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc'); | ||||
|         // click on the search result | ||||
|         await page.getByRole('searchbox', { name: 'OpenMCT Search' }).getByText('Alpha Sine Wave').first().click(); | ||||
|         // wait for plots to load | ||||
|         await expect(page.locator('.js-series-data-loaded')).toBeVisible(); | ||||
|         // expect plot to be paused | ||||
|         await expect(page.locator('[title="Resume displaying real-time data"]')).toBeVisible(); | ||||
|  | ||||
|         await setFixedTimeMode(page); | ||||
|     // click on the tagged plot point | ||||
|     await canvas.click({ | ||||
|       position: { | ||||
|         x: 325, | ||||
|         y: 377 | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     test('Tags work with Plot View of telemetry items', async ({ page }) => { | ||||
|         await createDomainObjectWithDefaults(page, { | ||||
|             type: "Sine Wave Generator" | ||||
|         }); | ||||
|     await expect(page.getByText('Science')).toBeVisible(); | ||||
|     await expect(page.getByText('Driving')).toBeHidden(); | ||||
|   } | ||||
|  | ||||
|         const canvas = page.locator('canvas').nth(1); | ||||
|         await createTags({ | ||||
|             page, | ||||
|             canvas, | ||||
|             xEnd: 700, | ||||
|             yEnd: 480 | ||||
|         }); | ||||
|         await basicTagsTests(page); | ||||
|   /** | ||||
|    * Given a page, tests that tags are searchable, deletable, and persist across reloads. | ||||
|    * @param {import('@playwright/test').Page} page | ||||
|    * @returns {Promise} | ||||
|    */ | ||||
|   async function basicTagsTests(page) { | ||||
|     // Search for Driving | ||||
|     await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); | ||||
|  | ||||
|     // Clicking elsewhere should cause annotation selection to be cleared | ||||
|     await expect(page.getByText('No tags to display for this item')).toBeVisible(); | ||||
|  | ||||
|     await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('driv'); | ||||
|     // click on the search result | ||||
|     await page | ||||
|       .getByRole('searchbox', { name: 'OpenMCT Search' }) | ||||
|       .getByText(/Sine Wave/) | ||||
|       .first() | ||||
|       .click(); | ||||
|  | ||||
|     // Delete Driving | ||||
|     await page.hover('[aria-label="Tag"]:has-text("Driving")'); | ||||
|     await page.locator('[aria-label="Remove tag Driving"]').click(); | ||||
|  | ||||
|     // Search for Science | ||||
|     await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); | ||||
|     await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc'); | ||||
|     await expect(page.locator('[aria-label="Search Result"]').nth(0)).toContainText('Science'); | ||||
|     await expect(page.locator('[aria-label="Search Result"]').nth(0)).not.toContainText('Drilling'); | ||||
|  | ||||
|     // Search for Driving | ||||
|     await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); | ||||
|     await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('driv'); | ||||
|     await expect(page.getByText('No results found')).toBeVisible(); | ||||
|  | ||||
|     //Reload Page | ||||
|     await page.reload({ waitUntil: 'domcontentloaded' }); | ||||
|     // wait for plots to load | ||||
|     await waitForPlotsToRender(page); | ||||
|  | ||||
|     await page.getByText('Annotations').click(); | ||||
|     await expect(page.getByText('No tags to display for this item')).toBeVisible(); | ||||
|  | ||||
|     const canvas = page.locator('canvas').nth(1); | ||||
|     // click on the tagged plot point | ||||
|     await canvas.click({ | ||||
|       position: { | ||||
|         x: 100, | ||||
|         y: 100 | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     test('Tags work with Stacked Plots', async ({ page }) => { | ||||
|         const stackedPlot = await createDomainObjectWithDefaults(page, { | ||||
|             type: "Stacked Plot" | ||||
|         }); | ||||
|     await expect(page.getByText('Science')).toBeVisible(); | ||||
|     await expect(page.getByText('Driving')).toBeHidden(); | ||||
|   } | ||||
|  | ||||
|         const alphaSineWave = await createDomainObjectWithDefaults(page, { | ||||
|             type: "Sine Wave Generator", | ||||
|             name: "Alpha Sine Wave", | ||||
|             parent: stackedPlot.uuid | ||||
|         }); | ||||
|   test.beforeEach(async ({ page }) => { | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|   }); | ||||
|  | ||||
|         await createDomainObjectWithDefaults(page, { | ||||
|             type: "Sine Wave Generator", | ||||
|             name: "Beta Sine Wave", | ||||
|             parent: stackedPlot.uuid | ||||
|         }); | ||||
|   test('Tags work with Overlay Plots', async ({ page }) => { | ||||
|     //Test.slow decorator is currently broken. Needs to be fixed in https://github.com/nasa/openmct/issues/5374 | ||||
|     test.slow(); | ||||
|  | ||||
|         await page.goto(stackedPlot.url); | ||||
|  | ||||
|         const canvas = page.locator('canvas').nth(1); | ||||
|  | ||||
|         await createTags({ | ||||
|             page, | ||||
|             canvas, | ||||
|             xEnd: 700, | ||||
|             yEnd: 215 | ||||
|         }); | ||||
|         await basicTagsTests(page); | ||||
|         await testTelemetryItem(page, alphaSineWave); | ||||
|     const overlayPlot = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Overlay Plot' | ||||
|     }); | ||||
|  | ||||
|     const alphaSineWave = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Sine Wave Generator', | ||||
|       name: 'Alpha Sine Wave', | ||||
|       parent: overlayPlot.uuid | ||||
|     }); | ||||
|  | ||||
|     await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Sine Wave Generator', | ||||
|       name: 'Beta Sine Wave', | ||||
|       parent: overlayPlot.uuid | ||||
|     }); | ||||
|  | ||||
|     await page.goto(overlayPlot.url); | ||||
|  | ||||
|     let canvas = page.locator('canvas').nth(1); | ||||
|  | ||||
|     // Switch to real-time mode | ||||
|     // Adding tags should pause the plot | ||||
|     await setRealTimeMode(page); | ||||
|  | ||||
|     await createTags({ | ||||
|       page, | ||||
|       canvas, | ||||
|       xEnd: 700, | ||||
|       yEnd: 480 | ||||
|     }); | ||||
|  | ||||
|     await setFixedTimeMode(page); | ||||
|  | ||||
|     await basicTagsTests(page); | ||||
|     await testTelemetryItem(page, alphaSineWave); | ||||
|  | ||||
|     // set to real time mode | ||||
|     await setRealTimeMode(page); | ||||
|  | ||||
|     // Search for Science | ||||
|     await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); | ||||
|     await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc'); | ||||
|     // click on the search result | ||||
|     await page | ||||
|       .getByRole('searchbox', { name: 'OpenMCT Search' }) | ||||
|       .getByText('Alpha Sine Wave') | ||||
|       .first() | ||||
|       .click(); | ||||
|     // wait for plots to load | ||||
|     await expect(page.locator('.js-series-data-loaded')).toBeVisible(); | ||||
|     // expect plot to be paused | ||||
|     await expect(page.locator('[title="Resume displaying real-time data"]')).toBeVisible(); | ||||
|  | ||||
|     await setFixedTimeMode(page); | ||||
|   }); | ||||
|  | ||||
|   test('Tags work with Plot View of telemetry items', async ({ page }) => { | ||||
|     await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Sine Wave Generator' | ||||
|     }); | ||||
|  | ||||
|     const canvas = page.locator('canvas').nth(1); | ||||
|     await createTags({ | ||||
|       page, | ||||
|       canvas, | ||||
|       xEnd: 700, | ||||
|       yEnd: 480 | ||||
|     }); | ||||
|     await basicTagsTests(page); | ||||
|   }); | ||||
|  | ||||
|   test('Tags work with Stacked Plots', async ({ page }) => { | ||||
|     const stackedPlot = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Stacked Plot' | ||||
|     }); | ||||
|  | ||||
|     const alphaSineWave = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Sine Wave Generator', | ||||
|       name: 'Alpha Sine Wave', | ||||
|       parent: stackedPlot.uuid | ||||
|     }); | ||||
|  | ||||
|     await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Sine Wave Generator', | ||||
|       name: 'Beta Sine Wave', | ||||
|       parent: stackedPlot.uuid | ||||
|     }); | ||||
|  | ||||
|     await page.goto(stackedPlot.url); | ||||
|  | ||||
|     const canvas = page.locator('canvas').nth(1); | ||||
|  | ||||
|     await createTags({ | ||||
|       page, | ||||
|       canvas, | ||||
|       xEnd: 700, | ||||
|       yEnd: 215 | ||||
|     }); | ||||
|     await basicTagsTests(page); | ||||
|     await testTelemetryItem(page, alphaSineWave); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -24,52 +24,59 @@ const { createDomainObjectWithDefaults } = require('../../../../appActions'); | ||||
| const { test, expect } = require('../../../../pluginFixtures'); | ||||
|  | ||||
| test.describe('Telemetry Table', () => { | ||||
|     test('unpauses and filters data when paused by button and user changes bounds', async ({ page }) => { | ||||
|         test.info().annotations.push({ | ||||
|             type: 'issue', | ||||
|             description: 'https://github.com/nasa/openmct/issues/5113' | ||||
|         }); | ||||
|  | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|  | ||||
|         const table = await createDomainObjectWithDefaults(page, { type: 'Telemetry Table' }); | ||||
|         await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Sine Wave Generator', | ||||
|             parent: table.uuid | ||||
|         }); | ||||
|  | ||||
|         // focus the Telemetry Table | ||||
|         page.goto(table.url); | ||||
|  | ||||
|         // Click pause button | ||||
|         const pauseButton = page.locator('button.c-button.icon-pause'); | ||||
|         await pauseButton.click(); | ||||
|  | ||||
|         const tableWrapper = page.locator('div.c-table-wrapper'); | ||||
|         await expect(tableWrapper).toHaveClass(/is-paused/); | ||||
|  | ||||
|         // Subtract 5 minutes from the current end bound datetime and set it | ||||
|         const endTimeInput = page.locator('input[type="text"].c-input--datetime').nth(1); | ||||
|         await endTimeInput.click(); | ||||
|  | ||||
|         let endDate = await endTimeInput.inputValue(); | ||||
|         endDate = new Date(endDate); | ||||
|  | ||||
|         endDate.setUTCMinutes(endDate.getUTCMinutes() - 5); | ||||
|         endDate = endDate.toISOString().replace(/T/, ' '); | ||||
|  | ||||
|         await endTimeInput.fill(''); | ||||
|         await endTimeInput.fill(endDate); | ||||
|         await page.keyboard.press('Enter'); | ||||
|  | ||||
|         await expect(tableWrapper).not.toHaveClass(/is-paused/); | ||||
|  | ||||
|         // Get the most recent telemetry date | ||||
|         const latestTelemetryDate = await page.locator('table.c-telemetry-table__body > tbody > tr').last().locator('td').nth(1).getAttribute('title'); | ||||
|  | ||||
|         // Verify that it is <= our new end bound | ||||
|         const latestMilliseconds = Date.parse(latestTelemetryDate); | ||||
|         const endBoundMilliseconds = Date.parse(endDate); | ||||
|         expect(latestMilliseconds).toBeLessThanOrEqual(endBoundMilliseconds); | ||||
|   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' | ||||
|     }); | ||||
|  | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|  | ||||
|     const table = await createDomainObjectWithDefaults(page, { type: 'Telemetry Table' }); | ||||
|     await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Sine Wave Generator', | ||||
|       parent: table.uuid | ||||
|     }); | ||||
|  | ||||
|     // focus the Telemetry Table | ||||
|     page.goto(table.url); | ||||
|  | ||||
|     // Click pause button | ||||
|     const pauseButton = page.locator('button.c-button.icon-pause'); | ||||
|     await pauseButton.click(); | ||||
|  | ||||
|     const tableWrapper = page.locator('div.c-table-wrapper'); | ||||
|     await expect(tableWrapper).toHaveClass(/is-paused/); | ||||
|  | ||||
|     // Subtract 5 minutes from the current end bound datetime and set it | ||||
|     const endTimeInput = page.locator('input[type="text"].c-input--datetime').nth(1); | ||||
|     await endTimeInput.click(); | ||||
|  | ||||
|     let endDate = await endTimeInput.inputValue(); | ||||
|     endDate = new Date(endDate); | ||||
|  | ||||
|     endDate.setUTCMinutes(endDate.getUTCMinutes() - 5); | ||||
|     endDate = endDate.toISOString().replace(/T/, ' '); | ||||
|  | ||||
|     await endTimeInput.fill(''); | ||||
|     await endTimeInput.fill(endDate); | ||||
|     await page.keyboard.press('Enter'); | ||||
|  | ||||
|     await expect(tableWrapper).not.toHaveClass(/is-paused/); | ||||
|  | ||||
|     // Get the most recent telemetry date | ||||
|     const latestTelemetryDate = await page | ||||
|       .locator('table.c-telemetry-table__body > tbody > tr') | ||||
|       .last() | ||||
|       .locator('td') | ||||
|       .nth(1) | ||||
|       .getAttribute('title'); | ||||
|  | ||||
|     // Verify that it is <= our new end bound | ||||
|     const latestMilliseconds = Date.parse(latestTelemetryDate); | ||||
|     const endBoundMilliseconds = Date.parse(endDate); | ||||
|     expect(latestMilliseconds).toBeLessThanOrEqual(endBoundMilliseconds); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -21,170 +21,200 @@ | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| const { test, expect } = require('../../../../pluginFixtures'); | ||||
| const { setFixedTimeMode, setRealTimeMode, setStartOffset, setEndOffset } = require('../../../../appActions'); | ||||
| const { | ||||
|   setFixedTimeMode, | ||||
|   setRealTimeMode, | ||||
|   setStartOffset, | ||||
|   setEndOffset | ||||
| } = require('../../../../appActions'); | ||||
|  | ||||
| test.describe('Time conductor operations', () => { | ||||
|     test('validate start time does not exceeds end time', async ({ page }) => { | ||||
|         // Go to baseURL | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|         const year = new Date().getFullYear(); | ||||
|   test('validate start time does not exceeds end time', async ({ page }) => { | ||||
|     // Go to baseURL | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|     const year = new Date().getFullYear(); | ||||
|  | ||||
|         let startDate = 'xxxx-01-01 01:00:00.000Z'; | ||||
|         startDate = year + startDate.substring(4); | ||||
|     let startDate = 'xxxx-01-01 01:00:00.000Z'; | ||||
|     startDate = year + startDate.substring(4); | ||||
|  | ||||
|         let endDate = 'xxxx-01-01 02:00:00.000Z'; | ||||
|         endDate = year + endDate.substring(4); | ||||
|     let endDate = 'xxxx-01-01 02:00:00.000Z'; | ||||
|     endDate = year + endDate.substring(4); | ||||
|  | ||||
|         const startTimeLocator = page.locator('input[type="text"]').first(); | ||||
|         const endTimeLocator = page.locator('input[type="text"]').nth(1); | ||||
|     const startTimeLocator = page.locator('input[type="text"]').first(); | ||||
|     const endTimeLocator = page.locator('input[type="text"]').nth(1); | ||||
|  | ||||
|         // Click start time | ||||
|         await startTimeLocator.click(); | ||||
|     // Click start time | ||||
|     await startTimeLocator.click(); | ||||
|  | ||||
|         // Click end time | ||||
|         await endTimeLocator.click(); | ||||
|     // Click end time | ||||
|     await endTimeLocator.click(); | ||||
|  | ||||
|         await endTimeLocator.fill(endDate.toString()); | ||||
|         await startTimeLocator.fill(startDate.toString()); | ||||
|     await endTimeLocator.fill(endDate.toString()); | ||||
|     await startTimeLocator.fill(startDate.toString()); | ||||
|  | ||||
|         // invalid start date | ||||
|         startDate = (year + 1) + startDate.substring(4); | ||||
|         await startTimeLocator.fill(startDate.toString()); | ||||
|         await endTimeLocator.click(); | ||||
|     // invalid start date | ||||
|     startDate = year + 1 + startDate.substring(4); | ||||
|     await startTimeLocator.fill(startDate.toString()); | ||||
|     await endTimeLocator.click(); | ||||
|  | ||||
|         const startDateValidityStatus = await startTimeLocator.evaluate((element) => element.checkValidity()); | ||||
|         expect(startDateValidityStatus).not.toBeTruthy(); | ||||
|     const startDateValidityStatus = await startTimeLocator.evaluate((element) => | ||||
|       element.checkValidity() | ||||
|     ); | ||||
|     expect(startDateValidityStatus).not.toBeTruthy(); | ||||
|  | ||||
|         // fix to valid start date | ||||
|         startDate = (year - 1) + startDate.substring(4); | ||||
|         await startTimeLocator.fill(startDate.toString()); | ||||
|     // fix to valid start date | ||||
|     startDate = year - 1 + startDate.substring(4); | ||||
|     await startTimeLocator.fill(startDate.toString()); | ||||
|  | ||||
|         // invalid end date | ||||
|         endDate = (year - 2) + endDate.substring(4); | ||||
|         await endTimeLocator.fill(endDate.toString()); | ||||
|         await startTimeLocator.click(); | ||||
|     // invalid end date | ||||
|     endDate = year - 2 + endDate.substring(4); | ||||
|     await endTimeLocator.fill(endDate.toString()); | ||||
|     await startTimeLocator.click(); | ||||
|  | ||||
|         const endDateValidityStatus = await endTimeLocator.evaluate((element) => element.checkValidity()); | ||||
|         expect(endDateValidityStatus).not.toBeTruthy(); | ||||
|     }); | ||||
|     const endDateValidityStatus = await endTimeLocator.evaluate((element) => | ||||
|       element.checkValidity() | ||||
|     ); | ||||
|     expect(endDateValidityStatus).not.toBeTruthy(); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| // Testing instructions: | ||||
| // Try to change the realtime offsets when in realtime (local clock) mode. | ||||
| test.describe('Time conductor input fields real-time mode', () => { | ||||
|     test('validate input fields in real-time mode', async ({ page }) => { | ||||
|         const startOffset = { | ||||
|             secs: '23' | ||||
|         }; | ||||
|   test('validate input fields in real-time mode', async ({ page }) => { | ||||
|     const startOffset = { | ||||
|       secs: '23' | ||||
|     }; | ||||
|  | ||||
|         const endOffset = { | ||||
|             secs: '31' | ||||
|         }; | ||||
|     const endOffset = { | ||||
|       secs: '31' | ||||
|     }; | ||||
|  | ||||
|         // Go to baseURL | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|     // Go to baseURL | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|  | ||||
|         // Switch to real-time mode | ||||
|         await setRealTimeMode(page); | ||||
|     // Switch to real-time mode | ||||
|     await setRealTimeMode(page); | ||||
|  | ||||
|         // Set start time offset | ||||
|         await setStartOffset(page, startOffset); | ||||
|     // Set start time offset | ||||
|     await setStartOffset(page, startOffset); | ||||
|  | ||||
|         // Verify time was updated on time offset button | ||||
|         await expect(page.locator('data-testid=conductor-start-offset-button')).toContainText('00:30:23'); | ||||
|     // Verify time was updated on time offset button | ||||
|     await expect(page.locator('data-testid=conductor-start-offset-button')).toContainText( | ||||
|       '00:30:23' | ||||
|     ); | ||||
|  | ||||
|         // Set end time offset | ||||
|         await setEndOffset(page, endOffset); | ||||
|     // Set end time offset | ||||
|     await setEndOffset(page, endOffset); | ||||
|  | ||||
|         // Verify time was updated on preceding time offset button | ||||
|         await expect(page.locator('data-testid=conductor-end-offset-button')).toContainText('00:00:31'); | ||||
|     }); | ||||
|     // Verify time was updated on preceding time offset button | ||||
|     await expect(page.locator('data-testid=conductor-end-offset-button')).toContainText('00:00:31'); | ||||
|   }); | ||||
|  | ||||
|     /** | ||||
|      * Verify that offsets and url params are preserved when switching | ||||
|      * between fixed timespan and real-time mode. | ||||
|      */ | ||||
|     test('preserve offsets and url params when switching between fixed and real-time mode', async ({ page }) => { | ||||
|         const startOffset = { | ||||
|             mins: '30', | ||||
|             secs: '23' | ||||
|         }; | ||||
|   /** | ||||
|    * Verify that offsets and url params are preserved when switching | ||||
|    * between fixed timespan and real-time mode. | ||||
|    */ | ||||
|   test('preserve offsets and url params when switching between fixed and real-time mode', async ({ | ||||
|     page | ||||
|   }) => { | ||||
|     const startOffset = { | ||||
|       mins: '30', | ||||
|       secs: '23' | ||||
|     }; | ||||
|  | ||||
|         const endOffset = { | ||||
|             secs: '01' | ||||
|         }; | ||||
|     const endOffset = { | ||||
|       secs: '01' | ||||
|     }; | ||||
|  | ||||
|         // Convert offsets to milliseconds | ||||
|         const startDelta = (30 * 60 * 1000) + (23 * 1000); | ||||
|         const endDelta = (1 * 1000); | ||||
|     // Convert offsets to milliseconds | ||||
|     const startDelta = 30 * 60 * 1000 + 23 * 1000; | ||||
|     const endDelta = 1 * 1000; | ||||
|  | ||||
|         // Go to baseURL | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|     // Go to baseURL | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|  | ||||
|         // Switch to real-time mode | ||||
|         await setRealTimeMode(page); | ||||
|     // Switch to real-time mode | ||||
|     await setRealTimeMode(page); | ||||
|  | ||||
|         // Set start time offset | ||||
|         await setStartOffset(page, startOffset); | ||||
|     // Set start time offset | ||||
|     await setStartOffset(page, startOffset); | ||||
|  | ||||
|         // Set end time offset | ||||
|         await setEndOffset(page, endOffset); | ||||
|     // Set end time offset | ||||
|     await setEndOffset(page, endOffset); | ||||
|  | ||||
|         // Switch to fixed timespan mode | ||||
|         await setFixedTimeMode(page); | ||||
|     // Switch to fixed timespan mode | ||||
|     await setFixedTimeMode(page); | ||||
|  | ||||
|         // Switch back to real-time mode | ||||
|         await setRealTimeMode(page); | ||||
|     // Switch back to real-time mode | ||||
|     await setRealTimeMode(page); | ||||
|  | ||||
|         // Verify updated start time offset persists after mode switch | ||||
|         await expect(page.locator('data-testid=conductor-start-offset-button')).toContainText('00:30:23'); | ||||
|     // Verify updated start time offset persists after mode switch | ||||
|     await expect(page.locator('data-testid=conductor-start-offset-button')).toContainText( | ||||
|       '00:30:23' | ||||
|     ); | ||||
|  | ||||
|         // Verify updated end time offset persists after mode switch | ||||
|         await expect(page.locator('data-testid=conductor-end-offset-button')).toContainText('00:00:01'); | ||||
|     // Verify updated end time offset persists after mode switch | ||||
|     await expect(page.locator('data-testid=conductor-end-offset-button')).toContainText('00:00:01'); | ||||
|  | ||||
|         // Verify url parameters persist after mode switch | ||||
|         await page.waitForNavigation({ waitUntil: 'networkidle' }); | ||||
|         expect(page.url()).toContain(`startDelta=${startDelta}`); | ||||
|         expect(page.url()).toContain(`endDelta=${endDelta}`); | ||||
|     }); | ||||
|     // Verify url parameters persist after mode switch | ||||
|     await page.waitForNavigation({ waitUntil: 'networkidle' }); | ||||
|     expect(page.url()).toContain(`startDelta=${startDelta}`); | ||||
|     expect(page.url()).toContain(`endDelta=${endDelta}`); | ||||
|   }); | ||||
|  | ||||
|     test.fixme('time conductor history in fixed time mode will track changing start and end times', async ({ page }) => { | ||||
|         // change start time, verify it's tracked in history | ||||
|         // change end time, verify it's tracked in history | ||||
|     }); | ||||
|   test.fixme( | ||||
|     'time conductor history in fixed time mode will track changing start and end times', | ||||
|     async ({ page }) => { | ||||
|       // change start time, verify it's tracked in history | ||||
|       // change end time, verify it's tracked in history | ||||
|     } | ||||
|   ); | ||||
|  | ||||
|     test.fixme('time conductor history in realtime mode will track changing start and end times', async ({ page }) => { | ||||
|         // change start offset, verify it's tracked in history | ||||
|         // change end offset, verify it's tracked in history | ||||
|     }); | ||||
|   test.fixme( | ||||
|     'time conductor history in realtime mode will track changing start and end times', | ||||
|     async ({ page }) => { | ||||
|       // change start offset, verify it's tracked in history | ||||
|       // change end offset, verify it's tracked in history | ||||
|     } | ||||
|   ); | ||||
|  | ||||
|     test.fixme('time conductor history allows you to set a historical timeframe', async ({ page }) => { | ||||
|         // make sure there are historical history options | ||||
|         // select an option and make sure the time conductor start and end bounds are updated correctly | ||||
|     }); | ||||
|   test.fixme( | ||||
|     'time conductor history allows you to set a historical timeframe', | ||||
|     async ({ page }) => { | ||||
|       // make sure there are historical history options | ||||
|       // select an option and make sure the time conductor start and end bounds are updated correctly | ||||
|     } | ||||
|   ); | ||||
|  | ||||
|     test.fixme('time conductor history allows you to set a realtime offsets', async ({ page }) => { | ||||
|         // make sure there are realtime history options | ||||
|         // select an option and verify the offsets are updated correctly | ||||
|     }); | ||||
|   test.fixme('time conductor history allows you to set a realtime offsets', async ({ page }) => { | ||||
|     // make sure there are realtime history options | ||||
|     // select an option and verify the offsets are updated correctly | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| test.describe('Time Conductor History', () => { | ||||
|     test("shows milliseconds on hover @unstable", async ({ page }) => { | ||||
|         test.info().annotations.push({ | ||||
|             type: 'issue', | ||||
|             description: 'https://github.com/nasa/openmct/issues/4386' | ||||
|         }); | ||||
|         // Navigate to Open MCT in Fixed Time Mode, UTC Time System | ||||
|         // with startBound at 2022-01-01 00:00:00.000Z | ||||
|         // and endBound at 2022-01-01 00:00:00.200Z | ||||
|         await page.goto('./#/browse/mine?view=grid&tc.mode=fixed&tc.startBound=1640995200000&tc.endBound=1640995200200&tc.timeSystem=utc&hideInspector=true', { waitUntil: 'networkidle' }); | ||||
|         await page.locator("[aria-label='Time Conductor History']").hover({ trial: true}); | ||||
|         await page.locator("[aria-label='Time Conductor History']").click(); | ||||
|  | ||||
|         // Validate history item format | ||||
|         const historyItem = page.locator('text="2022-01-01 00:00:00 + 200ms"'); | ||||
|         await expect(historyItem).toBeEnabled(); | ||||
|         await expect(historyItem).toHaveAttribute('title', '2022-01-01 00:00:00.000 - 2022-01-01 00:00:00.200'); | ||||
|   test('shows milliseconds on hover @unstable', async ({ page }) => { | ||||
|     test.info().annotations.push({ | ||||
|       type: 'issue', | ||||
|       description: 'https://github.com/nasa/openmct/issues/4386' | ||||
|     }); | ||||
|     // Navigate to Open MCT in Fixed Time Mode, UTC Time System | ||||
|     // with startBound at 2022-01-01 00:00:00.000Z | ||||
|     // and endBound at 2022-01-01 00:00:00.200Z | ||||
|     await page.goto( | ||||
|       './#/browse/mine?view=grid&tc.mode=fixed&tc.startBound=1640995200000&tc.endBound=1640995200200&tc.timeSystem=utc&hideInspector=true', | ||||
|       { waitUntil: 'networkidle' } | ||||
|     ); | ||||
|     await page.locator("[aria-label='Time Conductor History']").hover({ trial: true }); | ||||
|     await page.locator("[aria-label='Time Conductor History']").click(); | ||||
|  | ||||
|     // Validate history item format | ||||
|     const historyItem = page.locator('text="2022-01-01 00:00:00 + 200ms"'); | ||||
|     await expect(historyItem).toBeEnabled(); | ||||
|     await expect(historyItem).toHaveAttribute( | ||||
|       'title', | ||||
|       '2022-01-01 00:00:00.000 - 2022-01-01 00:00:00.200' | ||||
|     ); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -21,43 +21,46 @@ | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| const { test, expect } = require('../../../../pluginFixtures'); | ||||
| const { openObjectTreeContextMenu, createDomainObjectWithDefaults } = require('../../../../appActions'); | ||||
| const { | ||||
|   openObjectTreeContextMenu, | ||||
|   createDomainObjectWithDefaults | ||||
| } = require('../../../../appActions'); | ||||
|  | ||||
| test.describe('Timer', () => { | ||||
|     let timer; | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|         timer = await createDomainObjectWithDefaults(page, { type: 'timer' }); | ||||
|   let timer; | ||||
|   test.beforeEach(async ({ page }) => { | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|     timer = await createDomainObjectWithDefaults(page, { type: 'timer' }); | ||||
|   }); | ||||
|  | ||||
|   test('Can perform actions on the Timer', async ({ page }) => { | ||||
|     test.info().annotations.push({ | ||||
|       type: 'issue', | ||||
|       description: 'https://github.com/nasa/openmct/issues/4313' | ||||
|     }); | ||||
|  | ||||
|     test('Can perform actions on the Timer', async ({ page }) => { | ||||
|         test.info().annotations.push({ | ||||
|             type: 'issue', | ||||
|             description: 'https://github.com/nasa/openmct/issues/4313' | ||||
|         }); | ||||
|     const timerUrl = timer.url; | ||||
|  | ||||
|         const timerUrl = timer.url; | ||||
|  | ||||
|         await test.step("From the tree context menu", async () => { | ||||
|             await triggerTimerContextMenuAction(page, timerUrl, 'Start'); | ||||
|             await triggerTimerContextMenuAction(page, timerUrl, 'Pause'); | ||||
|             await triggerTimerContextMenuAction(page, timerUrl, 'Restart at 0'); | ||||
|             await triggerTimerContextMenuAction(page, timerUrl, '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'); | ||||
|         }); | ||||
|     await test.step('From the tree context menu', async () => { | ||||
|       await triggerTimerContextMenuAction(page, timerUrl, 'Start'); | ||||
|       await triggerTimerContextMenuAction(page, timerUrl, 'Pause'); | ||||
|       await triggerTimerContextMenuAction(page, timerUrl, 'Restart at 0'); | ||||
|       await triggerTimerContextMenuAction(page, timerUrl, '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'); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| /** | ||||
| @@ -76,10 +79,10 @@ test.describe('Timer', () => { | ||||
|  * @param {TimerAction} action | ||||
|  */ | ||||
| async function triggerTimerContextMenuAction(page, timerUrl, action) { | ||||
|     const menuAction = `.c-menu ul li >> text="${action}"`; | ||||
|     await openObjectTreeContextMenu(page, timerUrl); | ||||
|     await page.locator(menuAction).click(); | ||||
|     assertTimerStateAfterAction(page, action); | ||||
|   const menuAction = `.c-menu ul li >> text="${action}"`; | ||||
|   await openObjectTreeContextMenu(page, timerUrl); | ||||
|   await page.locator(menuAction).click(); | ||||
|   assertTimerStateAfterAction(page, action); | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -88,21 +91,21 @@ async function triggerTimerContextMenuAction(page, timerUrl, action) { | ||||
|  * @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 maximum 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++; | ||||
|     } | ||||
|   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 maximum 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); | ||||
|   await page.locator(menuAction).click(); | ||||
|   assertTimerStateAfterAction(page, action); | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -111,10 +114,10 @@ async function triggerTimer3dotMenuAction(page, action) { | ||||
|  * @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); | ||||
|   await page.locator('.c-timer').hover({ trial: true }); | ||||
|   const buttonTitle = buttonTitleFromAction(action); | ||||
|   await page.click(`button[title="${buttonTitle}"]`); | ||||
|   assertTimerStateAfterAction(page, action); | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -122,14 +125,14 @@ async function triggerTimerViewAction(page, action) { | ||||
|  * @param {TimerViewAction} action | ||||
|  */ | ||||
| function buttonTitleFromAction(action) { | ||||
|     switch (action) { | ||||
|   switch (action) { | ||||
|     case 'Start': | ||||
|         return 'Start'; | ||||
|       return 'Start'; | ||||
|     case 'Pause': | ||||
|         return 'Pause'; | ||||
|       return 'Pause'; | ||||
|     case 'Restart at 0': | ||||
|         return 'Reset'; | ||||
|     } | ||||
|       return 'Reset'; | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -138,19 +141,19 @@ function buttonTitleFromAction(action) { | ||||
|  * @param {TimerAction} action | ||||
|  */ | ||||
| async function assertTimerStateAfterAction(page, action) { | ||||
|     let timerStateClass; | ||||
|     switch (action) { | ||||
|   let timerStateClass; | ||||
|   switch (action) { | ||||
|     case 'Start': | ||||
|     case 'Restart at 0': | ||||
|         timerStateClass = "is-started"; | ||||
|         break; | ||||
|       timerStateClass = 'is-started'; | ||||
|       break; | ||||
|     case 'Stop': | ||||
|         timerStateClass = 'is-stopped'; | ||||
|         break; | ||||
|       timerStateClass = 'is-stopped'; | ||||
|       break; | ||||
|     case 'Pause': | ||||
|         timerStateClass = 'is-paused'; | ||||
|         break; | ||||
|     } | ||||
|       timerStateClass = 'is-paused'; | ||||
|       break; | ||||
|   } | ||||
|  | ||||
|     await expect.soft(page.locator('.c-timer')).toHaveClass(new RegExp(timerStateClass)); | ||||
|   await expect.soft(page.locator('.c-timer')).toHaveClass(new RegExp(timerStateClass)); | ||||
| } | ||||
|   | ||||
| @@ -25,284 +25,327 @@ const { createDomainObjectWithDefaults } = require('../../appActions.js'); | ||||
| const { waitForAnimations } = require('../../baseFixtures.js'); | ||||
|  | ||||
| test.describe('Recent Objects', () => { | ||||
|     /** @type {import('@playwright/test').Locator} */ | ||||
|     let recentObjectsList; | ||||
|     /** @type {import('@playwright/test').Locator} */ | ||||
|     let clock; | ||||
|     /** @type {import('@playwright/test').Locator} */ | ||||
|     let folderA; | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|   /** @type {import('@playwright/test').Locator} */ | ||||
|   let recentObjectsList; | ||||
|   /** @type {import('@playwright/test').Locator} */ | ||||
|   let clock; | ||||
|   /** @type {import('@playwright/test').Locator} */ | ||||
|   let folderA; | ||||
|   test.beforeEach(async ({ page }) => { | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|  | ||||
|         // Set Recent Objects List locator for subsequent tests | ||||
|         recentObjectsList = page.getByRole('list', { | ||||
|             name: 'Recent Objects' | ||||
|         }); | ||||
|  | ||||
|         // Create a folder and nest a Clock within it | ||||
|         folderA = await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Folder' | ||||
|         }); | ||||
|         clock = await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Clock', | ||||
|             parent: folderA.uuid | ||||
|         }); | ||||
|  | ||||
|         // Drag the Recent Objects panel up a bit | ||||
|         await page.locator('.l-pane.l-pane--vertical-handle-before', { | ||||
|             hasText: 'Recently Viewed' | ||||
|         }).locator('.l-pane__handle').hover(); | ||||
|         await page.mouse.down(); | ||||
|         await page.mouse.move(0, 100); | ||||
|         await page.mouse.up(); | ||||
|     }); | ||||
|     test('Navigated objects show up in recents, object renames and deletions are reflected', async ({ page }) => { | ||||
|         // Verify that both created objects appear in the list and are in the correct order | ||||
|         await assertInitialRecentObjectsListState(); | ||||
|  | ||||
|         // Navigate to the folder by clicking on the main object name in the recent objects list item | ||||
|         await page.getByRole('listitem', { name: folderA.name }).getByText(folderA.name).click(); | ||||
|         await page.waitForURL(`**/${folderA.uuid}?*`); | ||||
|         expect(recentObjectsList.getByRole('listitem').nth(0).getByText(folderA.name)).toBeTruthy(); | ||||
|  | ||||
|         // Rename | ||||
|         folderA.name = `${folderA.name}-NEW!`; | ||||
|         await page.locator('.l-browse-bar__object-name').fill(""); | ||||
|         await page.locator('.l-browse-bar__object-name').fill(folderA.name); | ||||
|         await page.keyboard.press('Enter'); | ||||
|  | ||||
|         // Verify rename has been applied in recent objects list item and objects paths | ||||
|         expect(await page.getByRole('navigation', { | ||||
|             name: clock.name | ||||
|         }).locator('a').filter({ | ||||
|             hasText: folderA.name | ||||
|         }).count()).toBeGreaterThan(0); | ||||
|         expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeTruthy(); | ||||
|  | ||||
|         // Delete | ||||
|         await page.click('button[title="Show selected item in tree"]'); | ||||
|         // Delete the folder via the left tree pane treeitem context menu | ||||
|         await page.getByRole('treeitem', { name: new RegExp(folderA.name) }).locator('a').click({ | ||||
|             button: 'right' | ||||
|         }); | ||||
|         await page.getByRole('menuitem', { name: /Remove/ }).click(); | ||||
|         await page.getByRole('button', { name: 'OK' }).click(); | ||||
|  | ||||
|         // Verify that the folder and clock are no longer in the recent objects list | ||||
|         await expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeHidden(); | ||||
|         await expect(recentObjectsList.getByRole('listitem', { name: clock.name })).toBeHidden(); | ||||
|     }); | ||||
|     test("Clicking on an object in the path of a recent object navigates to the object", async ({ page, openmctConfig }) => { | ||||
|         const { myItemsFolderName } = openmctConfig; | ||||
|         test.info().annotations.push({ | ||||
|             type: 'issue', | ||||
|             description: 'https://github.com/nasa/openmct/issues/6151' | ||||
|         }); | ||||
|         await page.goto('./#/browse/mine'); | ||||
|  | ||||
|         // Navigate to the folder by clicking on its entry in the Clock's breadcrumb | ||||
|         const waitForFolderNavigation = page.waitForURL(`**/${folderA.uuid}?*`); | ||||
|         await page.getByRole('navigation', { | ||||
|             name: clock.name | ||||
|         }).locator('a').filter({ | ||||
|             hasText: folderA.name | ||||
|         }).click(); | ||||
|  | ||||
|         // Verify that the hash URL updates correctly | ||||
|         await waitForFolderNavigation; | ||||
|         expect(page.url()).toMatch(new RegExp(`.*${folderA.uuid}?.*`)); | ||||
|  | ||||
|         // Navigate to My Items by clicking on its entry in the Clock's breadcrumb | ||||
|         const waitForMyItemsNavigation = page.waitForURL(`**/mine?*`); | ||||
|         await page.getByRole('navigation', { | ||||
|             name: clock.name | ||||
|         }).locator('a').filter({ | ||||
|             hasText: myItemsFolderName | ||||
|         }).click(); | ||||
|  | ||||
|         // Verify that the hash URL updates correctly | ||||
|         await waitForMyItemsNavigation; | ||||
|         expect(page.url()).toMatch(new RegExp(`.*mine?.*`)); | ||||
|     }); | ||||
|     test("Clicking on the 'target button' scrolls the object into view in the tree and highlights it", async ({ page }) => { | ||||
|         const clockTreeItem = page.getByRole('tree', { name: 'Main Tree'}).getByRole('treeitem', { name: clock.name }); | ||||
|         const folderTreeItem = page.getByRole('tree', { name: 'Main Tree'}) | ||||
|             .getByRole('treeitem', { | ||||
|                 name: folderA.name, | ||||
|                 expanded: true | ||||
|             }); | ||||
|  | ||||
|         // Click the "Target" button for the Clock which is nested in a folder | ||||
|         await page.getByRole('button', { name: `Open and scroll to ${clock.name}`}).click(); | ||||
|  | ||||
|         // Assert that the Clock parent folder has expanded and the Clock is visible) | ||||
|         await expect(folderTreeItem.locator('.c-disclosure-triangle')).toHaveClass(/--expanded/); | ||||
|         await expect(clockTreeItem).toBeVisible(); | ||||
|  | ||||
|         // Assert that the Clock treeitem is highlighted | ||||
|         await expect(clockTreeItem.locator('.c-tree__item')).toHaveClass(/is-targeted-item/); | ||||
|  | ||||
|         // Wait for highlight animation to end | ||||
|         await waitForAnimations(clockTreeItem.locator('.c-tree__item')); | ||||
|  | ||||
|         // Assert that the Clock treeitem is no longer highlighted | ||||
|         await expect(clockTreeItem.locator('.c-tree__item')).not.toHaveClass(/is-targeted-item/); | ||||
|     }); | ||||
|     test("Persists on refresh", async ({ page }) => { | ||||
|         await assertInitialRecentObjectsListState(); | ||||
|         await page.reload(); | ||||
|         await assertInitialRecentObjectsListState(); | ||||
|     }); | ||||
|     test("Displays objects and aliases uniquely", async ({ page }) => { | ||||
|         const mainTree = page.getByRole('tree', { name: 'Main Tree'}); | ||||
|  | ||||
|         // Navigate to the clock and reveal it in the tree | ||||
|         await page.goto(clock.url); | ||||
|         await page.getByTitle('Show selected item in tree').click(); | ||||
|  | ||||
|         // Right click the clock and create an alias using the "link" context menu action | ||||
|         const clockTreeItem = page.getByRole('tree', { | ||||
|             name: 'Main Tree' | ||||
|         }).getByRole('treeitem', { | ||||
|             name: clock.name | ||||
|         }); | ||||
|         await clockTreeItem.click({ | ||||
|             button: 'right' | ||||
|         }); | ||||
|         await page.getByRole('menuitem', { | ||||
|             name: /Create Link/ | ||||
|         }).click(); | ||||
|         await page.getByRole('tree', { name: 'Create Modal Tree'}).getByRole('treeitem').first().click(); | ||||
|         await page.getByRole('button', { name: 'Save' }).click(); | ||||
|  | ||||
|         // Click the newly created object alias in the tree | ||||
|         await mainTree.getByRole('treeitem', { | ||||
|             name: new RegExp(clock.name) | ||||
|         }).filter({ | ||||
|             has: page.locator('.is-alias') | ||||
|         }).click(); | ||||
|  | ||||
|         // Assert that two recent objects are displayed and one of them is an alias | ||||
|         expect(await recentObjectsList.getByRole('listitem', { name: clock.name }).count()).toBe(2); | ||||
|         expect(await recentObjectsList.locator('.is-alias').count()).toBe(1); | ||||
|  | ||||
|         // Assert that the alias and the original's breadcrumbs are different | ||||
|         const clockBreadcrumbs = recentObjectsList.getByRole('listitem', {name: clock.name}).getByRole('navigation'); | ||||
|         expect(await clockBreadcrumbs.count()).toBe(2); | ||||
|         expect(await clockBreadcrumbs.nth(0).innerText()).not.toEqual(await clockBreadcrumbs.nth(1).innerText()); | ||||
|     }); | ||||
|     test("Enforces a limit of 20 recent objects and clears the recent objects", async ({ page }) => { | ||||
|         // Creating 21 objects takes a while, so increase the timeout | ||||
|         test.slow(); | ||||
|  | ||||
|         // Assert that the list initially contains 3 objects (clock, folder, my items) | ||||
|         expect(await recentObjectsList.locator('.c-recentobjects-listitem').count()).toBe(3); | ||||
|  | ||||
|         let lastFolder; | ||||
|         let lastClock; | ||||
|         // Create 19 more objects (3 in beforeEach() + 18 new = 21 total) | ||||
|         for (let i = 0; i < 9; i++) { | ||||
|             lastFolder = await createDomainObjectWithDefaults(page, { | ||||
|                 type: "Folder", | ||||
|                 parent: lastFolder?.uuid | ||||
|             }); | ||||
|             lastClock = await createDomainObjectWithDefaults(page, { | ||||
|                 type: "Clock", | ||||
|                 parent: lastFolder?.uuid | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         // Assert that the list contains 20 objects | ||||
|         expect(await recentObjectsList.locator('.c-recentobjects-listitem').count()).toBe(20); | ||||
|  | ||||
|         // Collapse the tree | ||||
|         await page.getByTitle("Collapse all tree items").click(); | ||||
|         const lastFolderTreeItem = page.getByRole('tree', { name: 'Main Tree'}) | ||||
|             .getByRole('treeitem', { | ||||
|                 name: lastFolder.name, | ||||
|                 expanded: true | ||||
|             }); | ||||
|         const lastClockTreeItem = page.getByRole('tree', { name: 'Main Tree'}) | ||||
|             .getByRole('treeitem', { | ||||
|                 name: lastClock.name | ||||
|             }); | ||||
|  | ||||
|         // Test "Open and Scroll To" in a deeply nested tree, while we're here | ||||
|         await page.getByRole('button', { name: `Open and scroll to ${lastClock.name}`}).click(); | ||||
|  | ||||
|         // Assert that the Clock parent folder has expanded and the Clock is visible) | ||||
|         await expect(lastFolderTreeItem.locator('.c-disclosure-triangle')).toHaveClass(/--expanded/); | ||||
|         await expect(lastClockTreeItem).toBeVisible(); | ||||
|  | ||||
|         // Assert that the Clock treeitem is highlighted | ||||
|         await expect(lastClockTreeItem.locator('.c-tree__item')).toHaveClass(/is-targeted-item/); | ||||
|  | ||||
|         // Wait for highlight animation to end | ||||
|         await waitForAnimations(lastClockTreeItem.locator('.c-tree__item')); | ||||
|  | ||||
|         // Assert that the Clock treeitem is no longer highlighted | ||||
|         await expect(lastClockTreeItem.locator('.c-tree__item')).not.toHaveClass(/is-targeted-item/); | ||||
|  | ||||
|         // Click the aria-label="Clear Recently Viewed" button | ||||
|         await page.getByRole('button', { name: 'Clear Recently Viewed' }).click(); | ||||
|  | ||||
|         // Click on the "OK" button in the confirmation dialog | ||||
|         await page.getByRole('button', { name: 'OK' }).click(); | ||||
|  | ||||
|         // Assert that the list is empty | ||||
|         expect(await recentObjectsList.locator('.c-recentobjects-listitem').count()).toBe(0); | ||||
|     }); | ||||
|     test("Ensure clear recent objects button is active or inactive", async ({ page }) => { | ||||
|         // Assert that the list initially contains 3 objects (clock, folder, my items) | ||||
|         expect(await recentObjectsList.locator('.c-recentobjects-listitem').count()).toBe(3); | ||||
|  | ||||
|         // Assert that the button is enabled | ||||
|         expect( | ||||
|             await page | ||||
|                 .getByRole("button", { name: "Clear Recently Viewed" }) | ||||
|                 .isEnabled() | ||||
|         ).toBe(true); | ||||
|  | ||||
|         // Click the aria-label="Clear Recently Viewed" button | ||||
|         await page.getByRole("button", { name: "Clear Recently Viewed" }).click(); | ||||
|  | ||||
|         // Click on the "OK" button in the confirmation dialog | ||||
|         await page.getByRole("button", { name: "OK" }).click(); | ||||
|  | ||||
|         // Assert that the list is empty | ||||
|         expect( | ||||
|             await recentObjectsList.locator(".c-recentobjects-listitem").count() | ||||
|         ).toBe(0); | ||||
|  | ||||
|         // Assert that the button is disabled | ||||
|         expect( | ||||
|             await page | ||||
|                 .getByRole("button", { name: "Clear Recently Viewed" }) | ||||
|                 .isEnabled() | ||||
|         ).toBe(false); | ||||
|  | ||||
|         // Navigate to folder object | ||||
|         await page.goto(folderA.url); | ||||
|  | ||||
|         // Assert that the list contains 1 object | ||||
|         expect(await recentObjectsList.locator('.c-recentobjects-listitem').count()).toBe(1); | ||||
|  | ||||
|         // Assert that the button is enabled | ||||
|         expect( | ||||
|             await page | ||||
|                 .getByRole("button", { name: "Clear Recently Viewed" }) | ||||
|                 .isEnabled() | ||||
|         ).toBe(true); | ||||
|     // Set Recent Objects List locator for subsequent tests | ||||
|     recentObjectsList = page.getByRole('list', { | ||||
|       name: 'Recent Objects' | ||||
|     }); | ||||
|  | ||||
|     function assertInitialRecentObjectsListState() { | ||||
|         return Promise.all([ | ||||
|             expect(recentObjectsList.getByRole('listitem', { name: clock.name })).toBeVisible(), | ||||
|             expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeVisible(), | ||||
|             expect(recentObjectsList.getByRole('listitem', { name: clock.name }).locator('a').getByText(folderA.name)).toBeVisible(), | ||||
|             expect(recentObjectsList.getByRole('listitem').nth(0).getByText(clock.name)).toBeVisible(), | ||||
|             expect(recentObjectsList.getByRole('listitem', { name: clock.name }).locator('a').getByText(folderA.name)).toBeVisible(), | ||||
|             expect(recentObjectsList.getByRole('listitem').nth(3).getByText(folderA.name)).toBeVisible() | ||||
|         ]); | ||||
|     // Create a folder and nest a Clock within it | ||||
|     folderA = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Folder' | ||||
|     }); | ||||
|     clock = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Clock', | ||||
|       parent: folderA.uuid | ||||
|     }); | ||||
|  | ||||
|     // Drag the Recent Objects panel up a bit | ||||
|     await page | ||||
|       .locator('.l-pane.l-pane--vertical-handle-before', { | ||||
|         hasText: 'Recently Viewed' | ||||
|       }) | ||||
|       .locator('.l-pane__handle') | ||||
|       .hover(); | ||||
|     await page.mouse.down(); | ||||
|     await page.mouse.move(0, 100); | ||||
|     await page.mouse.up(); | ||||
|   }); | ||||
|   test('Navigated objects show up in recents, object renames and deletions are reflected', async ({ | ||||
|     page | ||||
|   }) => { | ||||
|     // Verify that both created objects appear in the list and are in the correct order | ||||
|     await assertInitialRecentObjectsListState(); | ||||
|  | ||||
|     // Navigate to the folder by clicking on the main object name in the recent objects list item | ||||
|     await page.getByRole('listitem', { name: folderA.name }).getByText(folderA.name).click(); | ||||
|     await page.waitForURL(`**/${folderA.uuid}?*`); | ||||
|     expect(recentObjectsList.getByRole('listitem').nth(0).getByText(folderA.name)).toBeTruthy(); | ||||
|  | ||||
|     // Rename | ||||
|     folderA.name = `${folderA.name}-NEW!`; | ||||
|     await page.locator('.l-browse-bar__object-name').fill(''); | ||||
|     await page.locator('.l-browse-bar__object-name').fill(folderA.name); | ||||
|     await page.keyboard.press('Enter'); | ||||
|  | ||||
|     // Verify rename has been applied in recent objects list item and objects paths | ||||
|     expect( | ||||
|       await page | ||||
|         .getByRole('navigation', { | ||||
|           name: clock.name | ||||
|         }) | ||||
|         .locator('a') | ||||
|         .filter({ | ||||
|           hasText: folderA.name | ||||
|         }) | ||||
|         .count() | ||||
|     ).toBeGreaterThan(0); | ||||
|     expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeTruthy(); | ||||
|  | ||||
|     // Delete | ||||
|     await page.click('button[title="Show selected item in tree"]'); | ||||
|     // Delete the folder via the left tree pane treeitem context menu | ||||
|     await page | ||||
|       .getByRole('treeitem', { name: new RegExp(folderA.name) }) | ||||
|       .locator('a') | ||||
|       .click({ | ||||
|         button: 'right' | ||||
|       }); | ||||
|     await page.getByRole('menuitem', { name: /Remove/ }).click(); | ||||
|     await page.getByRole('button', { name: 'OK' }).click(); | ||||
|  | ||||
|     // Verify that the folder and clock are no longer in the recent objects list | ||||
|     await expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeHidden(); | ||||
|     await expect(recentObjectsList.getByRole('listitem', { name: clock.name })).toBeHidden(); | ||||
|   }); | ||||
|   test('Clicking on an object in the path of a recent object navigates to the object', async ({ | ||||
|     page, | ||||
|     openmctConfig | ||||
|   }) => { | ||||
|     const { myItemsFolderName } = openmctConfig; | ||||
|     test.info().annotations.push({ | ||||
|       type: 'issue', | ||||
|       description: 'https://github.com/nasa/openmct/issues/6151' | ||||
|     }); | ||||
|     await page.goto('./#/browse/mine'); | ||||
|  | ||||
|     // Navigate to the folder by clicking on its entry in the Clock's breadcrumb | ||||
|     const waitForFolderNavigation = page.waitForURL(`**/${folderA.uuid}?*`); | ||||
|     await page | ||||
|       .getByRole('navigation', { | ||||
|         name: clock.name | ||||
|       }) | ||||
|       .locator('a') | ||||
|       .filter({ | ||||
|         hasText: folderA.name | ||||
|       }) | ||||
|       .click(); | ||||
|  | ||||
|     // Verify that the hash URL updates correctly | ||||
|     await waitForFolderNavigation; | ||||
|     expect(page.url()).toMatch(new RegExp(`.*${folderA.uuid}?.*`)); | ||||
|  | ||||
|     // Navigate to My Items by clicking on its entry in the Clock's breadcrumb | ||||
|     const waitForMyItemsNavigation = page.waitForURL(`**/mine?*`); | ||||
|     await page | ||||
|       .getByRole('navigation', { | ||||
|         name: clock.name | ||||
|       }) | ||||
|       .locator('a') | ||||
|       .filter({ | ||||
|         hasText: myItemsFolderName | ||||
|       }) | ||||
|       .click(); | ||||
|  | ||||
|     // Verify that the hash URL updates correctly | ||||
|     await waitForMyItemsNavigation; | ||||
|     expect(page.url()).toMatch(new RegExp(`.*mine?.*`)); | ||||
|   }); | ||||
|   test("Clicking on the 'target button' scrolls the object into view in the tree and highlights it", async ({ | ||||
|     page | ||||
|   }) => { | ||||
|     const clockTreeItem = page | ||||
|       .getByRole('tree', { name: 'Main Tree' }) | ||||
|       .getByRole('treeitem', { name: clock.name }); | ||||
|     const folderTreeItem = page.getByRole('tree', { name: 'Main Tree' }).getByRole('treeitem', { | ||||
|       name: folderA.name, | ||||
|       expanded: true | ||||
|     }); | ||||
|  | ||||
|     // Click the "Target" button for the Clock which is nested in a folder | ||||
|     await page.getByRole('button', { name: `Open and scroll to ${clock.name}` }).click(); | ||||
|  | ||||
|     // Assert that the Clock parent folder has expanded and the Clock is visible) | ||||
|     await expect(folderTreeItem.locator('.c-disclosure-triangle')).toHaveClass(/--expanded/); | ||||
|     await expect(clockTreeItem).toBeVisible(); | ||||
|  | ||||
|     // Assert that the Clock treeitem is highlighted | ||||
|     await expect(clockTreeItem.locator('.c-tree__item')).toHaveClass(/is-targeted-item/); | ||||
|  | ||||
|     // Wait for highlight animation to end | ||||
|     await waitForAnimations(clockTreeItem.locator('.c-tree__item')); | ||||
|  | ||||
|     // Assert that the Clock treeitem is no longer highlighted | ||||
|     await expect(clockTreeItem.locator('.c-tree__item')).not.toHaveClass(/is-targeted-item/); | ||||
|   }); | ||||
|   test('Persists on refresh', async ({ page }) => { | ||||
|     await assertInitialRecentObjectsListState(); | ||||
|     await page.reload(); | ||||
|     await assertInitialRecentObjectsListState(); | ||||
|   }); | ||||
|   test('Displays objects and aliases uniquely', async ({ page }) => { | ||||
|     const mainTree = page.getByRole('tree', { name: 'Main Tree' }); | ||||
|  | ||||
|     // Navigate to the clock and reveal it in the tree | ||||
|     await page.goto(clock.url); | ||||
|     await page.getByTitle('Show selected item in tree').click(); | ||||
|  | ||||
|     // Right click the clock and create an alias using the "link" context menu action | ||||
|     const clockTreeItem = page | ||||
|       .getByRole('tree', { | ||||
|         name: 'Main Tree' | ||||
|       }) | ||||
|       .getByRole('treeitem', { | ||||
|         name: clock.name | ||||
|       }); | ||||
|     await clockTreeItem.click({ | ||||
|       button: 'right' | ||||
|     }); | ||||
|     await page | ||||
|       .getByRole('menuitem', { | ||||
|         name: /Create Link/ | ||||
|       }) | ||||
|       .click(); | ||||
|     await page | ||||
|       .getByRole('tree', { name: 'Create Modal Tree' }) | ||||
|       .getByRole('treeitem') | ||||
|       .first() | ||||
|       .click(); | ||||
|     await page.getByRole('button', { name: 'Save' }).click(); | ||||
|  | ||||
|     // Click the newly created object alias in the tree | ||||
|     await mainTree | ||||
|       .getByRole('treeitem', { | ||||
|         name: new RegExp(clock.name) | ||||
|       }) | ||||
|       .filter({ | ||||
|         has: page.locator('.is-alias') | ||||
|       }) | ||||
|       .click(); | ||||
|  | ||||
|     // Assert that two recent objects are displayed and one of them is an alias | ||||
|     expect(await recentObjectsList.getByRole('listitem', { name: clock.name }).count()).toBe(2); | ||||
|     expect(await recentObjectsList.locator('.is-alias').count()).toBe(1); | ||||
|  | ||||
|     // Assert that the alias and the original's breadcrumbs are different | ||||
|     const clockBreadcrumbs = recentObjectsList | ||||
|       .getByRole('listitem', { name: clock.name }) | ||||
|       .getByRole('navigation'); | ||||
|     expect(await clockBreadcrumbs.count()).toBe(2); | ||||
|     expect(await clockBreadcrumbs.nth(0).innerText()).not.toEqual( | ||||
|       await clockBreadcrumbs.nth(1).innerText() | ||||
|     ); | ||||
|   }); | ||||
|   test('Enforces a limit of 20 recent objects and clears the recent objects', async ({ page }) => { | ||||
|     // Creating 21 objects takes a while, so increase the timeout | ||||
|     test.slow(); | ||||
|  | ||||
|     // Assert that the list initially contains 3 objects (clock, folder, my items) | ||||
|     expect(await recentObjectsList.locator('.c-recentobjects-listitem').count()).toBe(3); | ||||
|  | ||||
|     let lastFolder; | ||||
|     let lastClock; | ||||
|     // Create 19 more objects (3 in beforeEach() + 18 new = 21 total) | ||||
|     for (let i = 0; i < 9; i++) { | ||||
|       lastFolder = await createDomainObjectWithDefaults(page, { | ||||
|         type: 'Folder', | ||||
|         parent: lastFolder?.uuid | ||||
|       }); | ||||
|       lastClock = await createDomainObjectWithDefaults(page, { | ||||
|         type: 'Clock', | ||||
|         parent: lastFolder?.uuid | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     // Assert that the list contains 20 objects | ||||
|     expect(await recentObjectsList.locator('.c-recentobjects-listitem').count()).toBe(20); | ||||
|  | ||||
|     // Collapse the tree | ||||
|     await page.getByTitle('Collapse all tree items').click(); | ||||
|     const lastFolderTreeItem = page.getByRole('tree', { name: 'Main Tree' }).getByRole('treeitem', { | ||||
|       name: lastFolder.name, | ||||
|       expanded: true | ||||
|     }); | ||||
|     const lastClockTreeItem = page.getByRole('tree', { name: 'Main Tree' }).getByRole('treeitem', { | ||||
|       name: lastClock.name | ||||
|     }); | ||||
|  | ||||
|     // Test "Open and Scroll To" in a deeply nested tree, while we're here | ||||
|     await page.getByRole('button', { name: `Open and scroll to ${lastClock.name}` }).click(); | ||||
|  | ||||
|     // Assert that the Clock parent folder has expanded and the Clock is visible) | ||||
|     await expect(lastFolderTreeItem.locator('.c-disclosure-triangle')).toHaveClass(/--expanded/); | ||||
|     await expect(lastClockTreeItem).toBeVisible(); | ||||
|  | ||||
|     // Assert that the Clock treeitem is highlighted | ||||
|     await expect(lastClockTreeItem.locator('.c-tree__item')).toHaveClass(/is-targeted-item/); | ||||
|  | ||||
|     // Wait for highlight animation to end | ||||
|     await waitForAnimations(lastClockTreeItem.locator('.c-tree__item')); | ||||
|  | ||||
|     // Assert that the Clock treeitem is no longer highlighted | ||||
|     await expect(lastClockTreeItem.locator('.c-tree__item')).not.toHaveClass(/is-targeted-item/); | ||||
|  | ||||
|     // Click the aria-label="Clear Recently Viewed" button | ||||
|     await page.getByRole('button', { name: 'Clear Recently Viewed' }).click(); | ||||
|  | ||||
|     // Click on the "OK" button in the confirmation dialog | ||||
|     await page.getByRole('button', { name: 'OK' }).click(); | ||||
|  | ||||
|     // Assert that the list is empty | ||||
|     expect(await recentObjectsList.locator('.c-recentobjects-listitem').count()).toBe(0); | ||||
|   }); | ||||
|   test('Ensure clear recent objects button is active or inactive', async ({ page }) => { | ||||
|     // Assert that the list initially contains 3 objects (clock, folder, my items) | ||||
|     expect(await recentObjectsList.locator('.c-recentobjects-listitem').count()).toBe(3); | ||||
|  | ||||
|     // Assert that the button is enabled | ||||
|     expect(await page.getByRole('button', { name: 'Clear Recently Viewed' }).isEnabled()).toBe( | ||||
|       true | ||||
|     ); | ||||
|  | ||||
|     // Click the aria-label="Clear Recently Viewed" button | ||||
|     await page.getByRole('button', { name: 'Clear Recently Viewed' }).click(); | ||||
|  | ||||
|     // Click on the "OK" button in the confirmation dialog | ||||
|     await page.getByRole('button', { name: 'OK' }).click(); | ||||
|  | ||||
|     // Assert that the list is empty | ||||
|     expect(await recentObjectsList.locator('.c-recentobjects-listitem').count()).toBe(0); | ||||
|  | ||||
|     // Assert that the button is disabled | ||||
|     expect(await page.getByRole('button', { name: 'Clear Recently Viewed' }).isEnabled()).toBe( | ||||
|       false | ||||
|     ); | ||||
|  | ||||
|     // Navigate to folder object | ||||
|     await page.goto(folderA.url); | ||||
|  | ||||
|     // Assert that the list contains 1 object | ||||
|     expect(await recentObjectsList.locator('.c-recentobjects-listitem').count()).toBe(1); | ||||
|  | ||||
|     // Assert that the button is enabled | ||||
|     expect(await page.getByRole('button', { name: 'Clear Recently Viewed' }).isEnabled()).toBe( | ||||
|       true | ||||
|     ); | ||||
|   }); | ||||
|  | ||||
|   function assertInitialRecentObjectsListState() { | ||||
|     return Promise.all([ | ||||
|       expect(recentObjectsList.getByRole('listitem', { name: clock.name })).toBeVisible(), | ||||
|       expect(recentObjectsList.getByRole('listitem', { name: folderA.name })).toBeVisible(), | ||||
|       expect( | ||||
|         recentObjectsList | ||||
|           .getByRole('listitem', { name: clock.name }) | ||||
|           .locator('a') | ||||
|           .getByText(folderA.name) | ||||
|       ).toBeVisible(), | ||||
|       expect(recentObjectsList.getByRole('listitem').nth(0).getByText(clock.name)).toBeVisible(), | ||||
|       expect( | ||||
|         recentObjectsList | ||||
|           .getByRole('listitem', { name: clock.name }) | ||||
|           .locator('a') | ||||
|           .getByText(folderA.name) | ||||
|       ).toBeVisible(), | ||||
|       expect(recentObjectsList.getByRole('listitem').nth(3).getByText(folderA.name)).toBeVisible() | ||||
|     ]); | ||||
|   } | ||||
| }); | ||||
|   | ||||
| @@ -28,242 +28,270 @@ const { createDomainObjectWithDefaults, selectInspectorTab } = require('../../ap | ||||
| const { v4: uuid } = require('uuid'); | ||||
|  | ||||
| test.describe('Grand Search', () => { | ||||
|     const searchResultSelector = '.c-gsearch-result__title'; | ||||
|     const searchResultDropDownSelector = '.c-gsearch__results'; | ||||
|   const searchResultSelector = '.c-gsearch-result__title'; | ||||
|   const searchResultDropDownSelector = '.c-gsearch__results'; | ||||
|  | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         // Go to baseURL | ||||
|         await page.goto("./", { waitUntil: "networkidle" }); | ||||
|   test.beforeEach(async ({ page }) => { | ||||
|     // Go to baseURL | ||||
|     await page.goto('./', { waitUntil: 'networkidle' }); | ||||
|   }); | ||||
|  | ||||
|   test('Can search for objects, and subsequent search dropdown behaves properly', async ({ | ||||
|     page, | ||||
|     openmctConfig | ||||
|   }) => { | ||||
|     const { myItemsFolderName } = openmctConfig; | ||||
|  | ||||
|     const createdObjects = await createObjectsForSearch(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"] >> nth=0')).toContainText( | ||||
|       `Clock A ${myItemsFolderName} Red Folder Blue Folder` | ||||
|     ); | ||||
|     await expect(page.locator('[aria-label="Search Result"] >> nth=1')).toContainText( | ||||
|       `Clock B ${myItemsFolderName} Red Folder Blue Folder` | ||||
|     ); | ||||
|     await expect(page.locator('[aria-label="Search Result"] >> nth=2')).toContainText( | ||||
|       `Clock C ${myItemsFolderName} Red Folder Blue Folder` | ||||
|     ); | ||||
|     await expect(page.locator('[aria-label="Search Result"] >> nth=3')).toContainText( | ||||
|       `Clock D ${myItemsFolderName} Red Folder Blue Folder` | ||||
|     ); | ||||
|     // Click the Elements pool to dismiss the search menu | ||||
|     await selectInspectorTab(page, 'Elements'); | ||||
|     await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeHidden(); | ||||
|  | ||||
|     await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click(); | ||||
|     await page.locator('[aria-label="Clock A clock result"] >> text=Clock A').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"] >> nth=0')).toBeVisible(); | ||||
|     await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toContainText( | ||||
|       `Clock A ${myItemsFolderName} Red Folder Blue Folder` | ||||
|     ); | ||||
|  | ||||
|     // Click [aria-label="OpenMCT Search"] a >> nth=0 | ||||
|     await page.locator('[aria-label="Search Result"] >> nth=0').click(); | ||||
|     await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeHidden(); | ||||
|  | ||||
|     // 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"] >> nth=0')).toBeHidden(); | ||||
|  | ||||
|     // 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'); | ||||
|     await Promise.all([ | ||||
|       page.waitForNavigation(), | ||||
|       page.locator('[aria-label="Clock A clock result"] >> text=Clock A').click() | ||||
|     ]); | ||||
|     await expect(page.locator('.is-object-type-clock')).toBeVisible(); | ||||
|  | ||||
|     await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').fill('Disp'); | ||||
|     await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toContainText( | ||||
|       createdObjects.displayLayout.name | ||||
|     ); | ||||
|     await expect(page.locator('[aria-label="Search Result"] >> nth=0')).not.toContainText('Folder'); | ||||
|  | ||||
|     await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Clock C'); | ||||
|     await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toContainText( | ||||
|       `Clock C ${myItemsFolderName} Red Folder Blue Folder` | ||||
|     ); | ||||
|  | ||||
|     await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Cloc'); | ||||
|     await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toContainText( | ||||
|       `Clock A ${myItemsFolderName} Red Folder Blue Folder` | ||||
|     ); | ||||
|     await expect(page.locator('[aria-label="Search Result"] >> nth=1')).toContainText( | ||||
|       `Clock B ${myItemsFolderName} Red Folder Blue Folder` | ||||
|     ); | ||||
|     await expect(page.locator('[aria-label="Search Result"] >> nth=2')).toContainText( | ||||
|       `Clock C ${myItemsFolderName} Red Folder Blue Folder` | ||||
|     ); | ||||
|     await expect(page.locator('[aria-label="Search Result"] >> nth=3')).toContainText( | ||||
|       `Clock D ${myItemsFolderName} Red Folder Blue Folder` | ||||
|     ); | ||||
|   }); | ||||
|  | ||||
|   test('Validate empty search result', async ({ page }) => { | ||||
|     // Invalid search for objects | ||||
|     await page.type('input[type=search]', 'not found'); | ||||
|  | ||||
|     // Wait for search to complete | ||||
|     await waitForSearchCompletion(page); | ||||
|  | ||||
|     // Get the search results | ||||
|     const searchResults = page.locator(searchResultSelector); | ||||
|  | ||||
|     // Verify that no results are found | ||||
|     expect(await searchResults.count()).toBe(0); | ||||
|  | ||||
|     // Verify proper message appears | ||||
|     await expect(page.locator('text=No results found')).toBeVisible(); | ||||
|   }); | ||||
|  | ||||
|   test('Validate single object in search result @couchdb', async ({ page }) => { | ||||
|     // Create a folder object | ||||
|     const folderName = uuid(); | ||||
|     await createDomainObjectWithDefaults(page, { | ||||
|       type: 'folder', | ||||
|       name: folderName | ||||
|     }); | ||||
|  | ||||
|     test('Can search for objects, and subsequent search dropdown behaves properly', async ({ page, openmctConfig }) => { | ||||
|         const { myItemsFolderName } = openmctConfig; | ||||
|     // Full search for object | ||||
|     await page.type('input[type=search]', folderName); | ||||
|  | ||||
|         const createdObjects = await createObjectsForSearch(page); | ||||
|     // Wait for search to complete | ||||
|     await waitForSearchCompletion(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"] >> nth=0')).toContainText(`Clock A ${myItemsFolderName} Red Folder Blue Folder`); | ||||
|         await expect(page.locator('[aria-label="Search Result"] >> nth=1')).toContainText(`Clock B ${myItemsFolderName} Red Folder Blue Folder`); | ||||
|         await expect(page.locator('[aria-label="Search Result"] >> nth=2')).toContainText(`Clock C ${myItemsFolderName} Red Folder Blue Folder`); | ||||
|         await expect(page.locator('[aria-label="Search Result"] >> nth=3')).toContainText(`Clock D ${myItemsFolderName} Red Folder Blue Folder`); | ||||
|         // Click the Elements pool to dismiss the search menu | ||||
|         await selectInspectorTab(page, 'Elements'); | ||||
|         await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeHidden(); | ||||
|     // Get the search results | ||||
|     const searchResults = page.locator(searchResultSelector); | ||||
|  | ||||
|         await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click(); | ||||
|         await page.locator('[aria-label="Clock A clock result"] >> text=Clock A').click(); | ||||
|         await expect(page.locator('.js-preview-window')).toBeVisible(); | ||||
|     // Verify that one result is found | ||||
|     await expect(searchResults).toBeVisible(); | ||||
|     expect(await searchResults.count()).toBe(1); | ||||
|     await expect(searchResults).toHaveText(folderName); | ||||
|   }); | ||||
|  | ||||
|         // Click [aria-label="Close"] | ||||
|         await page.locator('[aria-label="Close"]').click(); | ||||
|         await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeVisible(); | ||||
|         await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toContainText(`Clock A ${myItemsFolderName} Red Folder Blue Folder`); | ||||
|   test('Search results are debounced @couchdb', async ({ page }) => { | ||||
|     test.info().annotations.push({ | ||||
|       type: 'issue', | ||||
|       description: 'https://github.com/nasa/openmct/issues/6179' | ||||
|     }); | ||||
|     await createObjectsForSearch(page); | ||||
|  | ||||
|         // Click [aria-label="OpenMCT Search"] a >> nth=0 | ||||
|         await page.locator('[aria-label="Search Result"] >> nth=0').click(); | ||||
|         await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toBeHidden(); | ||||
|  | ||||
|         // 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"] >> nth=0')).toBeHidden(); | ||||
|  | ||||
|         // 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'); | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('[aria-label="Clock A clock result"] >> text=Clock A').click() | ||||
|         ]); | ||||
|         await expect(page.locator('.is-object-type-clock')).toBeVisible(); | ||||
|  | ||||
|         await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').fill('Disp'); | ||||
|         await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toContainText(createdObjects.displayLayout.name); | ||||
|         await expect(page.locator('[aria-label="Search Result"] >> nth=0')).not.toContainText('Folder'); | ||||
|  | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Clock C'); | ||||
|         await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toContainText(`Clock C ${myItemsFolderName} Red Folder Blue Folder`); | ||||
|  | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Cloc'); | ||||
|         await expect(page.locator('[aria-label="Search Result"] >> nth=0')).toContainText(`Clock A ${myItemsFolderName} Red Folder Blue Folder`); | ||||
|         await expect(page.locator('[aria-label="Search Result"] >> nth=1')).toContainText(`Clock B ${myItemsFolderName} Red Folder Blue Folder`); | ||||
|         await expect(page.locator('[aria-label="Search Result"] >> nth=2')).toContainText(`Clock C ${myItemsFolderName} Red Folder Blue Folder`); | ||||
|         await expect(page.locator('[aria-label="Search Result"] >> nth=3')).toContainText(`Clock D ${myItemsFolderName} Red Folder Blue Folder`); | ||||
|     let networkRequests = []; | ||||
|     page.on('request', (request) => { | ||||
|       const searchRequest = request.url().endsWith('_find'); | ||||
|       const fetchRequest = request.resourceType() === 'fetch'; | ||||
|       if (searchRequest && fetchRequest) { | ||||
|         networkRequests.push(request); | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     test('Validate empty search result', async ({ page }) => { | ||||
|         // Invalid search for objects | ||||
|         await page.type("input[type=search]", 'not found'); | ||||
|     // Full search for object | ||||
|     await page.type('input[type=search]', 'Clock', { delay: 100 }); | ||||
|  | ||||
|         // Wait for search to complete | ||||
|         await waitForSearchCompletion(page); | ||||
|     // Wait for search to finish | ||||
|     await waitForSearchCompletion(page); | ||||
|  | ||||
|         // Get the search results | ||||
|         const searchResults = page.locator(searchResultSelector); | ||||
|     // Network requests for the composite telemetry with multiple items should be: | ||||
|     // 1.  batched request for latest telemetry using the bulk API | ||||
|     expect(networkRequests.length).toBe(1); | ||||
|  | ||||
|         // Verify that no results are found | ||||
|         expect(await searchResults.count()).toBe(0); | ||||
|     const searchResultDropDown = await page.locator(searchResultDropDownSelector); | ||||
|  | ||||
|         // Verify proper message appears | ||||
|         await expect(page.locator('text=No results found')).toBeVisible(); | ||||
|     await expect(searchResultDropDown).toContainText('Clock A'); | ||||
|   }); | ||||
|  | ||||
|   test('Validate multiple objects in search results return partial matches', async ({ page }) => { | ||||
|     test.info().annotations.push({ | ||||
|       type: 'issue', | ||||
|       description: 'https://github.com/nasa/openmct/issues/4667' | ||||
|     }); | ||||
|  | ||||
|     test('Validate single object in search result @couchdb', async ({ page }) => { | ||||
|         // Create a folder object | ||||
|         const folderName = uuid(); | ||||
|         await createDomainObjectWithDefaults(page, { | ||||
|             type: 'folder', | ||||
|             name: folderName | ||||
|         }); | ||||
|     // Create folder objects | ||||
|     const folderName1 = 'e928a26e-e924-4ea0'; | ||||
|     const folderName2 = 'e928a26e-e924-4001'; | ||||
|  | ||||
|         // Full search for object | ||||
|         await page.type("input[type=search]", folderName); | ||||
|  | ||||
|         // Wait for search to complete | ||||
|         await waitForSearchCompletion(page); | ||||
|  | ||||
|         // Get the search results | ||||
|         const searchResults = page.locator(searchResultSelector); | ||||
|  | ||||
|         // Verify that one result is found | ||||
|         await expect(searchResults).toBeVisible(); | ||||
|         expect(await searchResults.count()).toBe(1); | ||||
|         await expect(searchResults).toHaveText(folderName); | ||||
|     await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Folder', | ||||
|       name: folderName1 | ||||
|     }); | ||||
|  | ||||
|     test('Search results are debounced @couchdb', async ({ page }) => { | ||||
|         test.info().annotations.push({ | ||||
|             type: 'issue', | ||||
|             description: 'https://github.com/nasa/openmct/issues/6179' | ||||
|         }); | ||||
|         await createObjectsForSearch(page); | ||||
|  | ||||
|         let networkRequests = []; | ||||
|         page.on('request', (request) => { | ||||
|             const searchRequest = request.url().endsWith('_find'); | ||||
|             const fetchRequest = request.resourceType() === 'fetch'; | ||||
|             if (searchRequest && fetchRequest) { | ||||
|                 networkRequests.push(request); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         // Full search for object | ||||
|         await page.type("input[type=search]", 'Clock', { delay: 100 }); | ||||
|  | ||||
|         // Wait for search to finish | ||||
|         await waitForSearchCompletion(page); | ||||
|  | ||||
|         // Network requests for the composite telemetry with multiple items should be: | ||||
|         // 1.  batched request for latest telemetry using the bulk API | ||||
|         expect(networkRequests.length).toBe(1); | ||||
|  | ||||
|         const searchResultDropDown = await page.locator(searchResultDropDownSelector); | ||||
|  | ||||
|         await expect(searchResultDropDown).toContainText('Clock A'); | ||||
|     await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Folder', | ||||
|       name: folderName2 | ||||
|     }); | ||||
|  | ||||
|     test("Validate multiple objects in search results return partial matches", async ({ page }) => { | ||||
|         test.info().annotations.push({ | ||||
|             type: 'issue', | ||||
|             description: 'https://github.com/nasa/openmct/issues/4667' | ||||
|         }); | ||||
|     // Partial search for objects | ||||
|     await page.type('input[type=search]', 'e928a26e'); | ||||
|  | ||||
|         // Create folder objects | ||||
|         const folderName1 = "e928a26e-e924-4ea0"; | ||||
|         const folderName2 = "e928a26e-e924-4001"; | ||||
|     // Wait for search to finish | ||||
|     await waitForSearchCompletion(page); | ||||
|  | ||||
|         await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Folder', | ||||
|             name: folderName1 | ||||
|         }); | ||||
|     const searchResultDropDown = page.locator(searchResultDropDownSelector); | ||||
|  | ||||
|         await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Folder', | ||||
|             name: folderName2 | ||||
|         }); | ||||
|     // Verify that the search result/s correctly match the search query | ||||
|     await expect(searchResultDropDown).toContainText(folderName1); | ||||
|     await expect(searchResultDropDown).toContainText(folderName2); | ||||
|  | ||||
|         // Partial search for objects | ||||
|         await page.type("input[type=search]", 'e928a26e'); | ||||
|  | ||||
|         // Wait for search to finish | ||||
|         await waitForSearchCompletion(page); | ||||
|  | ||||
|         const searchResultDropDown = page.locator(searchResultDropDownSelector); | ||||
|  | ||||
|         // Verify that the search result/s correctly match the search query | ||||
|         await expect(searchResultDropDown).toContainText(folderName1); | ||||
|         await expect(searchResultDropDown).toContainText(folderName2); | ||||
|  | ||||
|         // Get the search results | ||||
|         const searchResults = page.locator(searchResultSelector); | ||||
|         // Verify that two results are found | ||||
|         expect(await searchResults.count()).toBe(2); | ||||
|     }); | ||||
|     // Get the search results | ||||
|     const searchResults = page.locator(searchResultSelector); | ||||
|     // Verify that two results are found | ||||
|     expect(await searchResults.count()).toBe(2); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| async function waitForSearchCompletion(page) { | ||||
|     // Wait loading spinner to disappear | ||||
|     await page.waitForSelector('.search-finished'); | ||||
|   // Wait loading spinner to disappear | ||||
|   await page.waitForSelector('.search-finished'); | ||||
| } | ||||
|  | ||||
| /** | ||||
|   * Creates some domain objects for searching | ||||
|   * @param {import('@playwright/test').Page} page | ||||
|   */ | ||||
|  * Creates some domain objects for searching | ||||
|  * @param {import('@playwright/test').Page} page | ||||
|  */ | ||||
| async function createObjectsForSearch(page) { | ||||
|     const redFolder = await createDomainObjectWithDefaults(page, { | ||||
|         type: 'Folder', | ||||
|         name: 'Red Folder' | ||||
|     }); | ||||
|   const redFolder = await createDomainObjectWithDefaults(page, { | ||||
|     type: 'Folder', | ||||
|     name: 'Red Folder' | ||||
|   }); | ||||
|  | ||||
|     const blueFolder = await createDomainObjectWithDefaults(page, { | ||||
|         type: 'Folder', | ||||
|         name: 'Blue Folder', | ||||
|         parent: redFolder.uuid | ||||
|     }); | ||||
|   const blueFolder = await createDomainObjectWithDefaults(page, { | ||||
|     type: 'Folder', | ||||
|     name: 'Blue Folder', | ||||
|     parent: redFolder.uuid | ||||
|   }); | ||||
|  | ||||
|     const clockA = await createDomainObjectWithDefaults(page, { | ||||
|         type: 'Clock', | ||||
|         name: 'Clock A', | ||||
|         parent: blueFolder.uuid | ||||
|     }); | ||||
|     const clockB = await createDomainObjectWithDefaults(page, { | ||||
|         type: 'Clock', | ||||
|         name: 'Clock B', | ||||
|         parent: blueFolder.uuid | ||||
|     }); | ||||
|     const clockC = await createDomainObjectWithDefaults(page, { | ||||
|         type: 'Clock', | ||||
|         name: 'Clock C', | ||||
|         parent: blueFolder.uuid | ||||
|     }); | ||||
|     const clockD = await createDomainObjectWithDefaults(page, { | ||||
|         type: 'Clock', | ||||
|         name: 'Clock D', | ||||
|         parent: blueFolder.uuid | ||||
|     }); | ||||
|   const clockA = await createDomainObjectWithDefaults(page, { | ||||
|     type: 'Clock', | ||||
|     name: 'Clock A', | ||||
|     parent: blueFolder.uuid | ||||
|   }); | ||||
|   const clockB = await createDomainObjectWithDefaults(page, { | ||||
|     type: 'Clock', | ||||
|     name: 'Clock B', | ||||
|     parent: blueFolder.uuid | ||||
|   }); | ||||
|   const clockC = await createDomainObjectWithDefaults(page, { | ||||
|     type: 'Clock', | ||||
|     name: 'Clock C', | ||||
|     parent: blueFolder.uuid | ||||
|   }); | ||||
|   const clockD = await createDomainObjectWithDefaults(page, { | ||||
|     type: 'Clock', | ||||
|     name: 'Clock D', | ||||
|     parent: blueFolder.uuid | ||||
|   }); | ||||
|  | ||||
|     const displayLayout = await createDomainObjectWithDefaults(page, { | ||||
|         type: 'Display Layout' | ||||
|     }); | ||||
|   const displayLayout = await createDomainObjectWithDefaults(page, { | ||||
|     type: 'Display Layout' | ||||
|   }); | ||||
|  | ||||
|     // Go back into edit mode for the display layout | ||||
|     await page.locator('button[title="Edit"]').click(); | ||||
|   // Go back into edit mode for the display layout | ||||
|   await page.locator('button[title="Edit"]').click(); | ||||
|  | ||||
|     return { | ||||
|         redFolder, | ||||
|         blueFolder, | ||||
|         clockA, | ||||
|         clockB, | ||||
|         clockC, | ||||
|         clockD, | ||||
|         displayLayout | ||||
|     }; | ||||
|   return { | ||||
|     redFolder, | ||||
|     blueFolder, | ||||
|     clockA, | ||||
|     clockB, | ||||
|     clockC, | ||||
|     clockD, | ||||
|     displayLayout | ||||
|   }; | ||||
| } | ||||
|   | ||||
| @@ -35,25 +35,26 @@ Make no assumptions about the order that elements appear in the DOM. | ||||
|  | ||||
| const { test, expect } = require('../../pluginFixtures'); | ||||
|  | ||||
| test('Verify that the create button appears and that the Folder Domain Object is available for selection', async ({ page }) => { | ||||
| test('Verify that the create button appears and that the Folder Domain Object is available for selection', async ({ | ||||
|   page | ||||
| }) => { | ||||
|   //Go to baseURL | ||||
|   await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|  | ||||
|     //Go to baseURL | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|   //Click the Create button | ||||
|   await page.click('button:has-text("Create")'); | ||||
|  | ||||
|     //Click the Create button | ||||
|     await page.click('button:has-text("Create")'); | ||||
|  | ||||
|     // Verify that Create Folder appears in the dropdown | ||||
|     await expect(page.locator(':nth-match(:text("Folder"), 2)')).toBeEnabled(); | ||||
|   // Verify that Create Folder appears in the dropdown | ||||
|   await expect(page.locator(':nth-match(:text("Folder"), 2)')).toBeEnabled(); | ||||
| }); | ||||
|  | ||||
| test('Verify that My Items Tree appears @ipad', async ({ page, openmctConfig }) => { | ||||
|     const { myItemsFolderName } = openmctConfig; | ||||
|     //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('./'); | ||||
|   const { myItemsFolderName } = openmctConfig; | ||||
|   //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("${myItemsFolderName}")`)).toBeEnabled(); | ||||
|   //My Items to be visible | ||||
|   await expect(page.locator(`a:has-text("${myItemsFolderName}")`)).toBeEnabled(); | ||||
| }); | ||||
|   | ||||
| @@ -22,151 +22,158 @@ | ||||
|  | ||||
| const { test, expect } = require('../../pluginFixtures.js'); | ||||
| const { | ||||
|     createDomainObjectWithDefaults, | ||||
|     openObjectTreeContextMenu | ||||
|   createDomainObjectWithDefaults, | ||||
|   openObjectTreeContextMenu | ||||
| } = require('../../appActions.js'); | ||||
|  | ||||
| test.describe('Main Tree', () => { | ||||
|     test.beforeEach(async ({ page }) => { | ||||
|         await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|   test.beforeEach(async ({ page }) => { | ||||
|     await page.goto('./', { waitUntil: 'domcontentloaded' }); | ||||
|   }); | ||||
|  | ||||
|   test('Creating a child object within a folder and immediately opening it shows the created object in the tree @couchdb', async ({ | ||||
|     page | ||||
|   }) => { | ||||
|     test.info().annotations.push({ | ||||
|       type: 'issue', | ||||
|       description: 'https://github.com/nasa/openmct/issues/5975' | ||||
|     }); | ||||
|  | ||||
|     test('Creating a child object within a folder and immediately opening it shows the created object in the tree @couchdb', async ({ page }) => { | ||||
|         test.info().annotations.push({ | ||||
|             type: 'issue', | ||||
|             description: 'https://github.com/nasa/openmct/issues/5975' | ||||
|         }); | ||||
|  | ||||
|         const folder = await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Folder' | ||||
|         }); | ||||
|  | ||||
|         await page.getByTitle('Show selected item in tree').click(); | ||||
|  | ||||
|         const clock = await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Clock', | ||||
|             parent: folder.uuid | ||||
|         }); | ||||
|  | ||||
|         await expandTreePaneItemByName(page, folder.name); | ||||
|         await assertTreeItemIsVisible(page, clock.name); | ||||
|     const folder = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Folder' | ||||
|     }); | ||||
|  | ||||
|     test('Creating a child object on one tab and expanding its parent on the other shows the correct composition @2p', async ({ page, openmctConfig }) => { | ||||
|         test.info().annotations.push({ | ||||
|             type: 'issue', | ||||
|             description: 'https://github.com/nasa/openmct/issues/6391' | ||||
|         }); | ||||
|     await page.getByTitle('Show selected item in tree').click(); | ||||
|  | ||||
|         const { myItemsFolderName } = openmctConfig; | ||||
|         const page2 = await page.context().newPage(); | ||||
|  | ||||
|         // Both pages: Go to baseURL | ||||
|         await Promise.all([ | ||||
|             page.goto('./', { waitUntil: 'networkidle' }), | ||||
|             page2.goto('./', { waitUntil: 'networkidle' }) | ||||
|         ]); | ||||
|  | ||||
|         const page1Folder = await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Folder' | ||||
|         }); | ||||
|  | ||||
|         await expandTreePaneItemByName(page2, myItemsFolderName); | ||||
|         await assertTreeItemIsVisible(page2, page1Folder.name); | ||||
|     const clock = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Clock', | ||||
|       parent: folder.uuid | ||||
|     }); | ||||
|  | ||||
|     test('Creating a child object on one tab and expanding its parent on the other shows the correct composition @couchdb @2p', async ({ page, openmctConfig }) => { | ||||
|         test.info().annotations.push({ | ||||
|             type: 'issue', | ||||
|             description: 'https://github.com/nasa/openmct/issues/6391' | ||||
|         }); | ||||
|     await expandTreePaneItemByName(page, folder.name); | ||||
|     await assertTreeItemIsVisible(page, clock.name); | ||||
|   }); | ||||
|  | ||||
|         const { myItemsFolderName } = openmctConfig; | ||||
|         const page2 = await page.context().newPage(); | ||||
|  | ||||
|         // Both pages: Go to baseURL | ||||
|         await Promise.all([ | ||||
|             page.goto('./', { waitUntil: 'networkidle' }), | ||||
|             page2.goto('./', { waitUntil: 'networkidle' }) | ||||
|         ]); | ||||
|  | ||||
|         const page1Folder = await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Folder' | ||||
|         }); | ||||
|  | ||||
|         await expandTreePaneItemByName(page2, myItemsFolderName); | ||||
|         await assertTreeItemIsVisible(page2, page1Folder.name); | ||||
|   test('Creating a child object on one tab and expanding its parent on the other shows the correct composition @2p', async ({ | ||||
|     page, | ||||
|     openmctConfig | ||||
|   }) => { | ||||
|     test.info().annotations.push({ | ||||
|       type: 'issue', | ||||
|       description: 'https://github.com/nasa/openmct/issues/6391' | ||||
|     }); | ||||
|  | ||||
|     test('Renaming an object reorders the tree @unstable', async ({ page, openmctConfig }) => { | ||||
|         const { myItemsFolderName } = openmctConfig; | ||||
|     const { myItemsFolderName } = openmctConfig; | ||||
|     const page2 = await page.context().newPage(); | ||||
|  | ||||
|         await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Folder', | ||||
|             name: 'Foo' | ||||
|         }); | ||||
|     // Both pages: Go to baseURL | ||||
|     await Promise.all([ | ||||
|       page.goto('./', { waitUntil: 'networkidle' }), | ||||
|       page2.goto('./', { waitUntil: 'networkidle' }) | ||||
|     ]); | ||||
|  | ||||
|         await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Folder', | ||||
|             name: 'Bar' | ||||
|         }); | ||||
|  | ||||
|         await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Folder', | ||||
|             name: 'Baz' | ||||
|         }); | ||||
|  | ||||
|         const clock1 = await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Clock', | ||||
|             name: 'aaa' | ||||
|         }); | ||||
|  | ||||
|         await createDomainObjectWithDefaults(page, { | ||||
|             type: 'Clock', | ||||
|             name: 'www' | ||||
|         }); | ||||
|  | ||||
|         // Expand the root folder | ||||
|         await expandTreePaneItemByName(page, myItemsFolderName); | ||||
|  | ||||
|         await test.step("Reorders objects with the same tree depth", async () => { | ||||
|             await getAndAssertTreeItems(page, ['aaa', 'Bar', 'Baz', 'Foo', 'www']); | ||||
|             await renameObjectFromContextMenu(page, clock1.url, 'zzz'); | ||||
|             await getAndAssertTreeItems(page, ['Bar', 'Baz', 'Foo', 'www', 'zzz']); | ||||
|         }); | ||||
|  | ||||
|         await test.step("Reorders links to objects as well as original objects", async () => { | ||||
|             await page.click('role=treeitem[name=/Bar/]'); | ||||
|             await page.dragAndDrop('role=treeitem[name=/www/]', '.c-object-view'); | ||||
|             await page.dragAndDrop('role=treeitem[name=/zzz/]', '.c-object-view'); | ||||
|             await page.click('role=treeitem[name=/Baz/]'); | ||||
|             await page.dragAndDrop('role=treeitem[name=/www/]', '.c-object-view'); | ||||
|             await page.dragAndDrop('role=treeitem[name=/zzz/]', '.c-object-view'); | ||||
|             await page.click('role=treeitem[name=/Foo/]'); | ||||
|             await page.dragAndDrop('role=treeitem[name=/www/]', '.c-object-view'); | ||||
|             await page.dragAndDrop('role=treeitem[name=/zzz/]', '.c-object-view'); | ||||
|             // Expand the unopened folders | ||||
|             await expandTreePaneItemByName(page, 'Bar'); | ||||
|             await expandTreePaneItemByName(page, 'Baz'); | ||||
|             await expandTreePaneItemByName(page, 'Foo'); | ||||
|  | ||||
|             await renameObjectFromContextMenu(page, clock1.url, '___'); | ||||
|             await getAndAssertTreeItems(page, | ||||
|                 [ | ||||
|                     "___", | ||||
|                     "Bar", | ||||
|                     "___", | ||||
|                     "www", | ||||
|                     "Baz", | ||||
|                     "___", | ||||
|                     "www", | ||||
|                     "Foo", | ||||
|                     "___", | ||||
|                     "www", | ||||
|                     "www" | ||||
|                 ]); | ||||
|         }); | ||||
|     const page1Folder = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Folder' | ||||
|     }); | ||||
|  | ||||
|     await expandTreePaneItemByName(page2, myItemsFolderName); | ||||
|     await assertTreeItemIsVisible(page2, page1Folder.name); | ||||
|   }); | ||||
|  | ||||
|   test('Creating a child object on one tab and expanding its parent on the other shows the correct composition @couchdb @2p', async ({ | ||||
|     page, | ||||
|     openmctConfig | ||||
|   }) => { | ||||
|     test.info().annotations.push({ | ||||
|       type: 'issue', | ||||
|       description: 'https://github.com/nasa/openmct/issues/6391' | ||||
|     }); | ||||
|  | ||||
|     const { myItemsFolderName } = openmctConfig; | ||||
|     const page2 = await page.context().newPage(); | ||||
|  | ||||
|     // Both pages: Go to baseURL | ||||
|     await Promise.all([ | ||||
|       page.goto('./', { waitUntil: 'networkidle' }), | ||||
|       page2.goto('./', { waitUntil: 'networkidle' }) | ||||
|     ]); | ||||
|  | ||||
|     const page1Folder = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Folder' | ||||
|     }); | ||||
|  | ||||
|     await expandTreePaneItemByName(page2, myItemsFolderName); | ||||
|     await assertTreeItemIsVisible(page2, page1Folder.name); | ||||
|   }); | ||||
|  | ||||
|   test('Renaming an object reorders the tree @unstable', async ({ page, openmctConfig }) => { | ||||
|     const { myItemsFolderName } = openmctConfig; | ||||
|  | ||||
|     await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Folder', | ||||
|       name: 'Foo' | ||||
|     }); | ||||
|  | ||||
|     await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Folder', | ||||
|       name: 'Bar' | ||||
|     }); | ||||
|  | ||||
|     await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Folder', | ||||
|       name: 'Baz' | ||||
|     }); | ||||
|  | ||||
|     const clock1 = await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Clock', | ||||
|       name: 'aaa' | ||||
|     }); | ||||
|  | ||||
|     await createDomainObjectWithDefaults(page, { | ||||
|       type: 'Clock', | ||||
|       name: 'www' | ||||
|     }); | ||||
|  | ||||
|     // Expand the root folder | ||||
|     await expandTreePaneItemByName(page, myItemsFolderName); | ||||
|  | ||||
|     await test.step('Reorders objects with the same tree depth', async () => { | ||||
|       await getAndAssertTreeItems(page, ['aaa', 'Bar', 'Baz', 'Foo', 'www']); | ||||
|       await renameObjectFromContextMenu(page, clock1.url, 'zzz'); | ||||
|       await getAndAssertTreeItems(page, ['Bar', 'Baz', 'Foo', 'www', 'zzz']); | ||||
|     }); | ||||
|  | ||||
|     await test.step('Reorders links to objects as well as original objects', async () => { | ||||
|       await page.click('role=treeitem[name=/Bar/]'); | ||||
|       await page.dragAndDrop('role=treeitem[name=/www/]', '.c-object-view'); | ||||
|       await page.dragAndDrop('role=treeitem[name=/zzz/]', '.c-object-view'); | ||||
|       await page.click('role=treeitem[name=/Baz/]'); | ||||
|       await page.dragAndDrop('role=treeitem[name=/www/]', '.c-object-view'); | ||||
|       await page.dragAndDrop('role=treeitem[name=/zzz/]', '.c-object-view'); | ||||
|       await page.click('role=treeitem[name=/Foo/]'); | ||||
|       await page.dragAndDrop('role=treeitem[name=/www/]', '.c-object-view'); | ||||
|       await page.dragAndDrop('role=treeitem[name=/zzz/]', '.c-object-view'); | ||||
|       // Expand the unopened folders | ||||
|       await expandTreePaneItemByName(page, 'Bar'); | ||||
|       await expandTreePaneItemByName(page, 'Baz'); | ||||
|       await expandTreePaneItemByName(page, 'Foo'); | ||||
|  | ||||
|       await renameObjectFromContextMenu(page, clock1.url, '___'); | ||||
|       await getAndAssertTreeItems(page, [ | ||||
|         '___', | ||||
|         'Bar', | ||||
|         '___', | ||||
|         'www', | ||||
|         'Baz', | ||||
|         '___', | ||||
|         'www', | ||||
|         'Foo', | ||||
|         '___', | ||||
|         'www', | ||||
|         'www' | ||||
|       ]); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| /** | ||||
| @@ -174,22 +181,22 @@ test.describe('Main Tree', () => { | ||||
|  * @param {Array<string>} expected | ||||
|  */ | ||||
| async function getAndAssertTreeItems(page, expected) { | ||||
|     const treeItems = page.locator('[role="treeitem"]'); | ||||
|     const allTexts = await treeItems.allInnerTexts(); | ||||
|     // Get rid of root folder ('My Items') as its position will not change | ||||
|     allTexts.shift(); | ||||
|     expect(allTexts).toEqual(expected); | ||||
|   const treeItems = page.locator('[role="treeitem"]'); | ||||
|   const allTexts = await treeItems.allInnerTexts(); | ||||
|   // Get rid of root folder ('My Items') as its position will not change | ||||
|   allTexts.shift(); | ||||
|   expect(allTexts).toEqual(expected); | ||||
| } | ||||
|  | ||||
| async function assertTreeItemIsVisible(page, name) { | ||||
|     const mainTree = page.getByRole('tree', { | ||||
|         name: 'Main Tree' | ||||
|     }); | ||||
|     const treeItem = mainTree.getByRole('treeitem', { | ||||
|         name | ||||
|     }); | ||||
|   const mainTree = page.getByRole('tree', { | ||||
|     name: 'Main Tree' | ||||
|   }); | ||||
|   const treeItem = mainTree.getByRole('treeitem', { | ||||
|     name | ||||
|   }); | ||||
|  | ||||
|     await expect(treeItem).toBeVisible(); | ||||
|   await expect(treeItem).toBeVisible(); | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -197,14 +204,14 @@ async function assertTreeItemIsVisible(page, name) { | ||||
|  * @param {string} name | ||||
|  */ | ||||
| async function expandTreePaneItemByName(page, name) { | ||||
|     const mainTree = page.getByRole('tree', { | ||||
|         name: 'Main Tree' | ||||
|     }); | ||||
|     const treeItem = mainTree.getByRole('treeitem', { | ||||
|         name, | ||||
|         expanded: false | ||||
|     }); | ||||
|     await treeItem.locator('.c-disclosure-triangle').click(); | ||||
|   const mainTree = page.getByRole('tree', { | ||||
|     name: 'Main Tree' | ||||
|   }); | ||||
|   const treeItem = mainTree.getByRole('treeitem', { | ||||
|     name, | ||||
|     expanded: false | ||||
|   }); | ||||
|   await treeItem.locator('.c-disclosure-triangle').click(); | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -214,10 +221,10 @@ async function expandTreePaneItemByName(page, name) { | ||||
|  * @param {string} newName | ||||
|  */ | ||||
| async function renameObjectFromContextMenu(page, url, newName) { | ||||
|     await openObjectTreeContextMenu(page, url); | ||||
|     await page.click('li:text("Edit Properties")'); | ||||
|     const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]'); | ||||
|     await nameInput.fill(""); | ||||
|     await nameInput.fill(newName); | ||||
|     await page.click('[aria-label="Save"]'); | ||||
|   await openObjectTreeContextMenu(page, url); | ||||
|   await page.click('li:text("Edit Properties")'); | ||||
|   const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]'); | ||||
|   await nameInput.fill(''); | ||||
|   await nameInput.fill(newName); | ||||
|   await page.click('[aria-label="Save"]'); | ||||
| } | ||||
|   | ||||
| @@ -37,141 +37,154 @@ const { test, expect } = require('@playwright/test'); | ||||
| const filePath = 'e2e/test-data/PerformanceDisplayLayout.json'; | ||||
|  | ||||
| test.describe('Performance tests', () => { | ||||
|     test.beforeEach(async ({ page, browser }, testInfo) => { | ||||
|         // Go to baseURL | ||||
|         await page.goto('./', { waitUntil: 'networkidle' }); | ||||
|   test.beforeEach(async ({ page, browser }, testInfo) => { | ||||
|     // Go to baseURL | ||||
|     await page.goto('./', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|         // Click a:has-text("My Items") | ||||
|         await page.locator('a:has-text("My Items")').click({ | ||||
|             button: 'right' | ||||
|         }); | ||||
|  | ||||
|         // Click text=Import from JSON | ||||
|         await page.locator('text=Import from JSON').click(); | ||||
|  | ||||
|         // Upload Performance Display Layout.json | ||||
|         await page.setInputFiles('#fileElem', filePath); | ||||
|  | ||||
|         // Click text=OK | ||||
|         await page.locator('button:has-text("OK")').click(); | ||||
|  | ||||
|         await expect(page.locator('a:has-text("Performance Display Layout Display Layout")')).toBeVisible(); | ||||
|  | ||||
|         //Create a Chrome Performance Timeline trace to store as a test artifact | ||||
|         console.log("\n==== Devtools: startTracing ====\n"); | ||||
|         await browser.startTracing(page, { | ||||
|             path: `${testInfo.outputPath()}-trace.json`, | ||||
|             screenshots: true | ||||
|         }); | ||||
|     // Click a:has-text("My Items") | ||||
|     await page.locator('a:has-text("My Items")').click({ | ||||
|       button: 'right' | ||||
|     }); | ||||
|     test.afterEach(async ({ page, browser}) => { | ||||
|         console.log("\n==== Devtools: stopTracing ====\n"); | ||||
|         await browser.stopTracing(); | ||||
|  | ||||
|         /* Measurement Section | ||||
|     // Click text=Import from JSON | ||||
|     await page.locator('text=Import from JSON').click(); | ||||
|  | ||||
|     // Upload Performance Display Layout.json | ||||
|     await page.setInputFiles('#fileElem', filePath); | ||||
|  | ||||
|     // Click text=OK | ||||
|     await page.locator('button:has-text("OK")').click(); | ||||
|  | ||||
|     await expect( | ||||
|       page.locator('a:has-text("Performance Display Layout Display Layout")') | ||||
|     ).toBeVisible(); | ||||
|  | ||||
|     //Create a Chrome Performance Timeline trace to store as a test artifact | ||||
|     console.log('\n==== Devtools: startTracing ====\n'); | ||||
|     await browser.startTracing(page, { | ||||
|       path: `${testInfo.outputPath()}-trace.json`, | ||||
|       screenshots: true | ||||
|     }); | ||||
|   }); | ||||
|   test.afterEach(async ({ page, browser }) => { | ||||
|     console.log('\n==== Devtools: stopTracing ====\n'); | ||||
|     await browser.stopTracing(); | ||||
|  | ||||
|     /* Measurement Section | ||||
|         / The following section includes a block of performance measurements. | ||||
|         */ | ||||
|         //Get time difference between viewlarge actionability and evaluate time | ||||
|         await page.evaluate(() => (window.performance.measure("machine-time-difference", "viewlarge.start", "viewLarge.start.test"))); | ||||
|     //Get time difference between viewlarge actionability and evaluate time | ||||
|     await page.evaluate(() => | ||||
|       window.performance.measure( | ||||
|         'machine-time-difference', | ||||
|         'viewlarge.start', | ||||
|         'viewLarge.start.test' | ||||
|       ) | ||||
|     ); | ||||
|  | ||||
|         //Get StartTime | ||||
|         const startTime = await page.evaluate(() => window.performance.timing.navigationStart); | ||||
|         console.log('window.performance.timing.navigationStart', startTime); | ||||
|     //Get StartTime | ||||
|     const startTime = await page.evaluate(() => window.performance.timing.navigationStart); | ||||
|     console.log('window.performance.timing.navigationStart', startTime); | ||||
|  | ||||
|         //Get All Performance Marks | ||||
|         const getAllMarksJson = await page.evaluate(() => JSON.stringify(window.performance.getEntriesByType("mark"))); | ||||
|         const getAllMarks = JSON.parse(getAllMarksJson); | ||||
|         console.log('window.performance.getEntriesByType("mark")', getAllMarks); | ||||
|     //Get All Performance Marks | ||||
|     const getAllMarksJson = await page.evaluate(() => | ||||
|       JSON.stringify(window.performance.getEntriesByType('mark')) | ||||
|     ); | ||||
|     const getAllMarks = JSON.parse(getAllMarksJson); | ||||
|     console.log('window.performance.getEntriesByType("mark")', getAllMarks); | ||||
|  | ||||
|         //Get All Performance Measures | ||||
|         const getAllMeasuresJson = await page.evaluate(() => JSON.stringify(window.performance.getEntriesByType("measure"))); | ||||
|         const getAllMeasures = JSON.parse(getAllMeasuresJson); | ||||
|         console.log('window.performance.getEntriesByType("measure")', getAllMeasures); | ||||
|  | ||||
|     }); | ||||
|     /* The following test will navigate to a previously created Performance Display Layout and measure the | ||||
|     //Get All Performance Measures | ||||
|     const getAllMeasuresJson = await page.evaluate(() => | ||||
|       JSON.stringify(window.performance.getEntriesByType('measure')) | ||||
|     ); | ||||
|     const getAllMeasures = JSON.parse(getAllMeasuresJson); | ||||
|     console.log('window.performance.getEntriesByType("measure")', getAllMeasures); | ||||
|   }); | ||||
|   /* The following test will navigate to a previously created Performance Display Layout and measure the | ||||
|     /  following metrics: | ||||
|     /  - ElementResourceTiming | ||||
|     /  - Interaction Timing | ||||
|     */ | ||||
|     test('Embedded View Large for Imagery is performant in Fixed Time', async ({ page, browser }) => { | ||||
|         const client = await page.context().newCDPSession(page); | ||||
|         // Tell the DevTools session to record performance metrics | ||||
|         // https://chromedevtools.github.io/devtools-protocol/tot/Performance/#method-getMetrics | ||||
|         await client.send('Performance.enable'); | ||||
|         // Go to baseURL | ||||
|         await page.goto('./'); | ||||
|   test('Embedded View Large for Imagery is performant in Fixed Time', async ({ page, browser }) => { | ||||
|     const client = await page.context().newCDPSession(page); | ||||
|     // Tell the DevTools session to record performance metrics | ||||
|     // https://chromedevtools.github.io/devtools-protocol/tot/Performance/#method-getMetrics | ||||
|     await client.send('Performance.enable'); | ||||
|     // Go to baseURL | ||||
|     await page.goto('./'); | ||||
|  | ||||
|         // Search Available after Launch | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); | ||||
|         await page.evaluate(() => window.performance.mark("search-available")); | ||||
|         // Fill Search input | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Performance Display Layout'); | ||||
|         await page.evaluate(() => window.performance.mark("search-entered")); | ||||
|         //Search Result Appears and is clicked | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('a:has-text("Performance Display Layout")').first().click(), | ||||
|             page.evaluate(() => window.performance.mark("click-search-result")) | ||||
|         ]); | ||||
|     // Search Available after Launch | ||||
|     await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); | ||||
|     await page.evaluate(() => window.performance.mark('search-available')); | ||||
|     // Fill Search input | ||||
|     await page | ||||
|       .locator('[aria-label="OpenMCT Search"] input[type="search"]') | ||||
|       .fill('Performance Display Layout'); | ||||
|     await page.evaluate(() => window.performance.mark('search-entered')); | ||||
|     //Search Result Appears and is clicked | ||||
|     await Promise.all([ | ||||
|       page.waitForNavigation(), | ||||
|       page.locator('a:has-text("Performance Display Layout")').first().click(), | ||||
|       page.evaluate(() => window.performance.mark('click-search-result')) | ||||
|     ]); | ||||
|  | ||||
|         //Time to Example Imagery Frame loads within Display Layout | ||||
|         await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible'}); | ||||
|         //Time to Example Imagery object loads | ||||
|         await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible'}); | ||||
|  | ||||
|         //Get background-image url from background-image css prop | ||||
|         const backgroundImage = await page.locator('.c-imagery__main-image__background-image'); | ||||
|         let backgroundImageUrl = await backgroundImage.evaluate((el) => { | ||||
|             return window.getComputedStyle(el).getPropertyValue('background-image').match(/url\(([^)]+)\)/)[1]; | ||||
|         }); | ||||
|         backgroundImageUrl = backgroundImageUrl.slice(1, -1); //forgive me, padre | ||||
|         console.log('backgroundImageurl ' + backgroundImageUrl); | ||||
|  | ||||
|         //Get ResourceTiming of background-image jpg | ||||
|         const resourceTimingJson = await page.evaluate((bgImageUrl) => | ||||
|             JSON.stringify(window.performance.getEntriesByName(bgImageUrl).pop()), | ||||
|         backgroundImageUrl | ||||
|         ); | ||||
|         console.log('resourceTimingJson ' + resourceTimingJson); | ||||
|  | ||||
|         //Open Large view | ||||
|         await page.locator('button:has-text("Large View")').click(); //This action includes the performance.mark named 'viewLarge.start' | ||||
|         await page.evaluate(() => window.performance.mark("viewLarge.start.test")); //This is a mark only to compare evaluate timing | ||||
|  | ||||
|         //Time to Imagery Rendered in Large Frame | ||||
|         await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible'}); | ||||
|         await page.evaluate(() => window.performance.mark("background-image-frame")); | ||||
|  | ||||
|         //Time to Example Imagery object loads | ||||
|         await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible'}); | ||||
|         await page.evaluate(() => window.performance.mark("background-image-visible")); | ||||
|  | ||||
|         // Get Current number of images in thumbstrip | ||||
|         await page.waitForSelector('.c-imagery__thumb'); | ||||
|         const thumbCount = await page.locator('.c-imagery__thumb').count(); | ||||
|         console.log('number of thumbs rendered ' + thumbCount); | ||||
|         await page.locator('.c-imagery__thumb').last().click(); | ||||
|  | ||||
|         //Get ResourceTiming of all jpg resources | ||||
|         const resourceTimingJson2 = await page.evaluate(() => | ||||
|             JSON.stringify(window.performance.getEntriesByType('resource')) | ||||
|         ); | ||||
|         const resourceTiming = JSON.parse(resourceTimingJson2); | ||||
|         const jpgResourceTiming = resourceTiming.find((element) => | ||||
|             element.name.includes('.jpg') | ||||
|         ); | ||||
|         console.log('jpgResourceTiming ' + JSON.stringify(jpgResourceTiming)); | ||||
|  | ||||
|         // Click Close Icon | ||||
|         await page.locator('[aria-label="Close"]').click(); | ||||
|         await page.evaluate(() => window.performance.mark("view-large-close-button")); | ||||
|  | ||||
|         //await client.send('HeapProfiler.enable'); | ||||
|         await client.send('HeapProfiler.collectGarbage'); | ||||
|  | ||||
|         let performanceMetrics = await client.send('Performance.getMetrics'); | ||||
|         console.log(performanceMetrics.metrics); | ||||
|     //Time to Example Imagery Frame loads within Display Layout | ||||
|     await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible' }); | ||||
|     //Time to Example Imagery object loads | ||||
|     await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible' }); | ||||
|  | ||||
|     //Get background-image url from background-image css prop | ||||
|     const backgroundImage = await page.locator('.c-imagery__main-image__background-image'); | ||||
|     let backgroundImageUrl = await backgroundImage.evaluate((el) => { | ||||
|       return window | ||||
|         .getComputedStyle(el) | ||||
|         .getPropertyValue('background-image') | ||||
|         .match(/url\(([^)]+)\)/)[1]; | ||||
|     }); | ||||
|     backgroundImageUrl = backgroundImageUrl.slice(1, -1); //forgive me, padre | ||||
|     console.log('backgroundImageurl ' + backgroundImageUrl); | ||||
|  | ||||
|     //Get ResourceTiming of background-image jpg | ||||
|     const resourceTimingJson = await page.evaluate( | ||||
|       (bgImageUrl) => JSON.stringify(window.performance.getEntriesByName(bgImageUrl).pop()), | ||||
|       backgroundImageUrl | ||||
|     ); | ||||
|     console.log('resourceTimingJson ' + resourceTimingJson); | ||||
|  | ||||
|     //Open Large view | ||||
|     await page.locator('button:has-text("Large View")').click(); //This action includes the performance.mark named 'viewLarge.start' | ||||
|     await page.evaluate(() => window.performance.mark('viewLarge.start.test')); //This is a mark only to compare evaluate timing | ||||
|  | ||||
|     //Time to Imagery Rendered in Large Frame | ||||
|     await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible' }); | ||||
|     await page.evaluate(() => window.performance.mark('background-image-frame')); | ||||
|  | ||||
|     //Time to Example Imagery object loads | ||||
|     await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible' }); | ||||
|     await page.evaluate(() => window.performance.mark('background-image-visible')); | ||||
|  | ||||
|     // Get Current number of images in thumbstrip | ||||
|     await page.waitForSelector('.c-imagery__thumb'); | ||||
|     const thumbCount = await page.locator('.c-imagery__thumb').count(); | ||||
|     console.log('number of thumbs rendered ' + thumbCount); | ||||
|     await page.locator('.c-imagery__thumb').last().click(); | ||||
|  | ||||
|     //Get ResourceTiming of all jpg resources | ||||
|     const resourceTimingJson2 = await page.evaluate(() => | ||||
|       JSON.stringify(window.performance.getEntriesByType('resource')) | ||||
|     ); | ||||
|     const resourceTiming = JSON.parse(resourceTimingJson2); | ||||
|     const jpgResourceTiming = resourceTiming.find((element) => element.name.includes('.jpg')); | ||||
|     console.log('jpgResourceTiming ' + JSON.stringify(jpgResourceTiming)); | ||||
|  | ||||
|     // Click Close Icon | ||||
|     await page.locator('[aria-label="Close"]').click(); | ||||
|     await page.evaluate(() => window.performance.mark('view-large-close-button')); | ||||
|  | ||||
|     //await client.send('HeapProfiler.enable'); | ||||
|     await client.send('HeapProfiler.collectGarbage'); | ||||
|  | ||||
|     let performanceMetrics = await client.send('Performance.getMetrics'); | ||||
|     console.log(performanceMetrics.metrics); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -38,82 +38,84 @@ const filePath = 'e2e/test-data/PerformanceDisplayLayout.json'; | ||||
|  | ||||
| // eslint-disable-next-line playwright/no-skipped-test | ||||
| test.describe.skip('Memory Performance tests', () => { | ||||
|     test.beforeEach(async ({ page, browser }, testInfo) => { | ||||
|         // Go to baseURL | ||||
|         await page.goto('./', { waitUntil: 'networkidle' }); | ||||
|   test.beforeEach(async ({ page, browser }, testInfo) => { | ||||
|     // Go to baseURL | ||||
|     await page.goto('./', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|         // Click a:has-text("My Items") | ||||
|         await page.locator('a:has-text("My Items")').click({ | ||||
|             button: 'right' | ||||
|         }); | ||||
|  | ||||
|         // Click text=Import from JSON | ||||
|         await page.locator('text=Import from JSON').click(); | ||||
|  | ||||
|         // Upload Performance Display Layout.json | ||||
|         await page.setInputFiles('#fileElem', filePath); | ||||
|  | ||||
|         // Click text=OK | ||||
|         await page.locator('text=OK').click(); | ||||
|  | ||||
|         await expect(page.locator('a:has-text("Performance Display Layout Display Layout")')).toBeVisible(); | ||||
|     // Click a:has-text("My Items") | ||||
|     await page.locator('a:has-text("My Items")').click({ | ||||
|       button: 'right' | ||||
|     }); | ||||
|  | ||||
|     test('Embedded View Large for Imagery is performant in Fixed Time', async ({ page, browser }) => { | ||||
|     // Click text=Import from JSON | ||||
|     await page.locator('text=Import from JSON').click(); | ||||
|  | ||||
|         await page.goto('./', {waitUntil: 'networkidle'}); | ||||
|     // Upload Performance Display Layout.json | ||||
|     await page.setInputFiles('#fileElem', filePath); | ||||
|  | ||||
|         // To to Search Available after Launch | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); | ||||
|         // Fill Search input | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Performance Display Layout'); | ||||
|         //Search Result Appears and is clicked | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('a:has-text("Performance Display Layout")').first().click() | ||||
|         ]); | ||||
|     // Click text=OK | ||||
|     await page.locator('text=OK').click(); | ||||
|  | ||||
|         //Time to Example Imagery Frame loads within Display Layout | ||||
|         await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible'}); | ||||
|         //Time to Example Imagery object loads | ||||
|         await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible'}); | ||||
|     await expect( | ||||
|       page.locator('a:has-text("Performance Display Layout Display Layout")') | ||||
|     ).toBeVisible(); | ||||
|   }); | ||||
|  | ||||
|         const client = await page.context().newCDPSession(page); | ||||
|         await client.send('HeapProfiler.enable'); | ||||
|         await client.send('HeapProfiler.startSampling'); | ||||
|         // await client.send('HeapProfiler.collectGarbage'); | ||||
|         await client.send('Performance.enable'); | ||||
|   test('Embedded View Large for Imagery is performant in Fixed Time', async ({ page, browser }) => { | ||||
|     await page.goto('./', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|         let performanceMetricsBefore = await client.send('Performance.getMetrics'); | ||||
|         console.log(performanceMetricsBefore.metrics); | ||||
|     // To to Search Available after Launch | ||||
|     await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); | ||||
|     // Fill Search input | ||||
|     await page | ||||
|       .locator('[aria-label="OpenMCT Search"] input[type="search"]') | ||||
|       .fill('Performance Display Layout'); | ||||
|     //Search Result Appears and is clicked | ||||
|     await Promise.all([ | ||||
|       page.waitForNavigation(), | ||||
|       page.locator('a:has-text("Performance Display Layout")').first().click() | ||||
|     ]); | ||||
|  | ||||
|         //await client.send('Performance.disable'); | ||||
|     //Time to Example Imagery Frame loads within Display Layout | ||||
|     await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible' }); | ||||
|     //Time to Example Imagery object loads | ||||
|     await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible' }); | ||||
|  | ||||
|         //Open Large view | ||||
|         await page.locator('button:has-text("Large View")').click(); | ||||
|         await client.send('HeapProfiler.takeHeapSnapshot'); | ||||
|     const client = await page.context().newCDPSession(page); | ||||
|     await client.send('HeapProfiler.enable'); | ||||
|     await client.send('HeapProfiler.startSampling'); | ||||
|     // await client.send('HeapProfiler.collectGarbage'); | ||||
|     await client.send('Performance.enable'); | ||||
|  | ||||
|         //Time to Imagery Rendered in Large Frame | ||||
|         await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible'}); | ||||
|     let performanceMetricsBefore = await client.send('Performance.getMetrics'); | ||||
|     console.log(performanceMetricsBefore.metrics); | ||||
|  | ||||
|         //Time to Example Imagery object loads | ||||
|         await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible'}); | ||||
|     //await client.send('Performance.disable'); | ||||
|  | ||||
|         // Click Close Icon | ||||
|         await page.locator('.c-click-icon').click(); | ||||
|     //Open Large view | ||||
|     await page.locator('button:has-text("Large View")').click(); | ||||
|     await client.send('HeapProfiler.takeHeapSnapshot'); | ||||
|  | ||||
|         //Time to Example Imagery Frame loads within Display Layout | ||||
|         await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible'}); | ||||
|         //Time to Example Imagery object loads | ||||
|         await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible'}); | ||||
|     //Time to Imagery Rendered in Large Frame | ||||
|     await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible' }); | ||||
|  | ||||
|         await client.send('HeapProfiler.collectGarbage'); | ||||
|         //await client.send('Performance.enable'); | ||||
|     //Time to Example Imagery object loads | ||||
|     await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible' }); | ||||
|  | ||||
|         let performanceMetricsAfter = await client.send('Performance.getMetrics'); | ||||
|         console.log(performanceMetricsAfter.metrics); | ||||
|     // Click Close Icon | ||||
|     await page.locator('.c-click-icon').click(); | ||||
|  | ||||
|         //await client.send('Performance.disable'); | ||||
|     //Time to Example Imagery Frame loads within Display Layout | ||||
|     await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible' }); | ||||
|     //Time to Example Imagery object loads | ||||
|     await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible' }); | ||||
|  | ||||
|     }); | ||||
|     await client.send('HeapProfiler.collectGarbage'); | ||||
|     //await client.send('Performance.enable'); | ||||
|  | ||||
|     let performanceMetricsAfter = await client.send('Performance.getMetrics'); | ||||
|     console.log(performanceMetricsAfter.metrics); | ||||
|  | ||||
|     //await client.send('Performance.disable'); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -36,124 +36,131 @@ const { test, expect } = require('@playwright/test'); | ||||
| const notebookFilePath = 'e2e/test-data/PerformanceNotebook.json'; | ||||
|  | ||||
| test.describe('Performance tests', () => { | ||||
|     test.beforeEach(async ({ page, browser }, testInfo) => { | ||||
|         // Go to baseURL | ||||
|         await page.goto('./', { waitUntil: 'networkidle' }); | ||||
|   test.beforeEach(async ({ page, browser }, testInfo) => { | ||||
|     // Go to baseURL | ||||
|     await page.goto('./', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|         // Click a:has-text("My Items") | ||||
|         await page.locator('a:has-text("My Items")').click({ | ||||
|             button: 'right' | ||||
|         }); | ||||
|  | ||||
|         // Click text=Import from JSON | ||||
|         await page.locator('text=Import from JSON').click(); | ||||
|  | ||||
|         // Upload Performance Display Layout.json | ||||
|         await page.setInputFiles('#fileElem', notebookFilePath); | ||||
|  | ||||
|         // TODO Fix this | ||||
|         await page.locator('text=OK >> nth=1').click(); | ||||
|  | ||||
|         await expect(page.locator('a:has-text("Performance Notebook")')).toBeVisible(); | ||||
|  | ||||
|         //Create a Chrome Performance Timeline trace to store as a test artifact | ||||
|         console.log("\n==== Devtools: startTracing ====\n"); | ||||
|         await browser.startTracing(page, { | ||||
|             path: `${testInfo.outputPath()}-trace.json`, | ||||
|             screenshots: true | ||||
|         }); | ||||
|     // Click a:has-text("My Items") | ||||
|     await page.locator('a:has-text("My Items")').click({ | ||||
|       button: 'right' | ||||
|     }); | ||||
|     test.afterEach(async ({ page, browser}) => { | ||||
|         console.log("\n==== Devtools: stopTracing ====\n"); | ||||
|         await browser.stopTracing(); | ||||
|  | ||||
|         /* Measurement Section | ||||
|     // Click text=Import from JSON | ||||
|     await page.locator('text=Import from JSON').click(); | ||||
|  | ||||
|     // Upload Performance Display Layout.json | ||||
|     await page.setInputFiles('#fileElem', notebookFilePath); | ||||
|  | ||||
|     // TODO Fix this | ||||
|     await page.locator('text=OK >> nth=1').click(); | ||||
|  | ||||
|     await expect(page.locator('a:has-text("Performance Notebook")')).toBeVisible(); | ||||
|  | ||||
|     //Create a Chrome Performance Timeline trace to store as a test artifact | ||||
|     console.log('\n==== Devtools: startTracing ====\n'); | ||||
|     await browser.startTracing(page, { | ||||
|       path: `${testInfo.outputPath()}-trace.json`, | ||||
|       screenshots: true | ||||
|     }); | ||||
|   }); | ||||
|   test.afterEach(async ({ page, browser }) => { | ||||
|     console.log('\n==== Devtools: stopTracing ====\n'); | ||||
|     await browser.stopTracing(); | ||||
|  | ||||
|     /* Measurement Section | ||||
|         / The following section includes a block of performance measurements. | ||||
|         */ | ||||
|         const startTime = await page.evaluate(() => window.performance.timing.navigationStart); | ||||
|         console.log('window.performance.timing.navigationStart', startTime); | ||||
|     const startTime = await page.evaluate(() => window.performance.timing.navigationStart); | ||||
|     console.log('window.performance.timing.navigationStart', startTime); | ||||
|  | ||||
|         //Get All Performance Marks | ||||
|         const getAllMarksJson = await page.evaluate(() => JSON.stringify(window.performance.getEntriesByType("mark"))); | ||||
|         const getAllMarks = JSON.parse(getAllMarksJson); | ||||
|         console.log('window.performance.getEntriesByType("mark")', getAllMarks); | ||||
|     //Get All Performance Marks | ||||
|     const getAllMarksJson = await page.evaluate(() => | ||||
|       JSON.stringify(window.performance.getEntriesByType('mark')) | ||||
|     ); | ||||
|     const getAllMarks = JSON.parse(getAllMarksJson); | ||||
|     console.log('window.performance.getEntriesByType("mark")', getAllMarks); | ||||
|  | ||||
|         //Get All Performance Measures | ||||
|         const getAllMeasuresJson = await page.evaluate(() => JSON.stringify(window.performance.getEntriesByType("measure"))); | ||||
|         const getAllMeasures = JSON.parse(getAllMeasuresJson); | ||||
|         console.log('window.performance.getEntriesByType("measure")', getAllMeasures); | ||||
|  | ||||
|     }); | ||||
|     /* The following test will navigate to a previously created Performance Display Layout and measure the | ||||
|     //Get All Performance Measures | ||||
|     const getAllMeasuresJson = await page.evaluate(() => | ||||
|       JSON.stringify(window.performance.getEntriesByType('measure')) | ||||
|     ); | ||||
|     const getAllMeasures = JSON.parse(getAllMeasuresJson); | ||||
|     console.log('window.performance.getEntriesByType("measure")', getAllMeasures); | ||||
|   }); | ||||
|   /* The following test will navigate to a previously created Performance Display Layout and measure the | ||||
|     /  following metrics: | ||||
|     /  - ElementResourceTiming | ||||
|     /  - Interaction Timing | ||||
|     */ | ||||
|     test('Notebook Search, Add Entry, Update Entry are performant', async ({ page, browser }) => { | ||||
|         const client = await page.context().newCDPSession(page); | ||||
|         // Tell the DevTools session to record performance metrics | ||||
|         // https://chromedevtools.github.io/devtools-protocol/tot/Performance/#method-getMetrics | ||||
|         await client.send('Performance.enable'); | ||||
|         // Go to baseURL | ||||
|         await page.goto('./'); | ||||
|   test('Notebook Search, Add Entry, Update Entry are performant', async ({ page, browser }) => { | ||||
|     const client = await page.context().newCDPSession(page); | ||||
|     // Tell the DevTools session to record performance metrics | ||||
|     // https://chromedevtools.github.io/devtools-protocol/tot/Performance/#method-getMetrics | ||||
|     await client.send('Performance.enable'); | ||||
|     // Go to baseURL | ||||
|     await page.goto('./'); | ||||
|  | ||||
|         // To to Search Available after Launch | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); | ||||
|         await page.evaluate(() => window.performance.mark("search-available")); | ||||
|         // Fill Search input | ||||
|         await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Performance Notebook'); | ||||
|         await page.evaluate(() => window.performance.mark("search-entered")); | ||||
|         //Search Result Appears and is clicked | ||||
|         await Promise.all([ | ||||
|             page.waitForNavigation(), | ||||
|             page.locator('a:has-text("Performance Notebook")').first().click(), | ||||
|             page.evaluate(() => window.performance.mark("click-search-result")) | ||||
|         ]); | ||||
|     // To to Search Available after Launch | ||||
|     await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); | ||||
|     await page.evaluate(() => window.performance.mark('search-available')); | ||||
|     // Fill Search input | ||||
|     await page | ||||
|       .locator('[aria-label="OpenMCT Search"] input[type="search"]') | ||||
|       .fill('Performance Notebook'); | ||||
|     await page.evaluate(() => window.performance.mark('search-entered')); | ||||
|     //Search Result Appears and is clicked | ||||
|     await Promise.all([ | ||||
|       page.waitForNavigation(), | ||||
|       page.locator('a:has-text("Performance Notebook")').first().click(), | ||||
|       page.evaluate(() => window.performance.mark('click-search-result')) | ||||
|     ]); | ||||
|  | ||||
|         await page.waitForSelector('.c-tree__item c-tree-and-search__loading loading', {state: 'hidden'}); | ||||
|         await page.evaluate(() => window.performance.mark("search-spinner-gone")); | ||||
|  | ||||
|         await page.waitForSelector('.l-browse-bar__object-name', { state: 'visible'}); | ||||
|         await page.evaluate(() => window.performance.mark("object-title-appears")); | ||||
|  | ||||
|         await page.waitForSelector('.c-notebook__entry >> nth=0', { state: 'visible'}); | ||||
|         await page.evaluate(() => window.performance.mark("notebook-entry-appears")); | ||||
|  | ||||
|         // Click Add new Notebook Entry | ||||
|         await page.locator('.c-notebook__drag-area').click(); | ||||
|         await page.evaluate(() => window.performance.mark("new-notebook-entry-created")); | ||||
|  | ||||
|         // Enter Notebook Entry text | ||||
|         await page.locator('div.c-ne__text').last().fill('New Entry'); | ||||
|         await page.keyboard.press('Enter'); | ||||
|         await page.evaluate(() => window.performance.mark("new-notebook-entry-filled")); | ||||
|  | ||||
|         //Individual Notebook Entry Search | ||||
|         await page.evaluate(() => window.performance.mark("notebook-search-start")); | ||||
|         await page.locator('.c-notebook__search >> input').fill('Existing Entry'); | ||||
|         await page.evaluate(() => window.performance.mark("notebook-search-filled")); | ||||
|         await page.waitForSelector('text=Search Results (3)', { state: 'visible'}); | ||||
|         await page.evaluate(() => window.performance.mark("notebook-search-processed")); | ||||
|         await page.waitForSelector('.c-notebook__entry >> nth=2', { state: 'visible'}); | ||||
|         await page.evaluate(() => window.performance.mark("notebook-search-processed")); | ||||
|  | ||||
|         //Clear Search | ||||
|         await page.locator('.c-search.c-notebook__search .c-search__input').hover(); | ||||
|         await page.locator('.c-search.c-notebook__search .c-search__clear-input').click(); | ||||
|         await page.evaluate(() => window.performance.mark("notebook-search-processed")); | ||||
|  | ||||
|         // Hover on Last | ||||
|         await page.evaluate(() => window.performance.mark("new-notebook-entry-delete")); | ||||
|         await page.locator('div.c-ne__time-and-content').last().hover(); | ||||
|         await page.locator('button[title="Delete this entry"]').last().click(); | ||||
|         await page.locator('button:has-text("Ok")').click(); | ||||
|         await page.waitForSelector('.c-notebook__entry >> nth=3', { state: 'detached'}); | ||||
|         await page.evaluate(() => window.performance.mark("new-notebook-entry-deleted")); | ||||
|  | ||||
|         //await client.send('HeapProfiler.enable'); | ||||
|         await client.send('HeapProfiler.collectGarbage'); | ||||
|  | ||||
|         let performanceMetrics = await client.send('Performance.getMetrics'); | ||||
|         console.log(performanceMetrics.metrics); | ||||
|     await page.waitForSelector('.c-tree__item c-tree-and-search__loading loading', { | ||||
|       state: 'hidden' | ||||
|     }); | ||||
|     await page.evaluate(() => window.performance.mark('search-spinner-gone')); | ||||
|  | ||||
|     await page.waitForSelector('.l-browse-bar__object-name', { state: 'visible' }); | ||||
|     await page.evaluate(() => window.performance.mark('object-title-appears')); | ||||
|  | ||||
|     await page.waitForSelector('.c-notebook__entry >> nth=0', { state: 'visible' }); | ||||
|     await page.evaluate(() => window.performance.mark('notebook-entry-appears')); | ||||
|  | ||||
|     // Click Add new Notebook Entry | ||||
|     await page.locator('.c-notebook__drag-area').click(); | ||||
|     await page.evaluate(() => window.performance.mark('new-notebook-entry-created')); | ||||
|  | ||||
|     // Enter Notebook Entry text | ||||
|     await page.locator('div.c-ne__text').last().fill('New Entry'); | ||||
|     await page.keyboard.press('Enter'); | ||||
|     await page.evaluate(() => window.performance.mark('new-notebook-entry-filled')); | ||||
|  | ||||
|     //Individual Notebook Entry Search | ||||
|     await page.evaluate(() => window.performance.mark('notebook-search-start')); | ||||
|     await page.locator('.c-notebook__search >> input').fill('Existing Entry'); | ||||
|     await page.evaluate(() => window.performance.mark('notebook-search-filled')); | ||||
|     await page.waitForSelector('text=Search Results (3)', { state: 'visible' }); | ||||
|     await page.evaluate(() => window.performance.mark('notebook-search-processed')); | ||||
|     await page.waitForSelector('.c-notebook__entry >> nth=2', { state: 'visible' }); | ||||
|     await page.evaluate(() => window.performance.mark('notebook-search-processed')); | ||||
|  | ||||
|     //Clear Search | ||||
|     await page.locator('.c-search.c-notebook__search .c-search__input').hover(); | ||||
|     await page.locator('.c-search.c-notebook__search .c-search__clear-input').click(); | ||||
|     await page.evaluate(() => window.performance.mark('notebook-search-processed')); | ||||
|  | ||||
|     // Hover on Last | ||||
|     await page.evaluate(() => window.performance.mark('new-notebook-entry-delete')); | ||||
|     await page.locator('div.c-ne__time-and-content').last().hover(); | ||||
|     await page.locator('button[title="Delete this entry"]').last().click(); | ||||
|     await page.locator('button:has-text("Ok")').click(); | ||||
|     await page.waitForSelector('.c-notebook__entry >> nth=3', { state: 'detached' }); | ||||
|     await page.evaluate(() => window.performance.mark('new-notebook-entry-deleted')); | ||||
|  | ||||
|     //await client.send('HeapProfiler.enable'); | ||||
|     await client.send('HeapProfiler.collectGarbage'); | ||||
|  | ||||
|     let performanceMetrics = await client.send('Performance.getMetrics'); | ||||
|     console.log(performanceMetrics.metrics); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -20,7 +20,7 @@ | ||||
|  * this source code distribution or the Licensing information page available | ||||
|  * at runtime from the About dialog for additional information. | ||||
|  *****************************************************************************/ | ||||
|  | ||||
| /* global __dirname */ | ||||
| /* | ||||
| Collection of Visual Tests set to run with modified init scripts to inject plugins not otherwise available in the default contexts. | ||||
|  | ||||
| @@ -40,23 +40,23 @@ const path = require('path'); | ||||
| const CUSTOM_NAME = 'CUSTOM_NAME'; | ||||
|  | ||||
| test.describe('Visual - addInit', () => { | ||||
|     test.use({ | ||||
|         clockOptions: { | ||||
|             now: 0, //Set browser clock to UNIX Epoch | ||||
|             shouldAdvanceTime: false //Don't advance the clock | ||||
|         } | ||||
|   test.use({ | ||||
|     clockOptions: { | ||||
|       now: 0, //Set browser clock to UNIX Epoch | ||||
|       shouldAdvanceTime: false //Don't advance the clock | ||||
|     } | ||||
|   }); | ||||
|  | ||||
|   test('Restricted Notebook is visually correct @addInit @unstable', async ({ page, theme }) => { | ||||
|     await page.addInitScript({ | ||||
|       path: path.join(__dirname, '../../helper', './addInitRestrictedNotebook.js') | ||||
|     }); | ||||
|     //Go to baseURL | ||||
|     await page.goto('./#/browse/mine?hideTree=true', { waitUntil: 'networkidle' }); | ||||
|  | ||||
|     test('Restricted Notebook is visually correct @addInit @unstable', async ({ page, theme }) => { | ||||
|         // eslint-disable-next-line no-undef | ||||
|         await page.addInitScript({ path: path.join(__dirname, '../../helper', './addInitRestrictedNotebook.js') }); | ||||
|         //Go to baseURL | ||||
|         await page.goto('./#/browse/mine?hideTree=true', { waitUntil: 'networkidle' }); | ||||
|     await createDomainObjectWithDefaults(page, { type: CUSTOM_NAME }); | ||||
|  | ||||
|         await createDomainObjectWithDefaults(page, { type: CUSTOM_NAME }); | ||||
|  | ||||
|         // Take a snapshot of the newly created CUSTOM_NAME notebook | ||||
|         await percySnapshot(page, `Restricted Notebook with CUSTOM_NAME (theme: '${theme}')`); | ||||
|  | ||||
|     }); | ||||
|     // Take a snapshot of the newly created CUSTOM_NAME notebook | ||||
|     await percySnapshot(page, `Restricted Notebook with CUSTOM_NAME (theme: '${theme}')`); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user