Compare commits
	
		
			50 Commits
		
	
	
		
			memory-lea
			...
			itc-mode-d
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					a07eb07090 | ||
| 
						 | 
					9f55e4c98e | ||
| 
						 | 
					17a1a74eed | ||
| 
						 | 
					ec95faa57f | ||
| 
						 | 
					488902f436 | ||
| 
						 | 
					0db9a5e2a7 | ||
| 
						 | 
					1950839f80 | ||
| 
						 | 
					0ddb49c11d | ||
| 
						 | 
					5f6cf7b0df | ||
| 
						 | 
					295acdb36b | ||
| 
						 | 
					d68a58f7e0 | ||
| 
						 | 
					4a2d903050 | ||
| 
						 | 
					7aa3762684 | ||
| 
						 | 
					74cfab0ec0 | ||
| 
						 | 
					1ebf566bb7 | ||
| 
						 | 
					784113012e | ||
| 
						 | 
					0260e06222 | ||
| 
						 | 
					266470a755 | ||
| 
						 | 
					d851b01363 | ||
| 
						 | 
					f41df68458 | ||
| 
						 | 
					ac5c579874 | ||
| 
						 | 
					204fb8c0e0 | ||
| 
						 | 
					c605cd7a17 | ||
| 
						 | 
					a5db0f3b71 | ||
| 
						 | 
					62483583eb | ||
| 
						 | 
					6c63773641 | ||
| 
						 | 
					e27e315784 | ||
| 
						 | 
					5dc82742bf | ||
| 
						 | 
					bd4d30f481 | ||
| 
						 | 
					b8322a8311 | ||
| 
						 | 
					2d6c6a6b38 | ||
| 
						 | 
					f4e747a85e | ||
| 
						 | 
					9ccdbcede8 | ||
| 
						 | 
					2e04c686f4 | ||
| 
						 | 
					508c2ebd87 | ||
| 
						 | 
					892963aa0e | ||
| 
						 | 
					34864771b3 | ||
| 
						 | 
					2f1eb7f1bc | ||
| 
						 | 
					2506dfb25d | ||
| 
						 | 
					6ec07490ff | ||
| 
						 | 
					04c76a0d0d | ||
| 
						 | 
					e5d701dea2 | ||
| 
						 | 
					f22f826546 | ||
| 
						 | 
					fc83d88670 | ||
| 
						 | 
					47e4c3af67 | ||
| 
						 | 
					d0cc125867 | ||
| 
						 | 
					65be53ba18 | ||
| 
						 | 
					1e302e9f5e | ||
| 
						 | 
					9e174c40ed | ||
| 
						 | 
					453311272e | 
@@ -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:
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										429
									
								
								.eslintrc.js
									
									
									
									
									
								
							
							
						
						
									
										429
									
								
								.eslintrc.js
									
									
									
									
									
								
							@@ -1,166 +1,271 @@
 | 
			
		||||
const LEGACY_FILES = ['example/**'];
 | 
			
		||||
const LEGACY_FILES = ["example/**"];
 | 
			
		||||
module.exports = {
 | 
			
		||||
  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'
 | 
			
		||||
      }
 | 
			
		||||
    "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"
 | 
			
		||||
    ],
 | 
			
		||||
    '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-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',
 | 
			
		||||
    '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'
 | 
			
		||||
          }
 | 
			
		||||
    "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-nested-ternary': 'off',
 | 
			
		||||
        'no-var': 'off',
 | 
			
		||||
        'one-var': 'off'
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
        "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],
 | 
			
		||||
 | 
			
		||||
        // 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-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": []
 | 
			
		||||
            }
 | 
			
		||||
        ],
 | 
			
		||||
        "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"
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    ]
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +0,0 @@
 | 
			
		||||
# 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,38 +1,39 @@
 | 
			
		||||
 | 
			
		||||
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"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										54
									
								
								.github/workflows/e2e-couchdb.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										54
									
								
								.github/workflows/e2e-couchdb.yml
									
									
									
									
										vendored
									
									
								
							@@ -1,4 +1,4 @@
 | 
			
		||||
name: 'e2e-couchdb'
 | 
			
		||||
name: "e2e-couchdb"
 | 
			
		||||
on:
 | 
			
		||||
  workflow_dispatch:
 | 
			
		||||
  pull_request:
 | 
			
		||||
@@ -7,77 +7,37 @@ on:
 | 
			
		||||
      - opened
 | 
			
		||||
jobs:
 | 
			
		||||
  e2e-couchdb:
 | 
			
		||||
    if: github.event.label.name == 'pr:e2e:couchdb' || github.event.action == 'opened' && github.actor == 'dependabot[bot]'
 | 
			
		||||
    if: ${{ github.event.label.name == 'pr:e2e:couchdb' }} || ${{ github.event.action == 'opened' }}
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    timeout-minutes: 60
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v3
 | 
			
		||||
      - uses: actions/setup-node@v3
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: 'lts/hydrogen'
 | 
			
		||||
 | 
			
		||||
      - name: Cache NPM dependencies
 | 
			
		||||
        uses: actions/cache@v3
 | 
			
		||||
        with:
 | 
			
		||||
          path: ~/.npm
 | 
			
		||||
          key: ${{ runner.os }}-node-${{ hashFiles('**/package.json') }}
 | 
			
		||||
          restore-keys: |
 | 
			
		||||
            ${{ runner.os }}-node-
 | 
			
		||||
      
 | 
			
		||||
      - run: npm install --cache ~/.npm --prefer-offline --no-audit --progress=false
 | 
			
		||||
 | 
			
		||||
      - name: Login to DockerHub
 | 
			
		||||
        uses: docker/login-action@v2 
 | 
			
		||||
        with:
 | 
			
		||||
          username: ${{ secrets.DOCKERHUB_USERNAME }}
 | 
			
		||||
          password: ${{ secrets.DOCKERHUB_TOKEN }}
 | 
			
		||||
      
 | 
			
		||||
          node-version: 'lts/gallium'
 | 
			
		||||
      - 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
 | 
			
		||||
          bash src/plugins/persistence/couch/setup-couchdb.sh
 | 
			
		||||
          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
 | 
			
		||||
        if: success() || failure()
 | 
			
		||||
        uses: actions/upload-artifact@v3
 | 
			
		||||
        with:
 | 
			
		||||
          path: test-results
 | 
			
		||||
 | 
			
		||||
      - name: Archive html test results
 | 
			
		||||
        if: success() || failure()
 | 
			
		||||
        uses: actions/upload-artifact@v3
 | 
			
		||||
        with:
 | 
			
		||||
          path: html-test-results
 | 
			
		||||
 | 
			
		||||
      - name: Remove pr:e2e:couchdb label (if present)
 | 
			
		||||
        if: always()
 | 
			
		||||
        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 ' + labelToRemove + ' label: ${error.message}`);
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										62
									
								
								.github/workflows/e2e-pr.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										62
									
								
								.github/workflows/e2e-pr.yml
									
									
									
									
										vendored
									
									
								
							@@ -1,4 +1,4 @@
 | 
			
		||||
name: 'e2e-pr'
 | 
			
		||||
name: "e2e-pr"
 | 
			
		||||
on:
 | 
			
		||||
  workflow_dispatch:
 | 
			
		||||
  pull_request:
 | 
			
		||||
@@ -7,31 +7,31 @@ on:
 | 
			
		||||
      - opened
 | 
			
		||||
jobs:
 | 
			
		||||
  e2e-full:
 | 
			
		||||
    if: github.event.label.name == 'pr:e2e' || github.event.action == 'opened' && github.actor == 'dependabot[bot]'
 | 
			
		||||
    if: ${{ github.event.label.name == 'pr:e2e' }}
 | 
			
		||||
    runs-on: ${{ matrix.os }}
 | 
			
		||||
    timeout-minutes: 60
 | 
			
		||||
    strategy:
 | 
			
		||||
      matrix:
 | 
			
		||||
        os:
 | 
			
		||||
          - ubuntu-latest
 | 
			
		||||
          - windows-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Trigger Success
 | 
			
		||||
        uses: actions/github-script@v6
 | 
			
		||||
        with:
 | 
			
		||||
          script: |
 | 
			
		||||
            github.rest.issues.createComment({
 | 
			
		||||
              issue_number: context.issue.number,
 | 
			
		||||
              owner: "nasa",
 | 
			
		||||
              repo: "openmct",
 | 
			
		||||
              body: 'Started e2e Run. Follow along: https://github.com/nasa/openmct/actions/runs/' + context.runId
 | 
			
		||||
            })
 | 
			
		||||
      - uses: actions/checkout@v3
 | 
			
		||||
      - uses: actions/setup-node@v3
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: 'lts/hydrogen'
 | 
			
		||||
      
 | 
			
		||||
      - name: Cache NPM dependencies
 | 
			
		||||
        uses: actions/cache@v3
 | 
			
		||||
        with:
 | 
			
		||||
          path: ~/.npm
 | 
			
		||||
          key: ${{ runner.os }}-node-${{ hashFiles('**/package.json') }}
 | 
			
		||||
          restore-keys: |
 | 
			
		||||
            ${{ runner.os }}-node-
 | 
			
		||||
            
 | 
			
		||||
          node-version: '16'
 | 
			
		||||
      - run: npx playwright@1.32.3 install
 | 
			
		||||
      - run: npx playwright install chrome-beta
 | 
			
		||||
      - run: npm install --cache ~/.npm --prefer-offline --no-audit --progress=false
 | 
			
		||||
      - run: npm install
 | 
			
		||||
      - run: npm run test:e2e:full -- --max-failures=40
 | 
			
		||||
      - run: npm run cov:e2e:report || true
 | 
			
		||||
      - shell: bash
 | 
			
		||||
@@ -44,21 +44,25 @@ jobs:
 | 
			
		||||
        uses: actions/upload-artifact@v3
 | 
			
		||||
        with:
 | 
			
		||||
          path: test-results
 | 
			
		||||
 | 
			
		||||
      - name: Remove pr:e2e label (if present)
 | 
			
		||||
        if: always()
 | 
			
		||||
      - name: Test success
 | 
			
		||||
        if: ${{ success() }}
 | 
			
		||||
        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 ' + labelToRemove + ' label: ${error.message}`);
 | 
			
		||||
            }
 | 
			
		||||
            github.rest.issues.createComment({
 | 
			
		||||
              issue_number: context.issue.number,
 | 
			
		||||
              owner: "nasa",
 | 
			
		||||
              repo: "openmct",
 | 
			
		||||
              body: 'Success ✅ ! Build artifacts are here: https://github.com/nasa/openmct/actions/runs/' + context.runId
 | 
			
		||||
            })
 | 
			
		||||
      - name: Test failure
 | 
			
		||||
        if: ${{ failure() }}
 | 
			
		||||
        uses: actions/github-script@v6
 | 
			
		||||
        with:
 | 
			
		||||
          script: |
 | 
			
		||||
            github.rest.issues.createComment({
 | 
			
		||||
              issue_number: context.issue.number,
 | 
			
		||||
              owner: "nasa",
 | 
			
		||||
              repo: "openmct",
 | 
			
		||||
              body: 'Failure ❌ ! Build artifacts are here: https://github.com/nasa/openmct/actions/runs/' + context.runId
 | 
			
		||||
            })
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								.github/workflows/npm-prerelease.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/npm-prerelease.yml
									
									
									
									
										vendored
									
									
								
							@@ -14,7 +14,7 @@ jobs:
 | 
			
		||||
      - uses: actions/checkout@v3
 | 
			
		||||
      - uses: actions/setup-node@v3
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: lts/hydrogen
 | 
			
		||||
          node-version: 16
 | 
			
		||||
      - run: npm install
 | 
			
		||||
      - run: |
 | 
			
		||||
          echo "//registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN" >> ~/.npmrc
 | 
			
		||||
@@ -29,7 +29,7 @@ jobs:
 | 
			
		||||
      - uses: actions/checkout@v3
 | 
			
		||||
      - uses: actions/setup-node@v3
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: lts/hydrogen
 | 
			
		||||
          node-version: 16
 | 
			
		||||
          registry-url: https://registry.npmjs.org/
 | 
			
		||||
      - run: npm install
 | 
			
		||||
      - run: npm publish --access=public --tag unstable
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										48
									
								
								.github/workflows/pr-platform.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										48
									
								
								.github/workflows/pr-platform.yml
									
									
									
									
										vendored
									
									
								
							@@ -1,16 +1,13 @@
 | 
			
		||||
name: 'pr-platform'
 | 
			
		||||
name: "pr-platform"
 | 
			
		||||
on:
 | 
			
		||||
  workflow_dispatch:
 | 
			
		||||
  pull_request:
 | 
			
		||||
    types:
 | 
			
		||||
      - labeled
 | 
			
		||||
      - opened
 | 
			
		||||
    types: [ labeled ]
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  pr-platform:
 | 
			
		||||
    if: github.event.label.name == 'pr:platform' || github.event.action == 'opened' && github.actor == 'dependabot[bot]'
 | 
			
		||||
  e2e-full:
 | 
			
		||||
    if: ${{ github.event.label.name == 'pr:platform' }}
 | 
			
		||||
    runs-on: ${{ matrix.os }}
 | 
			
		||||
    timeout-minutes: 60
 | 
			
		||||
    strategy:
 | 
			
		||||
      fail-fast: false
 | 
			
		||||
      matrix:
 | 
			
		||||
@@ -19,49 +16,18 @@ jobs:
 | 
			
		||||
          - macos-latest
 | 
			
		||||
          - windows-latest
 | 
			
		||||
        node_version:
 | 
			
		||||
          - lts/gallium
 | 
			
		||||
          - lts/hydrogen
 | 
			
		||||
          - 16
 | 
			
		||||
          - 18
 | 
			
		||||
        architecture:
 | 
			
		||||
          - x64
 | 
			
		||||
    
 | 
			
		||||
    name: Node ${{ matrix.node_version }} - ${{ matrix.architecture }} on ${{ matrix.os }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v3
 | 
			
		||||
      
 | 
			
		||||
      - name: Setup node
 | 
			
		||||
        uses: actions/setup-node@v3
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: ${{ matrix.node_version }}
 | 
			
		||||
          architecture: ${{ matrix.architecture }}
 | 
			
		||||
 | 
			
		||||
      - name: Cache NPM dependencies
 | 
			
		||||
        uses: actions/cache@v3
 | 
			
		||||
        with:
 | 
			
		||||
          path: ~/.npm
 | 
			
		||||
          key: ${{ runner.os }}-${{ matrix.node_version }}-${{ hashFiles('**/package.json') }}
 | 
			
		||||
          restore-keys: |
 | 
			
		||||
            ${{ runner.os }}-${{ matrix.node_version }}-
 | 
			
		||||
 | 
			
		||||
      - run: npm install --cache ~/.npm --prefer-offline --no-audit --progress=false
 | 
			
		||||
      
 | 
			
		||||
      - run: npm install
 | 
			
		||||
      - run: npm test
 | 
			
		||||
      
 | 
			
		||||
      - run: npm run lint -- --quiet
 | 
			
		||||
      
 | 
			
		||||
      - name: Remove pr:platform label (if present)
 | 
			
		||||
        if: always()
 | 
			
		||||
        uses: actions/github-script@v6
 | 
			
		||||
        with:
 | 
			
		||||
          script: |
 | 
			
		||||
            const { owner, repo, number } = context.issue;
 | 
			
		||||
            const labelToRemove = 'pr:platform';
 | 
			
		||||
            try {
 | 
			
		||||
              await github.rest.issues.removeLabel({
 | 
			
		||||
                owner,
 | 
			
		||||
                repo,
 | 
			
		||||
                issue_number: number,
 | 
			
		||||
                name: labelToRemove
 | 
			
		||||
              });
 | 
			
		||||
            } catch (error) {
 | 
			
		||||
              core.warning(`Failed to remove ' + labelToRemove + ' label: ${error.message}`);
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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 }}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,27 +0,0 @@
 | 
			
		||||
# 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
 | 
			
		||||
@@ -1,6 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "trailingComma": "none",
 | 
			
		||||
  "singleQuote": true,
 | 
			
		||||
  "printWidth": 100,
 | 
			
		||||
  "endOfLine": "auto"
 | 
			
		||||
}
 | 
			
		||||
@@ -8,155 +8,169 @@ 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'),
 | 
			
		||||
      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'
 | 
			
		||||
    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")
 | 
			
		||||
        }
 | 
			
		||||
      ]
 | 
			
		||||
    }),
 | 
			
		||||
    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 }
 | 
			
		||||
          }
 | 
			
		||||
    },
 | 
			
		||||
    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]"
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        ]
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        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
 | 
			
		||||
  }
 | 
			
		||||
    },
 | 
			
		||||
    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');
 | 
			
		||||
    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
 | 
			
		||||
      }
 | 
			
		||||
                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: "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,7 +116,6 @@ 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__.
 | 
			
		||||
@@ -133,26 +132,25 @@ changes.
 | 
			
		||||
 | 
			
		||||
### Code Standards
 | 
			
		||||
 | 
			
		||||
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.
 | 
			
		||||
JavaScript sources in Open MCT must satisfy the ESLint rules defined in 
 | 
			
		||||
this repository. This is 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).
 | 
			
		||||
@@ -161,25 +159,21 @@ 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:
 | 
			
		||||
@@ -188,24 +182,19 @@ 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
 | 
			
		||||
@@ -217,10 +206,8 @@ The following guidelines are provided for anyone contributing source code to the
 | 
			
		||||
       plugin.js
 | 
			
		||||
       pluginSpec.js
 | 
			
		||||
   ```
 | 
			
		||||
 | 
			
		||||
   is preferable to
 | 
			
		||||
 | 
			
		||||
   ```txt
 | 
			
		||||
   ```
 | 
			
		||||
   - telemetryTable
 | 
			
		||||
       - components
 | 
			
		||||
           TableRow.vue
 | 
			
		||||
@@ -232,7 +219,6 @@ 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.
 | 
			
		||||
 | 
			
		||||
@@ -271,7 +257,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,18 +139,16 @@ 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.
 | 
			
		||||
 | 
			
		||||
|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|
 | 
			
		||||
- `./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.
 | 
			
		||||
 | 
			
		||||
Our functional tests end in `*.e2e.spec.js`, visual tests in `*.visual.spec.js` and performance tests in `*.perf.spec.js`.
 | 
			
		||||
 | 
			
		||||
@@ -160,12 +158,10 @@ 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
 | 
			
		||||
 | 
			
		||||
|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|
 | 
			
		||||
- `./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
 | 
			
		||||
 | 
			
		||||
@@ -173,15 +169,13 @@ Test tags are a great way of organizing tests outside of a file structure. To le
 | 
			
		||||
 | 
			
		||||
Current list of test tags:
 | 
			
		||||
 | 
			
		||||
|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.|
 | 
			
		||||
- `@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
 | 
			
		||||
 | 
			
		||||
@@ -238,8 +232,7 @@ 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:
 | 
			
		||||
 | 
			
		||||
@@ -300,24 +293,13 @@ 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:
 | 
			
		||||
 | 
			
		||||
  ```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.
 | 
			
		||||
  - When possible, navigate directly by URL
 | 
			
		||||
  - Leverage `await page.goto('./', { waitUntil: 'networkidle' });`
 | 
			
		||||
  - 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
 | 
			
		||||
@@ -364,7 +346,7 @@ We leverage the following official Playwright reporters:
 | 
			
		||||
- Tracefile
 | 
			
		||||
- Screenshots
 | 
			
		||||
 | 
			
		||||
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 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 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,7 +55,6 @@
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
@@ -66,58 +65,58 @@ const { expect } = require('@playwright/test');
 | 
			
		||||
 * @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
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -126,17 +125,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);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -145,12 +144,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();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -160,67 +159,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'
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -228,25 +227,23 @@ 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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -256,12 +253,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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -275,25 +272,22 @@ 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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -303,8 +297,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());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -313,15 +307,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();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -329,7 +323,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);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -337,7 +331,7 @@ async function setFixedTimeMode(page) {
 | 
			
		||||
 * @param {import('@playwright/test').Page} page
 | 
			
		||||
 */
 | 
			
		||||
async function setRealTimeMode(page) {
 | 
			
		||||
  await setTimeConductorMode(page, false);
 | 
			
		||||
    await setTimeConductorMode(page, false);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -353,23 +347,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();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -378,8 +372,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);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -388,8 +382,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);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -399,117 +393,31 @@ 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();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 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;
 | 
			
		||||
    // 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();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line no-undef
 | 
			
		||||
module.exports = {
 | 
			
		||||
  createDomainObjectWithDefaults,
 | 
			
		||||
  createNotification,
 | 
			
		||||
  createPlanFromJSON,
 | 
			
		||||
  expandEntireTree,
 | 
			
		||||
  expandTreePaneItemByName,
 | 
			
		||||
  getCanvasPixels,
 | 
			
		||||
  getHashUrlToDomainObject,
 | 
			
		||||
  getFocusedObjectUuid,
 | 
			
		||||
  openObjectTreeContextMenu,
 | 
			
		||||
  setFixedTimeMode,
 | 
			
		||||
  setRealTimeMode,
 | 
			
		||||
  setStartOffset,
 | 
			
		||||
  setEndOffset,
 | 
			
		||||
  selectInspectorTab,
 | 
			
		||||
  waitForPlotsToRender
 | 
			
		||||
    createDomainObjectWithDefaults,
 | 
			
		||||
    createNotification,
 | 
			
		||||
    expandTreePaneItemByName,
 | 
			
		||||
    expandEntireTree,
 | 
			
		||||
    createPlanFromJSON,
 | 
			
		||||
    openObjectTreeContextMenu,
 | 
			
		||||
    getHashUrlToDomainObject,
 | 
			
		||||
    getFocusedObjectUuid,
 | 
			
		||||
    setFixedTimeMode,
 | 
			
		||||
    setRealTimeMode,
 | 
			
		||||
    setStartOffset,
 | 
			
		||||
    setEndOffset,
 | 
			
		||||
    selectInspectorTab
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -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,9 +56,12 @@ 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)));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -69,113 +72,103 @@ 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);
 | 
			
		||||
      }
 | 
			
		||||
    /**
 | 
			
		||||
     * 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
 | 
			
		||||
        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);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
    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(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);
 | 
			
		||||
        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')
 | 
			
		||||
      );
 | 
			
		||||
        // 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);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  /**
 | 
			
		||||
   * 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,71 +1,76 @@
 | 
			
		||||
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.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.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,275 +19,264 @@
 | 
			
		||||
 * 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) {
 | 
			
		||||
  await page.addInitScript({ path: path.join(__dirname, './', 'addInitExampleFaultProvider.js') });
 | 
			
		||||
    // eslint-disable-next-line no-undef
 | 
			
		||||
    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) {
 | 
			
		||||
  await page.addInitScript({
 | 
			
		||||
    path: path.join(__dirname, './', 'addInitExampleFaultProviderStatic.js')
 | 
			
		||||
  });
 | 
			
		||||
    // eslint-disable-next-line no-undef
 | 
			
		||||
    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) {
 | 
			
		||||
  await page.addInitScript({ path: path.join(__dirname, './', 'addInitFaultManagementPlugin.js') });
 | 
			
		||||
    // eslint-disable-next-line no-undef
 | 
			
		||||
    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,53 +32,46 @@ 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);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -89,13 +82,11 @@ 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,77 +9,74 @@ 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
 | 
			
		||||
  },
 | 
			
		||||
  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'
 | 
			
		||||
      }
 | 
			
		||||
    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
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      name: 'MMOC',
 | 
			
		||||
      testMatch: '**/*.e2e.spec.js', // only run e2e tests
 | 
			
		||||
      grepInvert: /@snapshot/,
 | 
			
		||||
      use: {
 | 
			
		||||
        browserName: 'chromium',
 | 
			
		||||
        viewport: {
 | 
			
		||||
          width: 2560,
 | 
			
		||||
          height: 1440
 | 
			
		||||
    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: '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
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    ['junit', { outputFile: '../test-results/results.xml' }],
 | 
			
		||||
    ['github'],
 | 
			
		||||
    ['@deploysentinel/playwright']
 | 
			
		||||
  ]
 | 
			
		||||
    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']
 | 
			
		||||
    ]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
module.exports = config;
 | 
			
		||||
 
 | 
			
		||||
@@ -7,101 +7,98 @@ 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
 | 
			
		||||
  },
 | 
			
		||||
  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'
 | 
			
		||||
      }
 | 
			
		||||
    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
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      name: 'MMOC',
 | 
			
		||||
      testMatch: '**/*.e2e.spec.js', // only run e2e tests
 | 
			
		||||
      grepInvert: /@snapshot/,
 | 
			
		||||
      use: {
 | 
			
		||||
        browserName: 'chromium',
 | 
			
		||||
        viewport: {
 | 
			
		||||
          width: 2560,
 | 
			
		||||
          height: 1440
 | 
			
		||||
    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: '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
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    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,41 +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',
 | 
			
		||||
    launchOptions: {
 | 
			
		||||
      args: ['--js-flags=--expose-gc']
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  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,51 +4,48 @@
 | 
			
		||||
 | 
			
		||||
/** @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
 | 
			
		||||
  },
 | 
			
		||||
  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'
 | 
			
		||||
      }
 | 
			
		||||
    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
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      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
 | 
			
		||||
      }
 | 
			
		||||
    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
 | 
			
		||||
        }]
 | 
			
		||||
    ]
 | 
			
		||||
  ]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
module.exports = config;
 | 
			
		||||
 
 | 
			
		||||
@@ -120,31 +120,34 @@ 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') });
 | 
			
		||||
    // 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 });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 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;
 | 
			
		||||
 | 
			
		||||
@@ -154,10 +157,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,7 +274,10 @@
 | 
			
		||||
            "id": "ac0d7eb1-b485-458f-bd2a-a63aa87a3a8a"
 | 
			
		||||
          }
 | 
			
		||||
        ],
 | 
			
		||||
        "layoutGrid": [10, 10],
 | 
			
		||||
        "layoutGrid": [
 | 
			
		||||
          10,
 | 
			
		||||
          10
 | 
			
		||||
        ],
 | 
			
		||||
        "objectStyles": {
 | 
			
		||||
          "ed63cc29-80e2-4e2b-a472-3d6d4adbf310": {
 | 
			
		||||
            "staticStyle": {
 | 
			
		||||
@@ -1452,7 +1455,9 @@
 | 
			
		||||
                  "id": "64e49fe7-5b36-43db-8347-4550b910de4c",
 | 
			
		||||
                  "telemetry": "any",
 | 
			
		||||
                  "operation": "greaterThan",
 | 
			
		||||
                  "input": ["120"],
 | 
			
		||||
                  "input": [
 | 
			
		||||
                    "120"
 | 
			
		||||
                  ],
 | 
			
		||||
                  "metadata": "sin"
 | 
			
		||||
                }
 | 
			
		||||
              ]
 | 
			
		||||
@@ -1470,7 +1475,10 @@
 | 
			
		||||
                  "id": "59f1c4bf-5d36-450c-9668-6546955fc066",
 | 
			
		||||
                  "telemetry": "any",
 | 
			
		||||
                  "operation": "between",
 | 
			
		||||
                  "input": ["120", "-20"],
 | 
			
		||||
                  "input": [
 | 
			
		||||
                    "120",
 | 
			
		||||
                    "-20"
 | 
			
		||||
                  ],
 | 
			
		||||
                  "metadata": "sin"
 | 
			
		||||
                }
 | 
			
		||||
              ]
 | 
			
		||||
@@ -1488,7 +1496,9 @@
 | 
			
		||||
                  "id": "6707be12-6a6e-4535-bb97-ab5c86f99934",
 | 
			
		||||
                  "telemetry": "any",
 | 
			
		||||
                  "operation": "lessThan",
 | 
			
		||||
                  "input": ["-20"],
 | 
			
		||||
                  "input": [
 | 
			
		||||
                    "-20"
 | 
			
		||||
                  ],
 | 
			
		||||
                  "metadata": "sin"
 | 
			
		||||
                }
 | 
			
		||||
              ]
 | 
			
		||||
@@ -1540,7 +1550,9 @@
 | 
			
		||||
                  "id": "64e49fe7-5b36-43db-8347-4550b910de4c",
 | 
			
		||||
                  "telemetry": "any",
 | 
			
		||||
                  "operation": "greaterThan",
 | 
			
		||||
                  "input": ["120"],
 | 
			
		||||
                  "input": [
 | 
			
		||||
                    "120"
 | 
			
		||||
                  ],
 | 
			
		||||
                  "metadata": "sin"
 | 
			
		||||
                }
 | 
			
		||||
              ]
 | 
			
		||||
@@ -1558,7 +1570,10 @@
 | 
			
		||||
                  "id": "59f1c4bf-5d36-450c-9668-6546955fc066",
 | 
			
		||||
                  "telemetry": "any",
 | 
			
		||||
                  "operation": "between",
 | 
			
		||||
                  "input": ["120", "-20"],
 | 
			
		||||
                  "input": [
 | 
			
		||||
                    "120",
 | 
			
		||||
                    "-20"
 | 
			
		||||
                  ],
 | 
			
		||||
                  "metadata": "sin"
 | 
			
		||||
                }
 | 
			
		||||
              ]
 | 
			
		||||
@@ -1576,7 +1591,9 @@
 | 
			
		||||
                  "id": "6707be12-6a6e-4535-bb97-ab5c86f99934",
 | 
			
		||||
                  "telemetry": "any",
 | 
			
		||||
                  "operation": "lessThan",
 | 
			
		||||
                  "input": ["-20"],
 | 
			
		||||
                  "input": [
 | 
			
		||||
                    "-20"
 | 
			
		||||
                  ],
 | 
			
		||||
                  "metadata": "sin"
 | 
			
		||||
                }
 | 
			
		||||
              ]
 | 
			
		||||
@@ -1628,7 +1645,9 @@
 | 
			
		||||
                  "id": "64e49fe7-5b36-43db-8347-4550b910de4c",
 | 
			
		||||
                  "telemetry": "any",
 | 
			
		||||
                  "operation": "greaterThan",
 | 
			
		||||
                  "input": ["150"],
 | 
			
		||||
                  "input": [
 | 
			
		||||
                    "150"
 | 
			
		||||
                  ],
 | 
			
		||||
                  "metadata": "sin"
 | 
			
		||||
                }
 | 
			
		||||
              ]
 | 
			
		||||
@@ -1646,7 +1665,10 @@
 | 
			
		||||
                  "id": "59f1c4bf-5d36-450c-9668-6546955fc066",
 | 
			
		||||
                  "telemetry": "any",
 | 
			
		||||
                  "operation": "between",
 | 
			
		||||
                  "input": ["50", "-50"],
 | 
			
		||||
                  "input": [
 | 
			
		||||
                    "50",
 | 
			
		||||
                    "-50"
 | 
			
		||||
                  ],
 | 
			
		||||
                  "metadata": "sin"
 | 
			
		||||
                }
 | 
			
		||||
              ]
 | 
			
		||||
@@ -1698,7 +1720,9 @@
 | 
			
		||||
                  "id": "64e49fe7-5b36-43db-8347-4550b910de4c",
 | 
			
		||||
                  "telemetry": "any",
 | 
			
		||||
                  "operation": "greaterThan",
 | 
			
		||||
                  "input": ["150"],
 | 
			
		||||
                  "input": [
 | 
			
		||||
                    "150"
 | 
			
		||||
                  ],
 | 
			
		||||
                  "metadata": "sin"
 | 
			
		||||
                }
 | 
			
		||||
              ]
 | 
			
		||||
@@ -1716,7 +1740,10 @@
 | 
			
		||||
                  "id": "59f1c4bf-5d36-450c-9668-6546955fc066",
 | 
			
		||||
                  "telemetry": "any",
 | 
			
		||||
                  "operation": "between",
 | 
			
		||||
                  "input": ["50", "-50"],
 | 
			
		||||
                  "input": [
 | 
			
		||||
                    "50",
 | 
			
		||||
                    "-50"
 | 
			
		||||
                  ],
 | 
			
		||||
                  "metadata": "sin"
 | 
			
		||||
                }
 | 
			
		||||
              ]
 | 
			
		||||
@@ -2177,4 +2204,4 @@
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "rootId": "45b24009-dfed-4023-a30b-d31f5e3a2d87"
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
@@ -1,90 +1 @@
 | 
			
		||||
{
 | 
			
		||||
  "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,96 +1 @@
 | 
			
		||||
{
 | 
			
		||||
  "openmct": {
 | 
			
		||||
    "6d2fa9fd-f2aa-461a-a1e1-164ac44bec9d": {
 | 
			
		||||
      "identifier": { "key": "6d2fa9fd-f2aa-461a-a1e1-164ac44bec9d", "namespace": "" },
 | 
			
		||||
      "name": "Performance Notebook",
 | 
			
		||||
      "type": "notebook",
 | 
			
		||||
      "configuration": {
 | 
			
		||||
        "defaultSort": "oldest",
 | 
			
		||||
        "entries": {
 | 
			
		||||
          "3e31c412-33ba-4757-8ade-e9821f6ba321": {
 | 
			
		||||
            "8c8f6035-631c-45af-8c24-786c60295335": [
 | 
			
		||||
              {
 | 
			
		||||
                "id": "entry-1652815305457",
 | 
			
		||||
                "createdOn": 1652815305457,
 | 
			
		||||
                "createdBy": "",
 | 
			
		||||
                "text": "Existing Entry 1",
 | 
			
		||||
                "embeds": []
 | 
			
		||||
              },
 | 
			
		||||
              {
 | 
			
		||||
                "id": "entry-1652815313465",
 | 
			
		||||
                "createdOn": 1652815313465,
 | 
			
		||||
                "createdBy": "",
 | 
			
		||||
                "text": "Existing Entry 2",
 | 
			
		||||
                "embeds": []
 | 
			
		||||
              },
 | 
			
		||||
              {
 | 
			
		||||
                "id": "entry-1652815399955",
 | 
			
		||||
                "createdOn": 1652815399955,
 | 
			
		||||
                "createdBy": "",
 | 
			
		||||
                "text": "Existing Entry 3",
 | 
			
		||||
                "embeds": []
 | 
			
		||||
              }
 | 
			
		||||
            ]
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "imageMigrationVer": "v1",
 | 
			
		||||
        "pageTitle": "Page",
 | 
			
		||||
        "sections": [
 | 
			
		||||
          {
 | 
			
		||||
            "id": "3e31c412-33ba-4757-8ade-e9821f6ba321",
 | 
			
		||||
            "isDefault": false,
 | 
			
		||||
            "isSelected": false,
 | 
			
		||||
            "name": "Section1",
 | 
			
		||||
            "pages": [
 | 
			
		||||
              {
 | 
			
		||||
                "id": "8c8f6035-631c-45af-8c24-786c60295335",
 | 
			
		||||
                "isDefault": false,
 | 
			
		||||
                "isSelected": false,
 | 
			
		||||
                "name": "Page1",
 | 
			
		||||
                "pageTitle": "Page"
 | 
			
		||||
              },
 | 
			
		||||
              {
 | 
			
		||||
                "id": "36555942-c9aa-439c-bbdb-0aaf50db50f5",
 | 
			
		||||
                "isDefault": false,
 | 
			
		||||
                "isSelected": false,
 | 
			
		||||
                "name": "Page2",
 | 
			
		||||
                "pageTitle": "Page"
 | 
			
		||||
              }
 | 
			
		||||
            ],
 | 
			
		||||
            "sectionTitle": "Section"
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "id": "dab0bd1d-2c5a-405c-987f-107123d6189a",
 | 
			
		||||
            "isDefault": false,
 | 
			
		||||
            "isSelected": true,
 | 
			
		||||
            "name": "Section2",
 | 
			
		||||
            "pages": [
 | 
			
		||||
              {
 | 
			
		||||
                "id": "f625a86a-cb99-4898-8082-80543c8de534",
 | 
			
		||||
                "isDefault": false,
 | 
			
		||||
                "isSelected": false,
 | 
			
		||||
                "name": "Page1",
 | 
			
		||||
                "pageTitle": "Page"
 | 
			
		||||
              },
 | 
			
		||||
              {
 | 
			
		||||
                "id": "e77ef810-f785-42a7-942e-07e999b79c59",
 | 
			
		||||
                "isDefault": false,
 | 
			
		||||
                "isSelected": true,
 | 
			
		||||
                "name": "Page2",
 | 
			
		||||
                "pageTitle": "Page"
 | 
			
		||||
              }
 | 
			
		||||
            ],
 | 
			
		||||
            "sectionTitle": "Section"
 | 
			
		||||
          }
 | 
			
		||||
        ],
 | 
			
		||||
        "sectionTitle": "Section",
 | 
			
		||||
        "type": "General",
 | 
			
		||||
        "showTime": "0"
 | 
			
		||||
      },
 | 
			
		||||
      "modified": 1652815915219,
 | 
			
		||||
      "location": "mine",
 | 
			
		||||
      "persisted": 1652815915222
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "rootId": "6d2fa9fd-f2aa-461a-a1e1-164ac44bec9d"
 | 
			
		||||
}
 | 
			
		||||
{"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"
 | 
			
		||||
      }
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -19,4 +19,4 @@
 | 
			
		||||
      ]
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
@@ -21,149 +21,145 @@
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
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'
 | 
			
		||||
    });
 | 
			
		||||
        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 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 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);
 | 
			
		||||
        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}`);
 | 
			
		||||
            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'
 | 
			
		||||
    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 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' });
 | 
			
		||||
    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
 | 
			
		||||
    });
 | 
			
		||||
        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 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');
 | 
			
		||||
        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")');
 | 
			
		||||
        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'
 | 
			
		||||
        // 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);
 | 
			
		||||
    });
 | 
			
		||||
    const locatorTreeCollapsedItems = locatorTree.locator('role=treeitem[expanded=false]');
 | 
			
		||||
    expect(await locatorTreeCollapsedItems.count()).toBe(0);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -29,25 +29,27 @@ 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!
 | 
			
		||||
    ]);
 | 
			
		||||
  });
 | 
			
		||||
  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.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.warn('This should result in a pass')),
 | 
			
		||||
      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' });
 | 
			
		||||
 | 
			
		||||
        //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,16 +29,18 @@ 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,15 +21,16 @@
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * 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,39 +27,37 @@ 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,98 +21,91 @@
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * 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({})
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
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('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 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({})
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
    //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 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'
 | 
			
		||||
        //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();
 | 
			
		||||
    });
 | 
			
		||||
    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,31 +28,32 @@ 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
 | 
			
		||||
        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);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    //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,113 +27,93 @@ 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,265 +34,251 @@ 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 }) => {
 | 
			
		||||
    await page.addInitScript({
 | 
			
		||||
      path: path.join(__dirname, '../../helper', 'addInitFileInputObject.js')
 | 
			
		||||
    test.beforeEach(async ({ page }) => {
 | 
			
		||||
        // eslint-disable-next-line no-undef
 | 
			
		||||
        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 }) => {
 | 
			
		||||
    await page.addInitScript({
 | 
			
		||||
      path: path.join(__dirname, '../../helper', 'addNoneditableObject.js')
 | 
			
		||||
    // 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') });
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  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'
 | 
			
		||||
    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();
 | 
			
		||||
    });
 | 
			
		||||
    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'
 | 
			
		||||
    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('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();
 | 
			
		||||
 | 
			
		||||
    await page.goto('./', { waitUntil: 'domcontentloaded' });
 | 
			
		||||
        // Both pages: Go to baseURL
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            page.goto('./', { waitUntil: 'networkidle' }),
 | 
			
		||||
            page2.goto('./', { waitUntil: 'networkidle' })
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
    // Create a new 'Clock' object with default settings
 | 
			
		||||
    const clock = await createDomainObjectWithDefaults(page, {
 | 
			
		||||
      type: 'Clock'
 | 
			
		||||
        //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);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // 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,31 +29,22 @@ const { test, expect } = require('../../baseFixtures.js');
 | 
			
		||||
const path = require('path');
 | 
			
		||||
 | 
			
		||||
test.describe('Persistence operations @addInit', () => {
 | 
			
		||||
  // 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'
 | 
			
		||||
    // 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') });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const menuOptions = page.locator('.c-menu li');
 | 
			
		||||
    test('Non-persistable objects should not show persistence related actions', async ({ page }) => {
 | 
			
		||||
        await page.goto('./', { waitUntil: 'domcontentloaded' });
 | 
			
		||||
 | 
			
		||||
    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 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']);
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,303 +1,276 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * 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,91 +28,85 @@ 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'
 | 
			
		||||
    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);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // 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'
 | 
			
		||||
    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);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // 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,108 +20,66 @@
 | 
			
		||||
 * 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;
 | 
			
		||||
  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.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
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // 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);
 | 
			
		||||
    test("Displays all plan events", async ({ page }) => {
 | 
			
		||||
        await page.goto(ganttChart.url);
 | 
			
		||||
 | 
			
		||||
    // Navigate to the Gantt Chart
 | 
			
		||||
    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();
 | 
			
		||||
 | 
			
		||||
    // 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
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
        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);
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,122 +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.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
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.describe('Time List', () => {
 | 
			
		||||
  test('Create a Time List, add a single Plan to it and verify all the activities are displayed with no milliseconds', async ({
 | 
			
		||||
    page
 | 
			
		||||
  }) => {
 | 
			
		||||
    // Goto baseURL
 | 
			
		||||
    await page.goto('./', { waitUntil: 'domcontentloaded' });
 | 
			
		||||
 | 
			
		||||
    const timelist = await test.step('Create a Time List', async () => {
 | 
			
		||||
      const createdTimeList = await createDomainObjectWithDefaults(page, { type: 'Time List' });
 | 
			
		||||
      const objectName = await page.locator('.l-browse-bar__object-name').innerText();
 | 
			
		||||
      expect(objectName).toBe(createdTimeList.name);
 | 
			
		||||
 | 
			
		||||
      return createdTimeList;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await test.step('Create a Plan and add it to the timelist', async () => {
 | 
			
		||||
      const createdPlan = await createPlanFromJSON(page, {
 | 
			
		||||
        name: 'Test Plan',
 | 
			
		||||
        json: testPlan
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      await page.goto(timelist.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(
 | 
			
		||||
        `${timelist.url}?tc.mode=fixed&tc.startBound=${startBound}&tc.endBound=${endBound}&tc.timeSystem=utc&view=timelist.view`
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      // Verify all events are displayed
 | 
			
		||||
      const eventCount = await page.locator('.js-list-item').count();
 | 
			
		||||
      expect(eventCount).toEqual(testPlan.TEST_GROUP.length);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await test.step('Does not show milliseconds in times', async () => {
 | 
			
		||||
      // Get the first activity
 | 
			
		||||
      const row = await page.locator('.js-list-item').first();
 | 
			
		||||
      // Verify that none fo the times have milliseconds displayed.
 | 
			
		||||
      // Example: 2024-11-17T16:00:00Z is correct and 2024-11-17T16:00:00.000Z is wrong
 | 
			
		||||
 | 
			
		||||
      await expect(row.locator('.--start')).not.toContainText('.');
 | 
			
		||||
      await expect(row.locator('.--end')).not.toContainText('.');
 | 
			
		||||
      await expect(row.locator('.--duration')).not.toContainText('.');
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@@ -24,164 +24,158 @@ 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'
 | 
			
		||||
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);
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // 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'
 | 
			
		||||
 | 
			
		||||
    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();
 | 
			
		||||
 | 
			
		||||
    });
 | 
			
		||||
    //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'
 | 
			
		||||
    // 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
 | 
			
		||||
    });
 | 
			
		||||
    // 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,336 +33,293 @@ 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();
 | 
			
		||||
  });
 | 
			
		||||
  test('condition set object can be modified on @localStorage', async ({ page, openmctConfig }) => {
 | 
			
		||||
    const { myItemsFolderName } = openmctConfig;
 | 
			
		||||
        //Re-verify after reload
 | 
			
		||||
        await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
 | 
			
		||||
        //Assertions on loaded Condition Set in Inspector
 | 
			
		||||
        expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy();
 | 
			
		||||
 | 
			
		||||
    await page.goto(conditionSetUrl, { waitUntil: 'networkidle' });
 | 
			
		||||
    });
 | 
			
		||||
    test('condition set object can be modified on @localStorage', async ({ page, openmctConfig }) => {
 | 
			
		||||
        const { myItemsFolderName } = openmctConfig;
 | 
			
		||||
 | 
			
		||||
    //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');
 | 
			
		||||
        await page.goto(conditionSetUrl, { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
    //Update the Condition Set properties
 | 
			
		||||
    // Click Edit Button
 | 
			
		||||
    await page.locator('text=Conditions View Snapshot >> button').nth(3).click();
 | 
			
		||||
        //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');
 | 
			
		||||
 | 
			
		||||
    //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();
 | 
			
		||||
        //Update the Condition Set properties
 | 
			
		||||
        // Click Edit Button
 | 
			
		||||
        await page.locator('text=Conditions View Snapshot >> button').nth(3).click();
 | 
			
		||||
 | 
			
		||||
    //Verify Main section reflects updated Name Property
 | 
			
		||||
    await expect
 | 
			
		||||
      .soft(page.locator('.l-browse-bar__object-name'))
 | 
			
		||||
      .toContainText('Renamed Condition Set');
 | 
			
		||||
        //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 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 Main section reflects updated Name Property
 | 
			
		||||
        await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Renamed Condition Set');
 | 
			
		||||
 | 
			
		||||
    // 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 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();
 | 
			
		||||
 | 
			
		||||
    //Reload Page
 | 
			
		||||
    await Promise.all([page.reload(), page.waitForLoadState('networkidle')]);
 | 
			
		||||
        // 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 Main section reflects updated Name Property
 | 
			
		||||
    await expect
 | 
			
		||||
      .soft(page.locator('.l-browse-bar__object-name'))
 | 
			
		||||
      .toContainText('Renamed Condition Set');
 | 
			
		||||
        //Reload Page
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            page.reload(),
 | 
			
		||||
            page.waitForLoadState('networkidle')
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
    // 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 Main section reflects updated Name Property
 | 
			
		||||
        await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Renamed Condition Set');
 | 
			
		||||
 | 
			
		||||
    // 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 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();
 | 
			
		||||
 | 
			
		||||
    //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();
 | 
			
		||||
        // 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' });
 | 
			
		||||
 | 
			
		||||
    const numberOfConditionSetsToStart = await page
 | 
			
		||||
      .locator('a:has-text("Unnamed Condition Set Condition Set")')
 | 
			
		||||
      .count();
 | 
			
		||||
        //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();
 | 
			
		||||
 | 
			
		||||
    // 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();
 | 
			
		||||
        const numberOfConditionSetsToStart = await page.locator('a:has-text("Unnamed Condition Set Condition Set")').count();
 | 
			
		||||
 | 
			
		||||
    // Click 'Remove' and press OK
 | 
			
		||||
    await page.locator('li[role="menuitem"]:has-text("Remove")').click();
 | 
			
		||||
    await page.locator('button:has-text("OK")').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();
 | 
			
		||||
 | 
			
		||||
    //Expect Unnamed Condition Set to be removed in Main View
 | 
			
		||||
    const numberOfConditionSetsAtEnd = await page
 | 
			
		||||
      .locator('a:has-text("Unnamed Condition Set Condition Set")')
 | 
			
		||||
      .count();
 | 
			
		||||
        // Click 'Remove' and press OK
 | 
			
		||||
        await page.locator('li[role="menuitem"]:has-text("Remove")').click();
 | 
			
		||||
        await page.locator('button:has-text("OK")').click();
 | 
			
		||||
 | 
			
		||||
    expect(numberOfConditionSetsAtEnd).toEqual(numberOfConditionSetsToStart - 1);
 | 
			
		||||
        //Expect Unnamed Condition Set to be removed in Main View
 | 
			
		||||
        const numberOfConditionSetsAtEnd = await page.locator('a:has-text("Unnamed Condition Set Condition Set")').count();
 | 
			
		||||
 | 
			
		||||
    //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');
 | 
			
		||||
  });
 | 
			
		||||
        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');
 | 
			
		||||
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
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('Can add a condition', async ({ page }) => {
 | 
			
		||||
    // Create a new condition set
 | 
			
		||||
    await createDomainObjectWithDefaults(page, {
 | 
			
		||||
      type: 'Condition Set',
 | 
			
		||||
      name: 'Test Condition Set'
 | 
			
		||||
    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' });
 | 
			
		||||
    });
 | 
			
		||||
    // Change the object to edit mode
 | 
			
		||||
    await page.locator('[title="Edit"]').click();
 | 
			
		||||
    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();
 | 
			
		||||
 | 
			
		||||
    // 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'
 | 
			
		||||
        // 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'
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
    await createDomainObjectWithDefaults(page, {
 | 
			
		||||
      type: 'Sine Wave Generator',
 | 
			
		||||
      name: 'Alpha Sine Wave Generator'
 | 
			
		||||
        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: 'Beta 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('---');
 | 
			
		||||
    });
 | 
			
		||||
    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,255 +21,173 @@
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
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'
 | 
			
		||||
    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);
 | 
			
		||||
    });
 | 
			
		||||
    // Edit Display Layout
 | 
			
		||||
    await page.locator('[title="Edit"]').click();
 | 
			
		||||
    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'
 | 
			
		||||
        // 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);
 | 
			
		||||
    });
 | 
			
		||||
    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 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 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();
 | 
			
		||||
    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();
 | 
			
		||||
 | 
			
		||||
    // 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();
 | 
			
		||||
        // 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(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'
 | 
			
		||||
        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);
 | 
			
		||||
    });
 | 
			
		||||
    // 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);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -282,20 +200,18 @@ 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,231 +25,216 @@ 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');
 | 
			
		||||
        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);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    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,138 +24,130 @@ 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 Sine Wave Generator
 | 
			
		||||
        sineWaveObject = await createDomainObjectWithDefaults(page, {
 | 
			
		||||
            type: 'Sine Wave Generator'
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Create Clock Object
 | 
			
		||||
        clockObject = await createDomainObjectWithDefaults(page, {
 | 
			
		||||
            type: 'Clock'
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
    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();
 | 
			
		||||
 | 
			
		||||
    // Create Clock Object
 | 
			
		||||
    clockObject = await createDomainObjectWithDefaults(page, {
 | 
			
		||||
      type: 'Clock'
 | 
			
		||||
        // 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('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 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);
 | 
			
		||||
    });
 | 
			
		||||
    const sineWaveGeneratorTreeItem = treePane.getByRole('treeitem', {
 | 
			
		||||
      name: new RegExp(sineWaveObject.name)
 | 
			
		||||
    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 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,116 +21,104 @@
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * 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('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.beforeEach(async ({ page }) => {
 | 
			
		||||
        // Open a browser, navigate to the main page, and wait until all networkevents to resolve
 | 
			
		||||
        await page.goto('./', { waitUntil: 'domcontentloaded' });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // 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();
 | 
			
		||||
    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 another sine wave generator within the gauge
 | 
			
		||||
    const swg2 = await createDomainObjectWithDefaults(page, {
 | 
			
		||||
      type: 'Sine Wave Generator',
 | 
			
		||||
      name: `swg-${uuid()}`,
 | 
			
		||||
      parent: gauge.uuid
 | 
			
		||||
        // 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();
 | 
			
		||||
    });
 | 
			
		||||
    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")');
 | 
			
		||||
 | 
			
		||||
    // 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');
 | 
			
		||||
        // 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"]');
 | 
			
		||||
 | 
			
		||||
    // 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'
 | 
			
		||||
        // TODO: Verify changes in the UI
 | 
			
		||||
    });
 | 
			
		||||
    await page.locator('li[title="Remove this object from its containing object."]').click();
 | 
			
		||||
    test('Can edit a single Gauge-specific property', async ({ page }) => {
 | 
			
		||||
        test.info().annotations.push({
 | 
			
		||||
            type: 'issue',
 | 
			
		||||
            description: 'https://github.com/nasa/openmct/issues/5985'
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
    // 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');
 | 
			
		||||
        // 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 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'
 | 
			
		||||
        // TODO: Verify changes in the UI
 | 
			
		||||
    });
 | 
			
		||||
    //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,31 +29,22 @@ 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,54 +1,48 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * 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,201 +21,189 @@
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
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 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 Sine Wave Generator
 | 
			
		||||
    await createDomainObjectWithDefaults(page, {
 | 
			
		||||
      type: 'Sine Wave Generator',
 | 
			
		||||
      name: 'Test Sine Wave Generator',
 | 
			
		||||
      parent: ladTable.uuid
 | 
			
		||||
    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();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    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'
 | 
			
		||||
    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);
 | 
			
		||||
    });
 | 
			
		||||
    // Edit LAD table
 | 
			
		||||
    await page.locator('[title="Edit"]').click();
 | 
			
		||||
    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();
 | 
			
		||||
        // 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();
 | 
			
		||||
        // 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);
 | 
			
		||||
 | 
			
		||||
    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'
 | 
			
		||||
        // 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);
 | 
			
		||||
    });
 | 
			
		||||
    // 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);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -228,20 +216,18 @@ 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,454 +32,405 @@ 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
 | 
			
		||||
  }) => {
 | 
			
		||||
    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');
 | 
			
		||||
    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');
 | 
			
		||||
 | 
			
		||||
    // Add a section
 | 
			
		||||
    await page.locator('.js-sidebar-sections .c-icon-button.icon-plus').click();
 | 
			
		||||
        // 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();
 | 
			
		||||
 | 
			
		||||
    // 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
 | 
			
		||||
  });
 | 
			
		||||
        // 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
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
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')
 | 
			
		||||
    // 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
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
    await page.goto('./', { waitUntil: 'domcontentloaded' });
 | 
			
		||||
    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);
 | 
			
		||||
 | 
			
		||||
    notebookObject = await createDomainObjectWithDefaults(page, {
 | 
			
		||||
      type: NOTEBOOK_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 a new entry is created, it should be focused and selected', async ({ page }) => {
 | 
			
		||||
    // Navigate to the notebook object
 | 
			
		||||
    await page.goto(notebookObject.url);
 | 
			
		||||
    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'
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
    // 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'
 | 
			
		||||
        // 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);
 | 
			
		||||
    });
 | 
			
		||||
    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 page
 | 
			
		||||
      .getByRole('treeitem', { name: overlayPlot.name })
 | 
			
		||||
      .dragTo(page.locator('.c-notebook__drag-area'));
 | 
			
		||||
        await nbUtils.enterTextEntry(page, 'Entry to drop into');
 | 
			
		||||
        await page.dragAndDrop(`role=treeitem[name=/${overlayPlot.name}/]`, 'text=Entry to drop into');
 | 
			
		||||
 | 
			
		||||
    const embed = page.locator('.c-ne__embed__link');
 | 
			
		||||
    const embedName = await embed.innerText();
 | 
			
		||||
        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();
 | 
			
		||||
 | 
			
		||||
    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'
 | 
			
		||||
        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);
 | 
			
		||||
 | 
			
		||||
    // 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"')
 | 
			
		||||
        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();
 | 
			
		||||
    });
 | 
			
		||||
    const embed = existingEntry.locator('.c-ne__embed__link');
 | 
			
		||||
    const embedName = await embed.innerText();
 | 
			
		||||
    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 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);
 | 
			
		||||
        // 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();
 | 
			
		||||
  });
 | 
			
		||||
  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';
 | 
			
		||||
        // Reveal the notebook in the tree
 | 
			
		||||
        await page.getByTitle('Show selected item in tree').click();
 | 
			
		||||
 | 
			
		||||
    // Navigate to the notebook object
 | 
			
		||||
    await page.goto(notebookObject.url);
 | 
			
		||||
        await nbUtils.enterTextEntry(page, `This should be a link: ${TEST_LINK} is it?`);
 | 
			
		||||
 | 
			
		||||
    // Reveal the notebook in the tree
 | 
			
		||||
    await page.getByTitle('Show selected item in tree').click();
 | 
			
		||||
        const validLink = page.locator(`a[href="${TEST_LINK}"]`);
 | 
			
		||||
 | 
			
		||||
    await nbUtils.enterTextEntry(page, `This should be a link: ${TEST_LINK} is it?`);
 | 
			
		||||
        // Start waiting for popup before clicking. Note no await.
 | 
			
		||||
        const popupPromise = page.waitForEvent('popup');
 | 
			
		||||
 | 
			
		||||
    const validLink = page.locator(`a[href="${TEST_LINK}"]`);
 | 
			
		||||
        await validLink.click();
 | 
			
		||||
        const popup = await popupPromise;
 | 
			
		||||
 | 
			
		||||
    // Start waiting for popup before clicking. Note no await.
 | 
			
		||||
    const popupPromise = page.waitForEvent('popup');
 | 
			
		||||
        // Wait for the popup to load.
 | 
			
		||||
        await popup.waitForLoadState();
 | 
			
		||||
        expect.soft(popup.url()).toContain('www.google.com');
 | 
			
		||||
 | 
			
		||||
    await validLink.click();
 | 
			
		||||
    const popup = await popupPromise;
 | 
			
		||||
        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';
 | 
			
		||||
 | 
			
		||||
    // Wait for the popup to load.
 | 
			
		||||
    await popup.waitForLoadState();
 | 
			
		||||
    expect.soft(popup.url()).toContain('www.google.com');
 | 
			
		||||
        // Navigate to the notebook object
 | 
			
		||||
        await page.goto(notebookObject.url);
 | 
			
		||||
 | 
			
		||||
    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';
 | 
			
		||||
        // Reveal the notebook in the tree
 | 
			
		||||
        await page.getByTitle('Show selected item in tree').click();
 | 
			
		||||
 | 
			
		||||
    // Navigate to the notebook object
 | 
			
		||||
    await page.goto(notebookObject.url);
 | 
			
		||||
        await nbUtils.enterTextEntry(page, `This should NOT be a link: ${TEST_LINK} is it?`);
 | 
			
		||||
 | 
			
		||||
    // Reveal the notebook in the tree
 | 
			
		||||
    await page.getByTitle('Show selected item in tree').click();
 | 
			
		||||
        const invalidLink = page.locator(`a[href="${TEST_LINK}"]`);
 | 
			
		||||
 | 
			
		||||
    await nbUtils.enterTextEntry(page, `This should NOT be a link: ${TEST_LINK} is it?`);
 | 
			
		||||
        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';
 | 
			
		||||
 | 
			
		||||
    const invalidLink = page.locator(`a[href="${TEST_LINK}"]`);
 | 
			
		||||
        // Navigate to the notebook object
 | 
			
		||||
        await page.goto(notebookObject.url);
 | 
			
		||||
 | 
			
		||||
    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';
 | 
			
		||||
        // Reveal the notebook in the tree
 | 
			
		||||
        await page.getByTitle('Show selected item in tree').click();
 | 
			
		||||
 | 
			
		||||
    // Navigate to the notebook object
 | 
			
		||||
    await page.goto(notebookObject.url);
 | 
			
		||||
        await nbUtils.enterTextEntry(page, `This should NOT be a link: ${TEST_LINK} is it?`);
 | 
			
		||||
 | 
			
		||||
    // Reveal the notebook in the tree
 | 
			
		||||
    await page.getByTitle('Show selected item in tree').click();
 | 
			
		||||
        const invalidLink = page.locator(`a[href="${TEST_LINK}"]`);
 | 
			
		||||
 | 
			
		||||
    await nbUtils.enterTextEntry(page, `This should NOT be a link: ${TEST_LINK} is it?`);
 | 
			
		||||
        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';
 | 
			
		||||
 | 
			
		||||
    const invalidLink = page.locator(`a[href="${TEST_LINK}"]`);
 | 
			
		||||
        // Navigate to the notebook object
 | 
			
		||||
        await page.goto(notebookObject.url);
 | 
			
		||||
 | 
			
		||||
    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';
 | 
			
		||||
        // Reveal the notebook in the tree
 | 
			
		||||
        await page.getByTitle('Show selected item in tree').click();
 | 
			
		||||
 | 
			
		||||
    // Navigate to the notebook object
 | 
			
		||||
    await page.goto(notebookObject.url);
 | 
			
		||||
        await nbUtils.enterTextEntry(page, `This should be a link: ${INVALID_TEST_LINK} is it?`);
 | 
			
		||||
 | 
			
		||||
    // Reveal the notebook in the tree
 | 
			
		||||
    await page.getByTitle('Show selected item in tree').click();
 | 
			
		||||
        const validLink = page.locator(`a[href="${INVALID_TEST_LINK}"]`);
 | 
			
		||||
 | 
			
		||||
    await nbUtils.enterTextEntry(page, `This should be a link: ${INVALID_TEST_LINK} is it?`);
 | 
			
		||||
        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';
 | 
			
		||||
 | 
			
		||||
    const validLink = page.locator(`a[href="${INVALID_TEST_LINK}"]`);
 | 
			
		||||
        // Navigate to the notebook object
 | 
			
		||||
        await page.goto(notebookObject.url);
 | 
			
		||||
 | 
			
		||||
    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';
 | 
			
		||||
        // Reveal the notebook in the tree
 | 
			
		||||
        await page.getByTitle('Show selected item in tree').click();
 | 
			
		||||
 | 
			
		||||
    // Navigate to the notebook object
 | 
			
		||||
    await page.goto(notebookObject.url);
 | 
			
		||||
        await nbUtils.enterTextEntry(page, `This should be a link: ${TEST_LINK} is it?`);
 | 
			
		||||
 | 
			
		||||
    // Reveal the notebook in the tree
 | 
			
		||||
    await page.getByTitle('Show selected item in tree').click();
 | 
			
		||||
        const validLink = page.locator(`a[href="${TEST_LINK}"]`);
 | 
			
		||||
 | 
			
		||||
    await nbUtils.enterTextEntry(page, `This should be a link: ${TEST_LINK} is it?`);
 | 
			
		||||
        // Start waiting for popup before clicking. Note no await.
 | 
			
		||||
        const popupPromise = page.waitForEvent('popup');
 | 
			
		||||
 | 
			
		||||
    const validLink = page.locator(`a[href="${TEST_LINK}"]`);
 | 
			
		||||
        await validLink.click();
 | 
			
		||||
        const popup = await popupPromise;
 | 
			
		||||
 | 
			
		||||
    // Start waiting for popup before clicking. Note no await.
 | 
			
		||||
    const popupPromise = page.waitForEvent('popup');
 | 
			
		||||
        // Wait for the popup to load.
 | 
			
		||||
        await popup.waitForLoadState();
 | 
			
		||||
        expect.soft(popup.url()).toContain('www.google.com');
 | 
			
		||||
 | 
			
		||||
    await validLink.click();
 | 
			
		||||
    const popup = await popupPromise;
 | 
			
		||||
        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>`;
 | 
			
		||||
 | 
			
		||||
    // Wait for the popup to load.
 | 
			
		||||
    await popup.waitForLoadState();
 | 
			
		||||
    expect.soft(popup.url()).toContain('www.google.com');
 | 
			
		||||
        // Navigate to the notebook object
 | 
			
		||||
        await page.goto(notebookObject.url);
 | 
			
		||||
 | 
			
		||||
    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>`;
 | 
			
		||||
        // Reveal the notebook in the tree
 | 
			
		||||
        await page.getByTitle('Show selected item in tree').click();
 | 
			
		||||
 | 
			
		||||
    // Navigate to the notebook object
 | 
			
		||||
    await page.goto(notebookObject.url);
 | 
			
		||||
        await nbUtils.enterTextEntry(page, `This should be a link, BUT not a bad link: ${TEST_LINK_BAD} is it?`);
 | 
			
		||||
 | 
			
		||||
    // Reveal the notebook in the tree
 | 
			
		||||
    await page.getByTitle('Show selected item in tree').click();
 | 
			
		||||
        const sanitizedLink = page.locator(`a[href="${TEST_LINK}"]`);
 | 
			
		||||
        const unsanitizedLink = page.locator(`a[href="${TEST_LINK_BAD}"]`);
 | 
			
		||||
 | 
			
		||||
    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);
 | 
			
		||||
  });
 | 
			
		||||
        expect.soft(await sanitizedLink.count()).toBe(1);
 | 
			
		||||
        expect(await unsanitizedLink.count()).toBe(0);
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -29,135 +29,106 @@ 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,184 +29,180 @@ 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' });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  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'
 | 
			
		||||
        // Create Notebook
 | 
			
		||||
        testNotebook = await createDomainObjectWithDefaults(page, {type: 'Notebook' });
 | 
			
		||||
        await page.goto(testNotebook.url, { waitUntil: 'networkidle'});
 | 
			
		||||
    });
 | 
			
		||||
    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');
 | 
			
		||||
    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();
 | 
			
		||||
 | 
			
		||||
    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'
 | 
			
		||||
    );
 | 
			
		||||
        // Collect all request events to count and assert after notebook action
 | 
			
		||||
        let notebookElementsRequests = [];
 | 
			
		||||
        page.on('request', (request) => notebookElementsRequests.push(request));
 | 
			
		||||
 | 
			
		||||
    //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();
 | 
			
		||||
  });
 | 
			
		||||
        //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();
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 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');
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -216,17 +212,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');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -236,14 +232,12 @@ 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,12 +19,9 @@
 | 
			
		||||
 * 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');
 | 
			
		||||
 | 
			
		||||
@@ -33,186 +30,190 @@ 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);
 | 
			
		||||
    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);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
  }) => {
 | 
			
		||||
    // 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);
 | 
			
		||||
    test('Can still: add page, rename, add entry, delete unlocked pages @addInit', async ({ page }) => {
 | 
			
		||||
        // Click text=Page Add >> button
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            page.waitForNavigation(),
 | 
			
		||||
            page.locator('text=Page Add >> button').click()
 | 
			
		||||
        ]);
 | 
			
		||||
        // Click text=Unnamed Page >> nth=1
 | 
			
		||||
        await page.locator('text=Unnamed Page').nth(1).click();
 | 
			
		||||
        // Press a with modifiers
 | 
			
		||||
        await page.locator('text=Unnamed Page').nth(1).fill(TEST_TEXT_NAME);
 | 
			
		||||
 | 
			
		||||
    // expect to be able to rename unlocked pages
 | 
			
		||||
    const newPageElement = page.getByText(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.locator(`text=${TEST_TEXT_NAME}`);
 | 
			
		||||
        const newPageCount = await newPageElement.count();
 | 
			
		||||
        await newPageElement.press('Enter'); // exit contenteditable state
 | 
			
		||||
        expect.soft(newPageCount).toEqual(1);
 | 
			
		||||
 | 
			
		||||
    // enter test text
 | 
			
		||||
    await nbUtils.enterTextEntry(page, TEST_TEXT);
 | 
			
		||||
        // enter test text
 | 
			
		||||
        await nbUtils.enterTextEntry(page, TEST_TEXT);
 | 
			
		||||
 | 
			
		||||
    // expect new page to be lockable
 | 
			
		||||
    const commitButton = page.getByRole('button', { name: ' Commit Entries' });
 | 
			
		||||
    expect.soft(await commitButton.count()).toEqual(1);
 | 
			
		||||
        // expect new page to be lockable
 | 
			
		||||
        const commitButton = page.locator('BUTTON:HAS-TEXT("COMMIT ENTRIES")');
 | 
			
		||||
        expect.soft(await commitButton.count()).toEqual(1);
 | 
			
		||||
 | 
			
		||||
    // 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();
 | 
			
		||||
        // 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()
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
    // deleted page, should no longer exist
 | 
			
		||||
    const deletedPageElement = page.getByText(TEST_TEXT_NAME);
 | 
			
		||||
    expect(await deletedPageElement.count()).toEqual(0);
 | 
			
		||||
  });
 | 
			
		||||
        // deleted page, should no longer exist
 | 
			
		||||
        const deletedPageElement = page.locator(`text=${TEST_TEXT_NAME}`);
 | 
			
		||||
        expect(await deletedPageElement.count()).toEqual(0);
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test.describe('Restricted Notebook with a page locked and with an embed @addInit', () => {
 | 
			
		||||
  test.beforeEach(async ({ page }) => {
 | 
			
		||||
    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.beforeEach(async ({ page }) => {
 | 
			
		||||
        const notebook = await startAndAddRestrictedNotebookObject(page);
 | 
			
		||||
        await nbUtils.dragAndDropEmbed(page, notebook);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const embedMenu = page.locator('body >> .c-menu');
 | 
			
		||||
    await expect(embedMenu).toContainText('Remove This Embed');
 | 
			
		||||
  });
 | 
			
		||||
    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('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).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');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    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) {
 | 
			
		||||
  await page.addInitScript({
 | 
			
		||||
    path: path.join(__dirname, '../../../../helper/', 'addInitRestrictedNotebook.js')
 | 
			
		||||
  });
 | 
			
		||||
  await page.goto('./', { waitUntil: 'domcontentloaded' });
 | 
			
		||||
    // eslint-disable-next-line no-undef
 | 
			
		||||
    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,247 +29,243 @@ 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'
 | 
			
		||||
        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();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Go back into edit mode for the display layout
 | 
			
		||||
    await page.locator('button[title="Edit"]').click();
 | 
			
		||||
    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 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 expect(page.locator('[aria-label="Tags Inspector"]')).toContainText("Science");
 | 
			
		||||
        await expect(page.locator('[aria-label="Tags Inspector"]')).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'
 | 
			
		||||
        await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc');
 | 
			
		||||
        await expect(page.locator('[aria-label="Search Result"]')).not.toContainText("Driving");
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await createNotebookEntryAndTags(page);
 | 
			
		||||
    test('Can delete entries without tags', async ({ page }) => {
 | 
			
		||||
        test.info().annotations.push({
 | 
			
		||||
            type: 'issue',
 | 
			
		||||
            description: 'https://github.com/nasa/openmct/issues/5823'
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
    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 createNotebookEntryAndTags(page);
 | 
			
		||||
 | 
			
		||||
    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.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');
 | 
			
		||||
 | 
			
		||||
  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.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.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' });
 | 
			
		||||
    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' });
 | 
			
		||||
 | 
			
		||||
    const ITERATIONS = 4;
 | 
			
		||||
    const notebook = await createNotebookEntryAndTags(page, ITERATIONS);
 | 
			
		||||
    await page.goto(notebook.url);
 | 
			
		||||
        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' });
 | 
			
		||||
 | 
			
		||||
    // 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');
 | 
			
		||||
    }
 | 
			
		||||
        const ITERATIONS = 4;
 | 
			
		||||
        const notebook = await createNotebookEntryAndTags(page, ITERATIONS);
 | 
			
		||||
        await page.goto(notebook.url);
 | 
			
		||||
 | 
			
		||||
    //Reload Page
 | 
			
		||||
    await page.reload({ waitUntil: 'domcontentloaded' });
 | 
			
		||||
        // 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 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);
 | 
			
		||||
        //Reload Page
 | 
			
		||||
        await page.reload({ waitUntil: 'domcontentloaded' });
 | 
			
		||||
 | 
			
		||||
    await selectInspectorTab(page, 'Annotations');
 | 
			
		||||
        // 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);
 | 
			
		||||
 | 
			
		||||
    // Click on the "Add Tag" button
 | 
			
		||||
    await page.locator('button:has-text("Add Tag")').click();
 | 
			
		||||
        await selectInspectorTab(page, 'Annotations');
 | 
			
		||||
 | 
			
		||||
    // Click inside the AutoComplete field
 | 
			
		||||
    await page.locator('[placeholder="Type to select tag"]').click();
 | 
			
		||||
        // Click on the "Add Tag" button
 | 
			
		||||
        await page.locator('button:has-text("Add 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 inside the AutoComplete field
 | 
			
		||||
        await page.locator('[placeholder="Type to select tag"]').click();
 | 
			
		||||
 | 
			
		||||
    // Verify there is a button with text "Add Tag"
 | 
			
		||||
    await expect(page.locator('button:has-text("Add Tag")')).toBeVisible();
 | 
			
		||||
        // Click on the "Tags" header (simulating a click outside the autocomplete)
 | 
			
		||||
        await page.locator('div.c-inspect-properties__header:has-text("Tags")').click();
 | 
			
		||||
 | 
			
		||||
    // Verify the AutoComplete field is hidden
 | 
			
		||||
    await expect(page.locator('[placeholder="Type to select tag"]')).toBeHidden();
 | 
			
		||||
  });
 | 
			
		||||
        // 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();
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -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,120 +38,119 @@ 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
 | 
			
		||||
    await page.addInitScript({
 | 
			
		||||
      path: path.join(__dirname, '../../../../helper/', 'addInitExampleUser.js')
 | 
			
		||||
    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' });
 | 
			
		||||
    });
 | 
			
		||||
    await page.addInitScript({
 | 
			
		||||
      path: path.join(__dirname, '../../../../helper/', 'addInitOperatorStatus.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.goto('./', { waitUntil: 'domcontentloaded' });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // 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();
 | 
			
		||||
    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();
 | 
			
		||||
 | 
			
		||||
    // expect default status to be 'GO'
 | 
			
		||||
    await expect(page.locator('.c-status-poll-panel')).toBeVisible();
 | 
			
		||||
  });
 | 
			
		||||
        // should still be visible
 | 
			
		||||
        await expect(page.locator('div[title="Set the current poll question"]')).toBeVisible();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
  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 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();
 | 
			
		||||
 | 
			
		||||
    // should still be visible
 | 
			
		||||
    await expect(page.locator('div[title="Set the current poll question"]')).toBeVisible();
 | 
			
		||||
  });
 | 
			
		||||
        // get user role value
 | 
			
		||||
        const userRole = page.locator('.c-status-poll-panel__user-role');
 | 
			
		||||
        const userRoleText = await userRole.innerText();
 | 
			
		||||
 | 
			
		||||
  // 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();
 | 
			
		||||
        // get selected status value
 | 
			
		||||
        const selectStatus = page.locator('select[name="setStatus"]');
 | 
			
		||||
        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"]');
 | 
			
		||||
    await selectStatus.selectOption({ index: 1 });
 | 
			
		||||
    const initialStatusValue = await selectStatus.inputValue();
 | 
			
		||||
        // 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();
 | 
			
		||||
 | 
			
		||||
    // 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');
 | 
			
		||||
 | 
			
		||||
    // 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();
 | 
			
		||||
        expect(updatedRowValuesArr[COLUMN_STATUS_INDEX].toLowerCase())
 | 
			
		||||
            .toEqual(updatedStatusValue.toLowerCase());
 | 
			
		||||
 | 
			
		||||
    const updatedRow = page.locator(`tr:has-text("${userRoleText}")`);
 | 
			
		||||
    const updatedRowValues = await updatedRow.innerText();
 | 
			
		||||
    const updatedRowValuesArr = updatedRowValues.split('\t');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    expect(updatedRowValuesArr[COLUMN_STATUS_INDEX].toLowerCase()).toEqual(
 | 
			
		||||
      updatedStatusValue.toLowerCase()
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
    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();
 | 
			
		||||
 | 
			
		||||
  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 user role value
 | 
			
		||||
        const userRole = page.locator('.c-status-poll-panel__user-role');
 | 
			
		||||
        const userRoleText = await userRole.innerText();
 | 
			
		||||
 | 
			
		||||
    // get user role value
 | 
			
		||||
    const userRole = page.locator('.c-status-poll-panel__user-role');
 | 
			
		||||
    const userRoleText = await userRole.innerText();
 | 
			
		||||
        // 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 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();
 | 
			
		||||
        // 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());
 | 
			
		||||
 | 
			
		||||
    // 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()
 | 
			
		||||
    );
 | 
			
		||||
        // clear the poll
 | 
			
		||||
        await page.locator('button[title="Clear the previous poll question"]').click();
 | 
			
		||||
 | 
			
		||||
    // clear the poll
 | 
			
		||||
    await page.locator('button[title="Clear the previous poll question"]').click();
 | 
			
		||||
        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);
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
  test.fixme('iterate through all possible response values', async ({ page }) => {
 | 
			
		||||
    // test all possible respone values for the poll
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -27,95 +27,81 @@ 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
 | 
			
		||||
      }
 | 
			
		||||
        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' });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    //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' });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -123,20 +109,16 @@ 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);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -144,57 +126,54 @@ async function setTimeRange(
 | 
			
		||||
 * @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();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -203,23 +182,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,50 +29,44 @@ 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);
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -81,149 +75,146 @@ 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' });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -232,63 +223,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,56 +27,55 @@ 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());
 | 
			
		||||
      }
 | 
			
		||||
        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);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    //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);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -84,42 +83,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()
 | 
			
		||||
    ]);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -127,20 +126,17 @@ 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' });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -148,14 +144,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,245 +26,261 @@ necessarily be used for reference when writing new tests in this area.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
const { test, expect } = require('../../../../pluginFixtures');
 | 
			
		||||
const {
 | 
			
		||||
  createDomainObjectWithDefaults,
 | 
			
		||||
  getCanvasPixels,
 | 
			
		||||
  selectInspectorTab,
 | 
			
		||||
  waitForPlotsToRender
 | 
			
		||||
} = require('../../../../appActions');
 | 
			
		||||
const { createDomainObjectWithDefaults, selectInspectorTab } = require('../../../../appActions');
 | 
			
		||||
 | 
			
		||||
test.describe('Overlay Plot', () => {
 | 
			
		||||
  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.beforeEach(async ({ page }) => {
 | 
			
		||||
        await page.goto('./', { waitUntil: 'domcontentloaded' });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await createDomainObjectWithDefaults(page, {
 | 
			
		||||
      type: 'Sine Wave Generator',
 | 
			
		||||
      parent: overlayPlot.uuid
 | 
			
		||||
    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 page.goto(overlayPlot.url);
 | 
			
		||||
    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 selectInspectorTab(page, 'Config');
 | 
			
		||||
        const swgA = await createDomainObjectWithDefaults(page, {
 | 
			
		||||
            type: "Sine Wave Generator",
 | 
			
		||||
            parent: overlayPlot.uuid
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
    // 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)');
 | 
			
		||||
  });
 | 
			
		||||
        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);
 | 
			
		||||
 | 
			
		||||
  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'
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const swgA = await createDomainObjectWithDefaults(page, {
 | 
			
		||||
      type: 'Sine Wave Generator',
 | 
			
		||||
      parent: overlayPlot.uuid
 | 
			
		||||
    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();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await page.goto(overlayPlot.url);
 | 
			
		||||
    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"
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
    // 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);
 | 
			
		||||
        const swgA = await createDomainObjectWithDefaults(page, {
 | 
			
		||||
            type: "Sine Wave Generator",
 | 
			
		||||
            parent: overlayPlot.uuid
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
    // Enter edit mode
 | 
			
		||||
    await page.click('button[title="Edit"]');
 | 
			
		||||
        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"]');
 | 
			
		||||
 | 
			
		||||
    // 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 selectInspectorTab(page, 'Elements');
 | 
			
		||||
 | 
			
		||||
    await assertLimitLinesExistAndAreVisible(page);
 | 
			
		||||
        await page.locator(`#inspector-elements-tree >> text=${swgA.name}`).click();
 | 
			
		||||
 | 
			
		||||
    // Save (exit edit mode)
 | 
			
		||||
    await page.locator('button[title="Save"]').click();
 | 
			
		||||
    await page.locator('li[title="Save and Finish Editing"]').click();
 | 
			
		||||
        // Wait for "View Large" plot series data to load and be drawn
 | 
			
		||||
        await expect(page.locator('.c-overlay .js-series-data-loaded')).toBeVisible();
 | 
			
		||||
 | 
			
		||||
    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 plotPixelSize = await getCanvasPixelsWithData(page);
 | 
			
		||||
        expect(plotPixelSize).toBeGreaterThan(0);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    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);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Asserts that limit lines exist and are visible
 | 
			
		||||
 * @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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 *
 | 
			
		||||
 * @param {import('@playwright/test').Page} page
 | 
			
		||||
 */
 | 
			
		||||
async function assertLimitLinesExistAndAreVisible(page) {
 | 
			
		||||
  // 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();
 | 
			
		||||
  }
 | 
			
		||||
    // 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();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										113
									
								
								e2e/tests/functional/plugins/plot/plotLegendSwatch.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								e2e/tests/functional/plugins/plot/plotLegendSwatch.e2e.spec.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,113 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * 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,46 +21,44 @@
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * 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, getCanvasPixels } = require('../../../../appActions');
 | 
			
		||||
const { createDomainObjectWithDefaults} = require('../../../../appActions');
 | 
			
		||||
 | 
			
		||||
test.describe('Plot Rendering', () => {
 | 
			
		||||
  let sineWaveGeneratorObject;
 | 
			
		||||
test.describe('Plot Integrity Testing @unstable', () => {
 | 
			
		||||
    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) => {
 | 
			
		||||
      createMineFolderRequests.push(req);
 | 
			
		||||
    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);
 | 
			
		||||
    });
 | 
			
		||||
    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 plotPixels = await getCanvasPixels(page, 'canvas');
 | 
			
		||||
    const plotPixelSize = plotPixels.length;
 | 
			
		||||
    expect(plotPixelSize).toBeGreaterThan(0);
 | 
			
		||||
  });
 | 
			
		||||
        //Get pixel data from Canvas
 | 
			
		||||
        const plotPixelSize = await getCanvasPixelsWithData(page);
 | 
			
		||||
        expect(plotPixelSize).toBeGreaterThan(0);
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -71,24 +69,71 @@ test.describe('Plot Rendering', () => {
 | 
			
		||||
 * @returns {Promise<CreatedObjectInfo>} An object containing information about the edited domain object.
 | 
			
		||||
 */
 | 
			
		||||
async function editSineWaveToUseInfinityOption(page, sineWaveGeneratorObject) {
 | 
			
		||||
  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();
 | 
			
		||||
    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
 | 
			
		||||
    .getByRole('button', {
 | 
			
		||||
      name: 'Save'
 | 
			
		||||
    })
 | 
			
		||||
    .click();
 | 
			
		||||
    // 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')
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
  // 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);
 | 
			
		||||
    // 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;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -21,89 +21,77 @@
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * 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' });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  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
 | 
			
		||||
        // Create the Scatter Plot
 | 
			
		||||
        scatterPlot = await createDomainObjectWithDefaults(page, { type: 'Scatter Plot' });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // 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();
 | 
			
		||||
    test('Can add and remove telemetry sources', async ({ page }) => {
 | 
			
		||||
        const editButton = page.locator('button[title="Edit"]');
 | 
			
		||||
        const saveButton = page.locator('button[title="Save"]');
 | 
			
		||||
 | 
			
		||||
    // Create another sine wave generator within the scatter plot
 | 
			
		||||
    const swg2 = await createDomainObjectWithDefaults(page, {
 | 
			
		||||
      type: 'Sine Wave Generator',
 | 
			
		||||
      name: `swg-${uuid()}`,
 | 
			
		||||
      parent: scatterPlot.uuid
 | 
			
		||||
        // 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();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // 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,202 +29,161 @@ 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'
 | 
			
		||||
        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
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    swgA = 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);
 | 
			
		||||
    });
 | 
			
		||||
    swgB = 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}`);
 | 
			
		||||
    });
 | 
			
		||||
    swgC = 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);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  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,250 +25,243 @@ Tests to verify plot tagging functionality.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
const { test, expect } = require('../../../../pluginFixtures');
 | 
			
		||||
const {
 | 
			
		||||
  createDomainObjectWithDefaults,
 | 
			
		||||
  setRealTimeMode,
 | 
			
		||||
  setFixedTimeMode,
 | 
			
		||||
  waitForPlotsToRender
 | 
			
		||||
} = require('../../../../appActions');
 | 
			
		||||
const { createDomainObjectWithDefaults, setRealTimeMode, setFixedTimeMode } = 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
 | 
			
		||||
      }
 | 
			
		||||
        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' });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    //Alt Drag End
 | 
			
		||||
    await page.keyboard.up('Alt');
 | 
			
		||||
    await page.keyboard.up('Shift');
 | 
			
		||||
    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();
 | 
			
		||||
 | 
			
		||||
    //Wait for canvas to stablize.
 | 
			
		||||
    await canvas.hover({ trial: true });
 | 
			
		||||
        const overlayPlot = await createDomainObjectWithDefaults(page, {
 | 
			
		||||
            type: "Overlay Plot"
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
    // 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();
 | 
			
		||||
        const alphaSineWave = await createDomainObjectWithDefaults(page, {
 | 
			
		||||
            type: "Sine Wave Generator",
 | 
			
		||||
            name: "Alpha 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 createDomainObjectWithDefaults(page, {
 | 
			
		||||
            type: "Sine Wave Generator",
 | 
			
		||||
            name: "Beta Sine Wave",
 | 
			
		||||
            parent: overlayPlot.uuid
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * 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 page.goto(overlayPlot.url);
 | 
			
		||||
 | 
			
		||||
    await expect(page.getByText('No tags to display for this item')).toBeVisible();
 | 
			
		||||
        let canvas = page.locator('canvas').nth(1);
 | 
			
		||||
 | 
			
		||||
    const canvas = page.locator('canvas').nth(1);
 | 
			
		||||
        // Switch to real-time mode
 | 
			
		||||
        // Adding tags should pause the plot
 | 
			
		||||
        await setRealTimeMode(page);
 | 
			
		||||
 | 
			
		||||
    //Wait for canvas to stablize.
 | 
			
		||||
    await canvas.hover({ trial: true });
 | 
			
		||||
        await createTags({
 | 
			
		||||
            page,
 | 
			
		||||
            canvas,
 | 
			
		||||
            xEnd: 700,
 | 
			
		||||
            yEnd: 480
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
    // click on the tagged plot point
 | 
			
		||||
    await canvas.click({
 | 
			
		||||
      position: {
 | 
			
		||||
        x: 325,
 | 
			
		||||
        y: 377
 | 
			
		||||
      }
 | 
			
		||||
        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);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await expect(page.getByText('Science')).toBeVisible();
 | 
			
		||||
    await expect(page.getByText('Driving')).toBeHidden();
 | 
			
		||||
  }
 | 
			
		||||
    test('Tags work with Plot View of telemetry items', async ({ page }) => {
 | 
			
		||||
        await createDomainObjectWithDefaults(page, {
 | 
			
		||||
            type: "Sine Wave Generator"
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * 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
 | 
			
		||||
      }
 | 
			
		||||
        const canvas = page.locator('canvas').nth(1);
 | 
			
		||||
        await createTags({
 | 
			
		||||
            page,
 | 
			
		||||
            canvas,
 | 
			
		||||
            xEnd: 700,
 | 
			
		||||
            yEnd: 480
 | 
			
		||||
        });
 | 
			
		||||
        await basicTagsTests(page);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await expect(page.getByText('Science')).toBeVisible();
 | 
			
		||||
    await expect(page.getByText('Driving')).toBeHidden();
 | 
			
		||||
  }
 | 
			
		||||
    test('Tags work with Stacked Plots', async ({ page }) => {
 | 
			
		||||
        const stackedPlot = await createDomainObjectWithDefaults(page, {
 | 
			
		||||
            type: "Stacked Plot"
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
  test.beforeEach(async ({ page }) => {
 | 
			
		||||
    await page.goto('./', { waitUntil: 'domcontentloaded' });
 | 
			
		||||
  });
 | 
			
		||||
        const alphaSineWave = await createDomainObjectWithDefaults(page, {
 | 
			
		||||
            type: "Sine Wave Generator",
 | 
			
		||||
            name: "Alpha 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 createDomainObjectWithDefaults(page, {
 | 
			
		||||
            type: "Sine Wave Generator",
 | 
			
		||||
            name: "Beta Sine Wave",
 | 
			
		||||
            parent: stackedPlot.uuid
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
    const overlayPlot = await createDomainObjectWithDefaults(page, {
 | 
			
		||||
      type: 'Overlay Plot'
 | 
			
		||||
        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 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,59 +24,52 @@ 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'
 | 
			
		||||
    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);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    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,200 +21,170 @@
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
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();
 | 
			
		||||
    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'
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
        // 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,46 +21,43 @@
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
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' });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  test('Can perform actions on the Timer', async ({ page }) => {
 | 
			
		||||
    test.info().annotations.push({
 | 
			
		||||
      type: 'issue',
 | 
			
		||||
      description: 'https://github.com/nasa/openmct/issues/4313'
 | 
			
		||||
    let timer;
 | 
			
		||||
    test.beforeEach(async ({ page }) => {
 | 
			
		||||
        await page.goto('./', { waitUntil: 'domcontentloaded' });
 | 
			
		||||
        timer = await createDomainObjectWithDefaults(page, { type: 'timer' });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const timerUrl = timer.url;
 | 
			
		||||
    test('Can perform actions on the Timer', async ({ page }) => {
 | 
			
		||||
        test.info().annotations.push({
 | 
			
		||||
            type: 'issue',
 | 
			
		||||
            description: 'https://github.com/nasa/openmct/issues/4313'
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
    await test.step('From the tree context menu', async () => {
 | 
			
		||||
      await triggerTimerContextMenuAction(page, timerUrl, 'Start');
 | 
			
		||||
      await triggerTimerContextMenuAction(page, timerUrl, 'Pause');
 | 
			
		||||
      await triggerTimerContextMenuAction(page, timerUrl, 'Restart at 0');
 | 
			
		||||
      await triggerTimerContextMenuAction(page, timerUrl, 'Stop');
 | 
			
		||||
    });
 | 
			
		||||
        const timerUrl = timer.url;
 | 
			
		||||
 | 
			
		||||
    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 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 object view', async () => {
 | 
			
		||||
      await triggerTimerViewAction(page, 'Start');
 | 
			
		||||
      await triggerTimerViewAction(page, 'Pause');
 | 
			
		||||
      await triggerTimerViewAction(page, 'Restart at 0');
 | 
			
		||||
        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');
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -79,10 +76,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);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -91,21 +88,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);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -114,10 +111,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);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -125,14 +122,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';
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -141,19 +138,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,327 +25,284 @@ 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'
 | 
			
		||||
        // 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);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // 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
 | 
			
		||||
      });
 | 
			
		||||
    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()
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 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,270 +28,242 @@ 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('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.beforeEach(async ({ page }) => {
 | 
			
		||||
        // Go to baseURL
 | 
			
		||||
        await page.goto("./", { waitUntil: "networkidle" });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Full search for object
 | 
			
		||||
    await page.type('input[type=search]', folderName);
 | 
			
		||||
    test('Can search for objects, and subsequent search dropdown behaves properly', async ({ page, openmctConfig }) => {
 | 
			
		||||
        const { myItemsFolderName } = openmctConfig;
 | 
			
		||||
 | 
			
		||||
    // Wait for search to complete
 | 
			
		||||
    await waitForSearchCompletion(page);
 | 
			
		||||
        const createdObjects = await createObjectsForSearch(page);
 | 
			
		||||
 | 
			
		||||
    // Get the search results
 | 
			
		||||
    const searchResults = page.locator(searchResultSelector);
 | 
			
		||||
        // 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();
 | 
			
		||||
 | 
			
		||||
    // Verify that one result is found
 | 
			
		||||
    await expect(searchResults).toBeVisible();
 | 
			
		||||
    expect(await searchResults.count()).toBe(1);
 | 
			
		||||
    await expect(searchResults).toHaveText(folderName);
 | 
			
		||||
  });
 | 
			
		||||
        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();
 | 
			
		||||
 | 
			
		||||
  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="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`);
 | 
			
		||||
 | 
			
		||||
    let networkRequests = [];
 | 
			
		||||
    page.on('request', (request) => {
 | 
			
		||||
      const searchRequest = request.url().endsWith('_find');
 | 
			
		||||
      const fetchRequest = request.resourceType() === 'fetch';
 | 
			
		||||
      if (searchRequest && fetchRequest) {
 | 
			
		||||
        networkRequests.push(request);
 | 
			
		||||
      }
 | 
			
		||||
        // 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`);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Full search for object
 | 
			
		||||
    await page.type('input[type=search]', 'Clock', { delay: 100 });
 | 
			
		||||
    test('Validate empty search result', async ({ page }) => {
 | 
			
		||||
        // Invalid search for objects
 | 
			
		||||
        await page.type("input[type=search]", 'not found');
 | 
			
		||||
 | 
			
		||||
    // Wait for search to finish
 | 
			
		||||
    await waitForSearchCompletion(page);
 | 
			
		||||
        // Wait for search to complete
 | 
			
		||||
        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);
 | 
			
		||||
        // Get the search results
 | 
			
		||||
        const searchResults = page.locator(searchResultSelector);
 | 
			
		||||
 | 
			
		||||
    const searchResultDropDown = await page.locator(searchResultDropDownSelector);
 | 
			
		||||
        // Verify that no results are found
 | 
			
		||||
        expect(await searchResults.count()).toBe(0);
 | 
			
		||||
 | 
			
		||||
    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'
 | 
			
		||||
        // Verify proper message appears
 | 
			
		||||
        await expect(page.locator('text=No results found')).toBeVisible();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Create folder objects
 | 
			
		||||
    const folderName1 = 'e928a26e-e924-4ea0';
 | 
			
		||||
    const folderName2 = 'e928a26e-e924-4001';
 | 
			
		||||
    test('Validate single object in search result @couchdb', async ({ page }) => {
 | 
			
		||||
        // Create a folder object
 | 
			
		||||
        const folderName = uuid();
 | 
			
		||||
        await createDomainObjectWithDefaults(page, {
 | 
			
		||||
            type: 'folder',
 | 
			
		||||
            name: folderName
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
    await createDomainObjectWithDefaults(page, {
 | 
			
		||||
      type: 'Folder',
 | 
			
		||||
      name: folderName1
 | 
			
		||||
        // 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: folderName2
 | 
			
		||||
    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');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Partial search for objects
 | 
			
		||||
    await page.type('input[type=search]', 'e928a26e');
 | 
			
		||||
    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'
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
    // Wait for search to finish
 | 
			
		||||
    await waitForSearchCompletion(page);
 | 
			
		||||
        // Create folder objects
 | 
			
		||||
        const folderName1 = "e928a26e-e924-4ea0";
 | 
			
		||||
        const folderName2 = "e928a26e-e924-4001";
 | 
			
		||||
 | 
			
		||||
    const searchResultDropDown = page.locator(searchResultDropDownSelector);
 | 
			
		||||
        await createDomainObjectWithDefaults(page, {
 | 
			
		||||
            type: 'Folder',
 | 
			
		||||
            name: folderName1
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
    // Verify that the search result/s correctly match the search query
 | 
			
		||||
    await expect(searchResultDropDown).toContainText(folderName1);
 | 
			
		||||
    await expect(searchResultDropDown).toContainText(folderName2);
 | 
			
		||||
        await createDomainObjectWithDefaults(page, {
 | 
			
		||||
            type: 'Folder',
 | 
			
		||||
            name: folderName2
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
    // Get the search results
 | 
			
		||||
    const searchResults = page.locator(searchResultSelector);
 | 
			
		||||
    // Verify that two results are found
 | 
			
		||||
    expect(await searchResults.count()).toBe(2);
 | 
			
		||||
  });
 | 
			
		||||
        // 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);
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
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,26 +35,25 @@ 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
 | 
			
		||||
}) => {
 | 
			
		||||
  //Go to baseURL
 | 
			
		||||
  await page.goto('./', { waitUntil: 'domcontentloaded' });
 | 
			
		||||
test('Verify that the create button appears and that the Folder Domain Object is available for selection', async ({ page }) => {
 | 
			
		||||
 | 
			
		||||
  //Click the Create button
 | 
			
		||||
  await page.click('button:has-text("Create")');
 | 
			
		||||
    //Go to baseURL
 | 
			
		||||
    await page.goto('./', { waitUntil: 'domcontentloaded' });
 | 
			
		||||
 | 
			
		||||
  // Verify that Create Folder appears in the dropdown
 | 
			
		||||
  await expect(page.locator(':nth-match(:text("Folder"), 2)')).toBeEnabled();
 | 
			
		||||
    //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();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
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,158 +22,151 @@
 | 
			
		||||
 | 
			
		||||
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('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.beforeEach(async ({ page }) => {
 | 
			
		||||
        await page.goto('./', { waitUntil: 'domcontentloaded' });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const folder = await createDomainObjectWithDefaults(page, {
 | 
			
		||||
      type: 'Folder'
 | 
			
		||||
    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);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await page.getByTitle('Show selected item in tree').click();
 | 
			
		||||
    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'
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
    const clock = await createDomainObjectWithDefaults(page, {
 | 
			
		||||
      type: 'Clock',
 | 
			
		||||
      parent: folder.uuid
 | 
			
		||||
        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);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await expandTreePaneItemByName(page, folder.name);
 | 
			
		||||
    await assertTreeItemIsVisible(page, clock.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'
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
  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'
 | 
			
		||||
        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 { myItemsFolderName } = openmctConfig;
 | 
			
		||||
    const page2 = await page.context().newPage();
 | 
			
		||||
    test('Renaming an object reorders the tree @unstable', async ({ page, openmctConfig }) => {
 | 
			
		||||
        const { myItemsFolderName } = openmctConfig;
 | 
			
		||||
 | 
			
		||||
    // Both pages: Go to baseURL
 | 
			
		||||
    await Promise.all([
 | 
			
		||||
      page.goto('./', { waitUntil: 'networkidle' }),
 | 
			
		||||
      page2.goto('./', { waitUntil: 'networkidle' })
 | 
			
		||||
    ]);
 | 
			
		||||
        await createDomainObjectWithDefaults(page, {
 | 
			
		||||
            type: 'Folder',
 | 
			
		||||
            name: 'Foo'
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
    const page1Folder = await createDomainObjectWithDefaults(page, {
 | 
			
		||||
      type: 'Folder'
 | 
			
		||||
        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"
 | 
			
		||||
                ]);
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    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'
 | 
			
		||||
      ]);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -181,22 +174,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();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -204,14 +197,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();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -221,10 +214,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,154 +37,141 @@ 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 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
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
    test.afterEach(async ({ page, browser}) => {
 | 
			
		||||
        console.log("\n==== Devtools: stopTracing ====\n");
 | 
			
		||||
        await browser.stopTracing();
 | 
			
		||||
 | 
			
		||||
    // 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
 | 
			
		||||
        /* 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' });
 | 
			
		||||
        //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);
 | 
			
		||||
 | 
			
		||||
    //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);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user