Compare commits
	
		
			7 Commits
		
	
	
		
			memleak2
			...
			fix-gha-pl
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					91cf387dff | ||
| 
						 | 
					bb0d3c2201 | ||
| 
						 | 
					d849b18e63 | ||
| 
						 | 
					3c33395c2c | ||
| 
						 | 
					e9e5f4aeca | ||
| 
						 | 
					1086ccfd58 | ||
| 
						 | 
					ee7f5a3c2f | 
@@ -2,7 +2,7 @@ version: 2.1
 | 
			
		||||
executors:
 | 
			
		||||
  pw-focal-development:
 | 
			
		||||
    docker:
 | 
			
		||||
      - image: mcr.microsoft.com/playwright:v1.21.1-focal
 | 
			
		||||
      - image: mcr.microsoft.com/playwright:v1.19.1-focal
 | 
			
		||||
    environment:
 | 
			
		||||
      NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed
 | 
			
		||||
parameters:
 | 
			
		||||
@@ -23,7 +23,7 @@ commands:
 | 
			
		||||
      - node/install:
 | 
			
		||||
          install-npm: true
 | 
			
		||||
          node-version: << parameters.node-version >>
 | 
			
		||||
      - run: npm install --prefer-offline --no-audit --progress=false
 | 
			
		||||
      - run: npm install
 | 
			
		||||
  restore_cache_cmd:
 | 
			
		||||
    description: "Custom command for restoring cache with the ability to bust cache. When BUST_CACHE is set to true, jobs will not restore cache"
 | 
			
		||||
    parameters:
 | 
			
		||||
@@ -64,7 +64,7 @@ commands:
 | 
			
		||||
        - run: curl -Os https://uploader.codecov.io/latest/linux/codecov;chmod +x codecov;./codecov 
 | 
			
		||||
orbs:
 | 
			
		||||
  node: circleci/node@4.9.0
 | 
			
		||||
  browser-tools: circleci/browser-tools@1.3.0
 | 
			
		||||
  browser-tools: circleci/browser-tools@1.2.3
 | 
			
		||||
jobs:
 | 
			
		||||
  npm-audit:
 | 
			
		||||
    parameters:
 | 
			
		||||
@@ -101,7 +101,7 @@ jobs:
 | 
			
		||||
            equal: [ "FirefoxESR", <<parameters.browser>> ]
 | 
			
		||||
          steps:
 | 
			
		||||
            - browser-tools/install-firefox:
 | 
			
		||||
                version: "91.7.1esr" #https://archive.mozilla.org/pub/firefox/releases/          
 | 
			
		||||
                version: "91.4.0esr" #https://archive.mozilla.org/pub/firefox/releases/          
 | 
			
		||||
      - when:
 | 
			
		||||
          condition:
 | 
			
		||||
            equal: [ "FirefoxHeadless", <<parameters.browser>> ]
 | 
			
		||||
@@ -113,7 +113,7 @@ jobs:
 | 
			
		||||
          steps:
 | 
			
		||||
            - browser-tools/install-chrome:
 | 
			
		||||
                replace-existing: false
 | 
			
		||||
      - run: npm run test -- --browsers=<<parameters.browser>>
 | 
			
		||||
      - run: npm run test:coverage -- --browsers=<<parameters.browser>>
 | 
			
		||||
      - save_cache_cmd:
 | 
			
		||||
          node-version: <<parameters.node-version>>
 | 
			
		||||
      - store_test_results:
 | 
			
		||||
@@ -128,58 +128,49 @@ jobs:
 | 
			
		||||
      suite:
 | 
			
		||||
        type: string
 | 
			
		||||
    executor: pw-focal-development
 | 
			
		||||
    parallelism: 4
 | 
			
		||||
    steps:
 | 
			
		||||
      - build_and_install:
 | 
			
		||||
          node-version: <<parameters.node-version>>
 | 
			
		||||
      - run: SHARD="$((${CIRCLE_NODE_INDEX}+1))"; npm run test:e2e:<<parameters.suite>> -- --shard=${SHARD}/${CIRCLE_NODE_TOTAL}
 | 
			
		||||
      - run: npx playwright install
 | 
			
		||||
      - run: npm run test:e2e:<<parameters.suite>>
 | 
			
		||||
      - store_test_results:
 | 
			
		||||
          path: test-results/results.xml
 | 
			
		||||
      - store_artifacts:
 | 
			
		||||
          path: test-results
 | 
			
		||||
      - generate_and_store_version_and_filesystem_artifacts
 | 
			
		||||
  perf-test:
 | 
			
		||||
    parameters:
 | 
			
		||||
      node-version:
 | 
			
		||||
        type: string
 | 
			
		||||
    executor: pw-focal-development
 | 
			
		||||
    steps:
 | 
			
		||||
      - build_and_install:
 | 
			
		||||
          node-version: <<parameters.node-version>>
 | 
			
		||||
      - run: npm run test:perf
 | 
			
		||||
      - store_test_results:
 | 
			
		||||
          path: test-results/results.xml
 | 
			
		||||
      - store_artifacts:
 | 
			
		||||
          path: test-results
 | 
			
		||||
      - generate_and_store_version_and_filesystem_artifacts      
 | 
			
		||||
workflows:
 | 
			
		||||
  overall-circleci-commit-status: #These jobs run on every commit
 | 
			
		||||
    jobs:
 | 
			
		||||
      - lint:
 | 
			
		||||
          name: node16-lint
 | 
			
		||||
          node-version: lts/gallium
 | 
			
		||||
      - unit-test:
 | 
			
		||||
          name: node12-chrome
 | 
			
		||||
          node-version: lts/erbium
 | 
			
		||||
          browser: ChromeHeadless
 | 
			
		||||
      - unit-test: 
 | 
			
		||||
          name: node14-chrome
 | 
			
		||||
          node-version: lts/fermium
 | 
			
		||||
          browser: ChromeHeadless
 | 
			
		||||
          post-steps:
 | 
			
		||||
            - upload_code_covio
 | 
			
		||||
            - upload_code_covio  
 | 
			
		||||
      - unit-test:
 | 
			
		||||
          name: node18-chrome
 | 
			
		||||
          node-version: "18"
 | 
			
		||||
          browser: ChromeHeadless
 | 
			
		||||
          name: node16-chrome
 | 
			
		||||
          node-version: lts/gallium
 | 
			
		||||
          browser: ChromeHeadless             
 | 
			
		||||
      - e2e-test:
 | 
			
		||||
          name: e2e-ci
 | 
			
		||||
          node-version: lts/gallium
 | 
			
		||||
          suite: ci
 | 
			
		||||
      - perf-test:
 | 
			
		||||
          node-version: lts/gallium
 | 
			
		||||
  the-nightly: #These jobs do not run on PRs, but against master at night
 | 
			
		||||
    jobs:
 | 
			
		||||
      - unit-test:
 | 
			
		||||
          name: node16-firefoxESR-nightly
 | 
			
		||||
          node-version: lts/gallium
 | 
			
		||||
          name: node12-firefoxESR-nightly
 | 
			
		||||
          node-version: lts/erbium
 | 
			
		||||
          browser: FirefoxESR
 | 
			
		||||
      - unit-test:
 | 
			
		||||
          name: node12-chrome-nightly
 | 
			
		||||
          node-version: lts/erbium
 | 
			
		||||
          browser: ChromeHeadless
 | 
			
		||||
      - unit-test:
 | 
			
		||||
          name: node14-firefox-nightly
 | 
			
		||||
          node-version: lts/fermium
 | 
			
		||||
@@ -192,10 +183,6 @@ workflows:
 | 
			
		||||
          name: node16-chrome-nightly
 | 
			
		||||
          node-version: lts/gallium
 | 
			
		||||
          browser: ChromeHeadless
 | 
			
		||||
      - unit-test:
 | 
			
		||||
          name: node18-chrome
 | 
			
		||||
          node-version: "18"
 | 
			
		||||
          browser: ChromeHeadless
 | 
			
		||||
      - npm-audit:
 | 
			
		||||
          node-version: lts/gallium
 | 
			
		||||
      - e2e-test:
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										13
									
								
								.eslintrc.js
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								.eslintrc.js
									
									
									
									
									
								
							@@ -11,14 +11,12 @@ module.exports = {
 | 
			
		||||
    },
 | 
			
		||||
    "extends": [
 | 
			
		||||
        "eslint:recommended",
 | 
			
		||||
        "plugin:compat/recommended",
 | 
			
		||||
        "plugin:vue/recommended",
 | 
			
		||||
        "plugin:you-dont-need-lodash-underscore/compatible"
 | 
			
		||||
    ],
 | 
			
		||||
    "parser": "vue-eslint-parser",
 | 
			
		||||
    "parserOptions": {
 | 
			
		||||
        "parser": "@babel/eslint-parser",
 | 
			
		||||
        "requireConfigFile": false,
 | 
			
		||||
        "parser": "babel-eslint",
 | 
			
		||||
        "allowImportExportEverywhere": true,
 | 
			
		||||
        "ecmaVersion": 2015,
 | 
			
		||||
        "ecmaFeatures": {
 | 
			
		||||
@@ -29,7 +27,6 @@ module.exports = {
 | 
			
		||||
        "you-dont-need-lodash-underscore/omit": "off",
 | 
			
		||||
        "you-dont-need-lodash-underscore/throttle": "off",
 | 
			
		||||
        "you-dont-need-lodash-underscore/flatten": "off",
 | 
			
		||||
        "you-dont-need-lodash-underscore/get": "off",
 | 
			
		||||
        "no-bitwise": "error",
 | 
			
		||||
        "curly": "error",
 | 
			
		||||
        "eqeqeq": "error",
 | 
			
		||||
@@ -38,6 +35,7 @@ module.exports = {
 | 
			
		||||
        "no-inner-declarations": "off",
 | 
			
		||||
        "no-use-before-define": ["error", "nofunc"],
 | 
			
		||||
        "no-caller": "error",
 | 
			
		||||
        "no-sequences": "error",
 | 
			
		||||
        "no-irregular-whitespace": "error",
 | 
			
		||||
        "no-new": "error",
 | 
			
		||||
        "no-shadow": "error",
 | 
			
		||||
@@ -241,12 +239,13 @@ module.exports = {
 | 
			
		||||
        ],
 | 
			
		||||
        "vue/max-attributes-per-line": ["error", {
 | 
			
		||||
            "singleline": 1,
 | 
			
		||||
            "multiline": 1,
 | 
			
		||||
            "multiline": {
 | 
			
		||||
                "max": 1,
 | 
			
		||||
                "allowFirstLine": true
 | 
			
		||||
            }
 | 
			
		||||
        }],
 | 
			
		||||
        "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"
 | 
			
		||||
 | 
			
		||||
    },
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
								
							@@ -27,7 +27,7 @@ assignees: ''
 | 
			
		||||
 | 
			
		||||
#### Environment
 | 
			
		||||
<!--- If encountered on local machine, execute the following:
 | 
			
		||||
<!--- npx envinfo --system --browsers --npmPackages --binaries --markdown -->
 | 
			
		||||
<!--- npx envinfo --system --browsers --npmPackages --binaries --languages --markdown -->
 | 
			
		||||
* Open MCT Version: <!--- date of build, version, or SHA -->
 | 
			
		||||
* Deployment Type: <!--- npm dev? VIPER Dev? openmct-yamcs? -->
 | 
			
		||||
* OS:
 | 
			
		||||
@@ -40,8 +40,6 @@ assignees: ''
 | 
			
		||||
- [ ] Is there a workaround available?
 | 
			
		||||
- [ ] Does this impact a critical component?
 | 
			
		||||
- [ ] Is this just a visual bug with no functional impact?
 | 
			
		||||
- [ ] Does this block the execution of e2e tests?
 | 
			
		||||
- [ ] Does this have an impact on Performance?
 | 
			
		||||
 | 
			
		||||
#### Additional Information
 | 
			
		||||
<!--- Include any screenshots, gifs, or logs which will expedite triage -->
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/PULL_REQUEST_TEMPLATE.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/PULL_REQUEST_TEMPLATE.md
									
									
									
									
										vendored
									
									
								
							@@ -16,7 +16,7 @@ Closes <!--- Insert Issue Number(s) this PR addresses. Start by typing # will op
 | 
			
		||||
* [ ] Unit tests included and/or updated with changes?
 | 
			
		||||
* [ ] Command line build passes?
 | 
			
		||||
* [ ] Has this been smoke tested?
 | 
			
		||||
* [ ] Testing instructions included in associated issue OR is this a dependency/testcase change?
 | 
			
		||||
* [ ] Testing instructions included in associated issue?
 | 
			
		||||
 | 
			
		||||
### Reviewer Checklist
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										6
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							@@ -32,12 +32,12 @@ jobs:
 | 
			
		||||
 | 
			
		||||
    # Initializes the CodeQL tools for scanning.
 | 
			
		||||
    - name: Initialize CodeQL
 | 
			
		||||
      uses: github/codeql-action/init@v2
 | 
			
		||||
      uses: github/codeql-action/init@v1
 | 
			
		||||
      with:
 | 
			
		||||
        languages: javascript
 | 
			
		||||
 | 
			
		||||
    - name: Autobuild
 | 
			
		||||
      uses: github/codeql-action/autobuild@v2
 | 
			
		||||
      uses: github/codeql-action/autobuild@v1
 | 
			
		||||
 | 
			
		||||
    - name: Perform CodeQL Analysis
 | 
			
		||||
      uses: github/codeql-action/analyze@v2
 | 
			
		||||
      uses: github/codeql-action/analyze@v1
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								.github/workflows/e2e-pr.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/e2e-pr.yml
									
									
									
									
										vendored
									
									
								
							@@ -30,11 +30,11 @@ jobs:
 | 
			
		||||
      - uses: actions/setup-node@v3
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: '16'
 | 
			
		||||
      - run: npx playwright@1.21.1 install
 | 
			
		||||
      - run: npx playwright@1.19.2 install
 | 
			
		||||
      - run: npm install
 | 
			
		||||
      - run: npm run test:e2e:full
 | 
			
		||||
      - name: Archive test results
 | 
			
		||||
        uses: actions/upload-artifact@v3
 | 
			
		||||
        uses: actions/upload-artifact@v2
 | 
			
		||||
        with:
 | 
			
		||||
          path: test-results
 | 
			
		||||
      - name: Test success
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/e2e-visual.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/e2e-visual.yml
									
									
									
									
										vendored
									
									
								
							@@ -17,7 +17,7 @@ jobs:
 | 
			
		||||
      - uses: actions/setup-node@v3
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: '16'
 | 
			
		||||
      - run: npx playwright@1.21.1 install
 | 
			
		||||
      - run: npx playwright@1.19.2 install
 | 
			
		||||
      - run: npm install
 | 
			
		||||
      - name: Run the e2e visual tests
 | 
			
		||||
        run: npm run test:e2e:visual
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										14
									
								
								.github/workflows/lighthouse.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										14
									
								
								.github/workflows/lighthouse.yml
									
									
									
									
										vendored
									
									
								
							@@ -9,6 +9,8 @@ on:
 | 
			
		||||
  pull_request:
 | 
			
		||||
    types: 
 | 
			
		||||
      - labeled
 | 
			
		||||
  schedule:
 | 
			
		||||
    - cron: '28 21 * * 1-5'
 | 
			
		||||
jobs:
 | 
			
		||||
  lighthouse-pr:
 | 
			
		||||
    if: ${{ github.event.label.name == 'pr:lighthouse' }}
 | 
			
		||||
@@ -18,10 +20,10 @@ jobs:
 | 
			
		||||
        uses: actions/checkout@v3
 | 
			
		||||
        with:
 | 
			
		||||
          ref: master #explicitly checkout master for baseline
 | 
			
		||||
      - name: Install Node 16
 | 
			
		||||
      - name: Install Node 14
 | 
			
		||||
        uses: actions/setup-node@v3
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: '16'
 | 
			
		||||
          node-version: '14'
 | 
			
		||||
      - name: Cache node modules
 | 
			
		||||
        uses: actions/cache@v2
 | 
			
		||||
        env:
 | 
			
		||||
@@ -52,10 +54,10 @@ jobs:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v3
 | 
			
		||||
      - name: Install Node 16
 | 
			
		||||
      - name: Install Node 14
 | 
			
		||||
        uses: actions/setup-node@v3
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: '16'
 | 
			
		||||
          node-version: '14'
 | 
			
		||||
      - name: Cache node modules
 | 
			
		||||
        uses: actions/cache@v2
 | 
			
		||||
        env:
 | 
			
		||||
@@ -81,9 +83,9 @@ jobs:
 | 
			
		||||
      - name: Install Node 14
 | 
			
		||||
        uses: actions/setup-node@v3
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: '16'
 | 
			
		||||
          node-version: '14'
 | 
			
		||||
      - name: Cache node modules
 | 
			
		||||
        uses: actions/cache@v3
 | 
			
		||||
        uses: actions/cache@v2
 | 
			
		||||
        env:
 | 
			
		||||
          cache-name: cache-node-modules
 | 
			
		||||
        with:
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/pr-platform.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/pr-platform.yml
									
									
									
									
										vendored
									
									
								
							@@ -16,9 +16,9 @@ jobs:
 | 
			
		||||
          - macos-latest
 | 
			
		||||
          - windows-latest
 | 
			
		||||
        node_version:
 | 
			
		||||
          - 12
 | 
			
		||||
          - 14
 | 
			
		||||
          - 16
 | 
			
		||||
          - 18
 | 
			
		||||
        architecture:
 | 
			
		||||
          - x64
 | 
			
		||||
    name: Node ${{ matrix.node_version }} - ${{ matrix.architecture }} on ${{ matrix.os }}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										61
									
								
								.npmignore
									
									
									
									
									
								
							
							
						
						
									
										61
									
								
								.npmignore
									
									
									
									
									
								
							@@ -1,27 +1,44 @@
 | 
			
		||||
# Ignore everything first (will not ignore special files like LICENSE.md,
 | 
			
		||||
# README.md, and package.json)...
 | 
			
		||||
/**/*
 | 
			
		||||
*.scssc
 | 
			
		||||
*.zip
 | 
			
		||||
*.gzip
 | 
			
		||||
*.tgz
 | 
			
		||||
*.DS_Store
 | 
			
		||||
 | 
			
		||||
# ...but include these folders...
 | 
			
		||||
!/dist/**/*
 | 
			
		||||
!/src/**/*
 | 
			
		||||
*.sass-cache
 | 
			
		||||
*COMPILE.css
 | 
			
		||||
 | 
			
		||||
# We might be able to remove this if it is not imported by any project directly.
 | 
			
		||||
# https://github.com/nasa/openmct/issues/4992
 | 
			
		||||
!/example/**/*
 | 
			
		||||
# Intellij project configuration files
 | 
			
		||||
*.idea
 | 
			
		||||
*.iml
 | 
			
		||||
 | 
			
		||||
# We will remove this in https://github.com/nasa/openmct/issues/4922
 | 
			
		||||
!/app.js
 | 
			
		||||
# External dependencies
 | 
			
		||||
 | 
			
		||||
# ...except for these files in the above folders.
 | 
			
		||||
/src/**/*Spec.js
 | 
			
		||||
/src/**/test/
 | 
			
		||||
# TODO move test utils into test/ folders
 | 
			
		||||
/src/utils/testing.js
 | 
			
		||||
# Build output
 | 
			
		||||
target
 | 
			
		||||
 | 
			
		||||
# Also include these special top-level files.
 | 
			
		||||
!copyright-notice.js
 | 
			
		||||
!copyright-notice.html
 | 
			
		||||
!index.html
 | 
			
		||||
!openmct.js
 | 
			
		||||
!SECURITY.md
 | 
			
		||||
# Mac OS X Finder
 | 
			
		||||
.DS_Store
 | 
			
		||||
 | 
			
		||||
# Closed source libraries
 | 
			
		||||
closed-lib
 | 
			
		||||
 | 
			
		||||
# Node, Bower dependencies
 | 
			
		||||
node_modules
 | 
			
		||||
bower_components
 | 
			
		||||
 | 
			
		||||
Procfile
 | 
			
		||||
 | 
			
		||||
# Protractor logs
 | 
			
		||||
protractor/logs
 | 
			
		||||
 | 
			
		||||
# npm-debug log
 | 
			
		||||
npm-debug.log
 | 
			
		||||
 | 
			
		||||
# Infra and tests
 | 
			
		||||
.circleci
 | 
			
		||||
.github
 | 
			
		||||
e2e
 | 
			
		||||
codecov.yml
 | 
			
		||||
lighthouserc.yml
 | 
			
		||||
*.Spec.js
 | 
			
		||||
karma.conf.js
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										5
									
								
								.npmrc
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								.npmrc
									
									
									
									
									
								
							@@ -1,4 +1,9 @@
 | 
			
		||||
loglevel=warn
 | 
			
		||||
 | 
			
		||||
# Temporary: istanbul-instrumenter-loader is working with webpack 5, but states
 | 
			
		||||
# webpack 4 being the latest version it supports, so this legacy-peer-deps
 | 
			
		||||
# allows us to install it anyway.
 | 
			
		||||
legacy-peer-deps=true
 | 
			
		||||
 | 
			
		||||
#Prevent folks from ignoring an important error when building from source
 | 
			
		||||
engine-strict=true
 | 
			
		||||
@@ -65,12 +65,6 @@ Open MCT is built using [`npm`](http://npmjs.com/) and [`webpack`](https://webpa
 | 
			
		||||
 | 
			
		||||
See our documentation for a guide on [building Applications with Open MCT](https://github.com/nasa/openmct/blob/master/API.md#starting-an-open-mct-application).
 | 
			
		||||
 | 
			
		||||
## Compatibility
 | 
			
		||||
 | 
			
		||||
This is a fast moving project and we do our best to test and support the widest possible range of browsers, operating systems, and nodejs APIs. We have a published list of support available in our package.json's `browserslist` key.
 | 
			
		||||
 | 
			
		||||
If you encounter an issue with a particular browser, OS, or nodejs API, please file a [GitHub issue](https://github.com/nasa/openmct/issues/new/choose)
 | 
			
		||||
 | 
			
		||||
## Plugins
 | 
			
		||||
 | 
			
		||||
Open MCT can be extended via plugins that make calls to the Open MCT API. A plugin is a group 
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								app.js
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								app.js
									
									
									
									
									
								
							@@ -64,7 +64,7 @@ app.use(require('webpack-dev-middleware')(
 | 
			
		||||
    compiler,
 | 
			
		||||
    {
 | 
			
		||||
        publicPath: '/dist',
 | 
			
		||||
        stats: 'errors-warnings'
 | 
			
		||||
        logLevel: 'warn'
 | 
			
		||||
    }
 | 
			
		||||
));
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +0,0 @@
 | 
			
		||||
// This is a Babel config that webpack.coverage.js uses in order to instrument
 | 
			
		||||
// code with coverage instrumentation.
 | 
			
		||||
const babelConfig = {
 | 
			
		||||
    plugins: [['babel-plugin-istanbul', {
 | 
			
		||||
        extension: ['.js', '.vue']
 | 
			
		||||
    }]]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
module.exports = babelConfig;
 | 
			
		||||
@@ -1,12 +1,4 @@
 | 
			
		||||
/* eslint-disable no-undef */
 | 
			
		||||
module.exports = {
 | 
			
		||||
    "extends": ["plugin:playwright/playwright-test"],
 | 
			
		||||
    "overrides": [
 | 
			
		||||
        {
 | 
			
		||||
            "files": ["tests/visual/*.spec.js"],
 | 
			
		||||
            "rules": {
 | 
			
		||||
                "playwright/no-wait-for-timeout": "off"
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    ]
 | 
			
		||||
    "extends": ["plugin:playwright/playwright-test"]
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -1,27 +0,0 @@
 | 
			
		||||
/* eslint-disable no-undef */
 | 
			
		||||
 | 
			
		||||
// This file extends the base functionality of the playwright test framework
 | 
			
		||||
const base = require('@playwright/test');
 | 
			
		||||
const { expect } = require('@playwright/test');
 | 
			
		||||
 | 
			
		||||
exports.test = base.test.extend({
 | 
			
		||||
    page: async ({ baseURL, page }, use) => {
 | 
			
		||||
        const messages = [];
 | 
			
		||||
        page.on('console', msg => messages.push(`[${msg.type()}] ${msg.text()}`));
 | 
			
		||||
        await use(page);
 | 
			
		||||
        await expect.soft(messages.toString()).not.toContain('[error]');
 | 
			
		||||
    },
 | 
			
		||||
    browser: async ({ playwright, browser }, use, workerInfo) => {
 | 
			
		||||
    // Use browserless if configured
 | 
			
		||||
        if (workerInfo.project.name.match(/browserless/)) {
 | 
			
		||||
            const vBrowser = await playwright.chromium.connectOverCDP({
 | 
			
		||||
                endpointURL: 'ws://localhost:3003'
 | 
			
		||||
            });
 | 
			
		||||
            await use(vBrowser);
 | 
			
		||||
        } else {
 | 
			
		||||
            // Use Local Browser for testing.
 | 
			
		||||
            await use(browser);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@@ -2,41 +2,38 @@
 | 
			
		||||
// playwright.config.js
 | 
			
		||||
// @ts-check
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line no-unused-vars
 | 
			
		||||
const { devices } = require('@playwright/test');
 | 
			
		||||
 | 
			
		||||
/** @type {import('@playwright/test').PlaywrightTestConfig} */
 | 
			
		||||
const config = {
 | 
			
		||||
    retries: 1,
 | 
			
		||||
    retries: 2,
 | 
			
		||||
    testDir: 'tests',
 | 
			
		||||
    testIgnore: '**/*.perf.spec.js', //Ignore performance tests and define in playwright-perfromance.config.js
 | 
			
		||||
    timeout: 60 * 1000,
 | 
			
		||||
    timeout: 90 * 1000,
 | 
			
		||||
    webServer: {
 | 
			
		||||
        command: 'npm run start',
 | 
			
		||||
        port: 8080,
 | 
			
		||||
        timeout: 200 * 1000,
 | 
			
		||||
        reuseExistingServer: !process.env.CI
 | 
			
		||||
    },
 | 
			
		||||
    maxFailures: process.env.CI ? 5 : undefined, //Limits failures to 5 to reduce CI Waste
 | 
			
		||||
    workers: 2, //Limit to 2 for CircleCI Agent
 | 
			
		||||
    use: {
 | 
			
		||||
        baseURL: 'http://localhost:8080/',
 | 
			
		||||
        headless: true,
 | 
			
		||||
        ignoreHTTPSErrors: true,
 | 
			
		||||
        screenshot: 'only-on-failure',
 | 
			
		||||
        trace: 'on-first-retry',
 | 
			
		||||
        video: 'on-first-retry'
 | 
			
		||||
        screenshot: 'on',
 | 
			
		||||
        trace: 'on',
 | 
			
		||||
        video: 'on'
 | 
			
		||||
    },
 | 
			
		||||
    projects: [
 | 
			
		||||
        {
 | 
			
		||||
            name: 'chrome',
 | 
			
		||||
            use: {
 | 
			
		||||
                browserName: 'chromium'
 | 
			
		||||
                browserName: 'chromium',
 | 
			
		||||
                ...devices['Desktop Chrome']
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            name: 'MMOC',
 | 
			
		||||
            grepInvert: /@snapshot/,
 | 
			
		||||
            use: {
 | 
			
		||||
                browserName: 'chromium',
 | 
			
		||||
                viewport: {
 | 
			
		||||
@@ -55,11 +52,8 @@ const config = {
 | 
			
		||||
    ],
 | 
			
		||||
    reporter: [
 | 
			
		||||
        ['list'],
 | 
			
		||||
        ['html', {
 | 
			
		||||
            open: 'never',
 | 
			
		||||
            outputFolder: '../test-results/html/'
 | 
			
		||||
        }],
 | 
			
		||||
        ['junit', { outputFile: 'test-results/results.xml' }],
 | 
			
		||||
        ['allure-playwright'],
 | 
			
		||||
        ['github']
 | 
			
		||||
    ]
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -2,14 +2,12 @@
 | 
			
		||||
// playwright.config.js
 | 
			
		||||
// @ts-check
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line no-unused-vars
 | 
			
		||||
const { devices } = require('@playwright/test');
 | 
			
		||||
 | 
			
		||||
/** @type {import('@playwright/test').PlaywrightTestConfig} */
 | 
			
		||||
const config = {
 | 
			
		||||
    retries: 0,
 | 
			
		||||
    testDir: 'tests',
 | 
			
		||||
    testIgnore: '**/*.perf.spec.js',
 | 
			
		||||
    timeout: 30 * 1000,
 | 
			
		||||
    webServer: {
 | 
			
		||||
        command: 'npm run start',
 | 
			
		||||
@@ -23,20 +21,20 @@ const config = {
 | 
			
		||||
        baseURL: 'http://localhost:8080/',
 | 
			
		||||
        headless: false,
 | 
			
		||||
        ignoreHTTPSErrors: true,
 | 
			
		||||
        screenshot: 'only-on-failure',
 | 
			
		||||
        trace: 'retain-on-failure',
 | 
			
		||||
        video: 'retain-on-failure'
 | 
			
		||||
        screenshot: 'on',
 | 
			
		||||
        trace: 'on',
 | 
			
		||||
        video: 'on'
 | 
			
		||||
    },
 | 
			
		||||
    projects: [
 | 
			
		||||
        {
 | 
			
		||||
            name: 'chrome',
 | 
			
		||||
            use: {
 | 
			
		||||
                browserName: 'chromium'
 | 
			
		||||
                browserName: 'chromium',
 | 
			
		||||
                ...devices['Desktop Chrome']
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            name: 'MMOC',
 | 
			
		||||
            grepInvert: /@snapshot/,
 | 
			
		||||
            use: {
 | 
			
		||||
                browserName: 'chromium',
 | 
			
		||||
                viewport: {
 | 
			
		||||
@@ -55,10 +53,7 @@ const config = {
 | 
			
		||||
    ],
 | 
			
		||||
    reporter: [
 | 
			
		||||
        ['list'],
 | 
			
		||||
        ['html', {
 | 
			
		||||
            open: 'on-failure',
 | 
			
		||||
            outputFolder: '../test-results'
 | 
			
		||||
        }]
 | 
			
		||||
        ['allure-playwright']
 | 
			
		||||
    ]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,41 +0,0 @@
 | 
			
		||||
/* eslint-disable no-undef */
 | 
			
		||||
// playwright.config.js
 | 
			
		||||
// @ts-check
 | 
			
		||||
 | 
			
		||||
/** @type {import('@playwright/test').PlaywrightTestConfig} */
 | 
			
		||||
const config = {
 | 
			
		||||
    retries: 0,
 | 
			
		||||
    testDir: 'tests/performance/',
 | 
			
		||||
    timeout: 30 * 1000,
 | 
			
		||||
    workers: 1, //Only run in serial with 1 worker
 | 
			
		||||
    webServer: {
 | 
			
		||||
        command: 'npm run start',
 | 
			
		||||
        port: 8080,
 | 
			
		||||
        timeout: 200 * 1000,
 | 
			
		||||
        reuseExistingServer: !process.env.CI
 | 
			
		||||
    },
 | 
			
		||||
    use: {
 | 
			
		||||
        browserName: "chromium",
 | 
			
		||||
        baseURL: 'http://localhost:8080/',
 | 
			
		||||
        headless: Boolean(process.env.CI), //Only if running locally
 | 
			
		||||
        ignoreHTTPSErrors: true,
 | 
			
		||||
        screenshot: 'off',
 | 
			
		||||
        trace: 'off',
 | 
			
		||||
        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,10 +4,10 @@
 | 
			
		||||
 | 
			
		||||
/** @type {import('@playwright/test').PlaywrightTestConfig} */
 | 
			
		||||
const config = {
 | 
			
		||||
    retries: 0, // visual tests should never retry due to snapshot comparison errors
 | 
			
		||||
    testDir: 'tests/visual',
 | 
			
		||||
    retries: 0,
 | 
			
		||||
    testDir: 'tests',
 | 
			
		||||
    timeout: 90 * 1000,
 | 
			
		||||
    workers: 1, // visual tests should never run in parallel due to test pollution
 | 
			
		||||
    workers: 1,
 | 
			
		||||
    webServer: {
 | 
			
		||||
        command: 'npm run start',
 | 
			
		||||
        port: 8080,
 | 
			
		||||
@@ -17,7 +17,7 @@ const config = {
 | 
			
		||||
    use: {
 | 
			
		||||
        browserName: "chromium",
 | 
			
		||||
        baseURL: 'http://localhost:8080/',
 | 
			
		||||
        headless: true, // this needs to remain headless to avoid visual changes due to GPU
 | 
			
		||||
        headless: true,
 | 
			
		||||
        ignoreHTTPSErrors: true,
 | 
			
		||||
        screenshot: 'on',
 | 
			
		||||
        trace: 'off',
 | 
			
		||||
@@ -25,7 +25,8 @@ const config = {
 | 
			
		||||
    },
 | 
			
		||||
    reporter: [
 | 
			
		||||
        ['list'],
 | 
			
		||||
        ['junit', { outputFile: 'test-results/results.xml' }]
 | 
			
		||||
        ['junit', { outputFile: 'test-results/results.xml' }],
 | 
			
		||||
        ['allure-playwright']
 | 
			
		||||
    ]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1 +0,0 @@
 | 
			
		||||
{"openmct":{"21338566-d472-4377-aed1-21b79272c8de":{"identifier":{"key":"21338566-d472-4377-aed1-21b79272c8de","namespace":""},"name":"Performance Display Layout","type":"layout","composition":[{"key":"644c2e47-2903-475f-8a4a-6be1588ee02f","namespace":""}],"configuration":{"items":[{"width":32,"height":18,"x":1,"y":1,"identifier":{"key":"644c2e47-2903-475f-8a4a-6be1588ee02f","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"5aeb5a71-3149-41ed-9d8a-d34b0a18b053"}],"layoutGrid":[10,10]},"modified":1652228997384,"location":"mine","persisted":1652228997384},"644c2e47-2903-475f-8a4a-6be1588ee02f":{"identifier":{"key":"644c2e47-2903-475f-8a4a-6be1588ee02f","namespace":""},"name":"Performance Example Imagery","type":"example.imagery","configuration":{"imageLocation":"","imageLoadDelayInMilliSeconds":20000,"imageSamples":[]},"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}},{"name":"Image Download Name","key":"imageDownloadName","format":"imageDownloadName","hints":{"imageDownloadName":1}}]},"modified":1652228997375,"location":"21338566-d472-4377-aed1-21b79272c8de","persisted":1652228997375}},"rootId":"21338566-d472-4377-aed1-21b79272c8de"}
 | 
			
		||||
@@ -1 +0,0 @@
 | 
			
		||||
{"openmct":{"6d2fa9fd-f2aa-461a-a1e1-164ac44bec9d":{"identifier":{"key":"6d2fa9fd-f2aa-461a-a1e1-164ac44bec9d","namespace":""},"name":"Performance Notebook","type":"notebook","configuration":{"defaultSort":"oldest","entries":{"3e31c412-33ba-4757-8ade-e9821f6ba321":{"8c8f6035-631c-45af-8c24-786c60295335":[{"id":"entry-1652815305457","createdOn":1652815305457,"createdBy":"","text":"Existing Entry 1","embeds":[]},{"id":"entry-1652815313465","createdOn":1652815313465,"createdBy":"","text":"Existing Entry 2","embeds":[]},{"id":"entry-1652815399955","createdOn":1652815399955,"createdBy":"","text":"Existing Entry 3","embeds":[]}]}},"imageMigrationVer":"v1","pageTitle":"Page","sections":[{"id":"3e31c412-33ba-4757-8ade-e9821f6ba321","isDefault":false,"isSelected":false,"name":"Section1","pages":[{"id":"8c8f6035-631c-45af-8c24-786c60295335","isDefault":false,"isSelected":false,"name":"Page1","pageTitle":"Page"},{"id":"36555942-c9aa-439c-bbdb-0aaf50db50f5","isDefault":false,"isSelected":false,"name":"Page2","pageTitle":"Page"}],"sectionTitle":"Section"},{"id":"dab0bd1d-2c5a-405c-987f-107123d6189a","isDefault":false,"isSelected":true,"name":"Section2","pages":[{"id":"f625a86a-cb99-4898-8082-80543c8de534","isDefault":false,"isSelected":false,"name":"Page1","pageTitle":"Page"},{"id":"e77ef810-f785-42a7-942e-07e999b79c59","isDefault":false,"isSelected":true,"name":"Page2","pageTitle":"Page"}],"sectionTitle":"Section"}],"sectionTitle":"Section","type":"General","showTime":"0"},"modified":1652815915219,"location":"mine","persisted":1652815915222}},"rootId":"6d2fa9fd-f2aa-461a-a1e1-164ac44bec9d"}
 | 
			
		||||
@@ -1,77 +0,0 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2022, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
This test suite is dedicated to tests which verify form functionality.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
const { test, expect } = require('@playwright/test');
 | 
			
		||||
 | 
			
		||||
const TEST_FOLDER = 'test folder';
 | 
			
		||||
 | 
			
		||||
test.describe('forms set', () => {
 | 
			
		||||
    test('New folder form has title as required field', async ({ page }) => {
 | 
			
		||||
        //Go to baseURL
 | 
			
		||||
        await page.goto('/', { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
        // Click button:has-text("Create")
 | 
			
		||||
        await page.click('button:has-text("Create")');
 | 
			
		||||
        // Click :nth-match(:text("Folder"), 2)
 | 
			
		||||
        await page.click(':nth-match(:text("Folder"), 2)');
 | 
			
		||||
        // Click text=Properties Title Notes >> input[type="text"]
 | 
			
		||||
        await page.click('text=Properties Title Notes >> input[type="text"]');
 | 
			
		||||
        // Fill text=Properties Title Notes >> input[type="text"]
 | 
			
		||||
        await page.fill('text=Properties Title Notes >> input[type="text"]', '');
 | 
			
		||||
        // Press Tab
 | 
			
		||||
        await page.press('text=Properties Title Notes >> input[type="text"]', 'Tab');
 | 
			
		||||
 | 
			
		||||
        const okButton = page.locator('text=OK');
 | 
			
		||||
 | 
			
		||||
        await expect(okButton).toBeDisabled();
 | 
			
		||||
        await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(/invalid/);
 | 
			
		||||
 | 
			
		||||
        // Click text=Properties Title Notes >> input[type="text"]
 | 
			
		||||
        await page.click('text=Properties Title Notes >> input[type="text"]');
 | 
			
		||||
        // Fill text=Properties Title Notes >> input[type="text"]
 | 
			
		||||
        await page.fill('text=Properties Title Notes >> input[type="text"]', TEST_FOLDER);
 | 
			
		||||
        // Press Tab
 | 
			
		||||
        await page.press('text=Properties Title Notes >> input[type="text"]', 'Tab');
 | 
			
		||||
 | 
			
		||||
        await expect(page.locator('.c-form-row__state-indicator').first()).not.toHaveClass(/invalid/);
 | 
			
		||||
 | 
			
		||||
        // Click text=OK
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            page.waitForNavigation(),
 | 
			
		||||
            page.click('text=OK')
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        await expect(page.locator('.l-browse-bar__object-name')).toContainText(TEST_FOLDER);
 | 
			
		||||
    });
 | 
			
		||||
    test.fixme('Create all object types and verify correctness', async ({ page }) => {
 | 
			
		||||
        //Create the following Domain Objects with their unique Object Types
 | 
			
		||||
        // Sine Wave Generator (number object)
 | 
			
		||||
        // Timer Object
 | 
			
		||||
        // Plan View Object
 | 
			
		||||
        // Clock Object
 | 
			
		||||
        // Hyperlink
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@@ -1,63 +0,0 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2022, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
This test suite is dedicated to tests which verify branding related components.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
const { test } = require('../fixtures.js');
 | 
			
		||||
const { expect } = require('@playwright/test');
 | 
			
		||||
 | 
			
		||||
test.describe('Branding tests', () => {
 | 
			
		||||
    test('About Modal launches with basic branding properties', async ({ page }) => {
 | 
			
		||||
        // Go to baseURL
 | 
			
		||||
        await page.goto('/', { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
        // Click About button
 | 
			
		||||
        await page.click('.l-shell__app-logo');
 | 
			
		||||
 | 
			
		||||
        // Verify that the NASA Logo Appears
 | 
			
		||||
        await expect(await page.locator('.c-about__image')).toBeVisible();
 | 
			
		||||
 | 
			
		||||
        // Modify the Build information in 'about' Modal
 | 
			
		||||
        const versionInformationLocator = page.locator('ul.t-info.l-info.s-info');
 | 
			
		||||
        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', async ({ page }) => {
 | 
			
		||||
        // Go to baseURL
 | 
			
		||||
        await page.goto('/', { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
        // 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()
 | 
			
		||||
        ]);
 | 
			
		||||
        expect(page2.waitForURL('**/licenses**')).toBeTruthy();
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@@ -1,63 +0,0 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2022, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
This test suite is dedicated to tests which verify the basic operations surrounding the example event generator.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
const { test } = require('../../fixtures.js');
 | 
			
		||||
const { expect } = require('@playwright/test');
 | 
			
		||||
 | 
			
		||||
test.describe('Example Event Generator Operations', () => {
 | 
			
		||||
    test('Can create example event generator with a name', async ({ page }) => {
 | 
			
		||||
        //Go to baseURL
 | 
			
		||||
        await page.goto('/', { waitUntil: 'networkidle' });
 | 
			
		||||
        // let's make an event generator
 | 
			
		||||
        await page.locator('button:has-text("Create")').click();
 | 
			
		||||
        // Click li:has-text("Event Message Generator")
 | 
			
		||||
        await page.locator('li:has-text("Event Message Generator")').click();
 | 
			
		||||
        // Click text=Properties Title Notes >> input[type="text"]
 | 
			
		||||
        await page.locator('text=Properties Title Notes >> input[type="text"]').click();
 | 
			
		||||
        // Fill text=Properties Title Notes >> input[type="text"]
 | 
			
		||||
        await page.locator('text=Properties Title Notes >> input[type="text"]').fill('Test Event Generator');
 | 
			
		||||
        // Press Enter
 | 
			
		||||
        await page.locator('text=Properties Title Notes >> input[type="text"]').press('Enter');
 | 
			
		||||
        // Click text=OK
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            page.waitForNavigation({ url: /.*&view=table/ }),
 | 
			
		||||
            page.locator('text=OK').click()
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        await expect(page.locator('.l-browse-bar__object-name')).toContainText('Test Event Generator');
 | 
			
		||||
        // Click button:has-text("Fixed Timespan")
 | 
			
		||||
        await page.locator('button:has-text("Fixed Timespan")').click();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    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 }) => {
 | 
			
		||||
        // Go to object created in step one
 | 
			
		||||
        // Verify the telemetry table has a class with "is-sorting asc"
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@@ -24,8 +24,7 @@
 | 
			
		||||
This test suite is dedicated to tests which verify the basic operations surrounding conditionSets.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
const { test } = require('../../../fixtures.js');
 | 
			
		||||
const { expect } = require('@playwright/test');
 | 
			
		||||
const { test, expect } = require('@playwright/test');
 | 
			
		||||
 | 
			
		||||
test.describe('Sine Wave Generator', () => {
 | 
			
		||||
    test('Create new Sine Wave Generator Object and validate create Form Logic', async ({ page }) => {
 | 
			
		||||
@@ -158,10 +157,5 @@ test.describe('Sine Wave Generator', () => {
 | 
			
		||||
                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]+/);
 | 
			
		||||
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,146 +0,0 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2022, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
This test suite is dedicated to tests which verify the basic operations surrounding moving objects.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
const { test } = require('../fixtures.js');
 | 
			
		||||
const { expect } = require('@playwright/test');
 | 
			
		||||
 | 
			
		||||
test.describe('Move item tests', () => {
 | 
			
		||||
    test('Create a basic object and verify that it can be moved to another folder', async ({ page }) => {
 | 
			
		||||
        // Go to Open MCT
 | 
			
		||||
        await page.goto('/');
 | 
			
		||||
 | 
			
		||||
        // Create a new folder in the root my items folder
 | 
			
		||||
        let folder1 = "Folder1";
 | 
			
		||||
        await page.locator('button:has-text("Create")').click();
 | 
			
		||||
        await page.locator('li.icon-folder').click();
 | 
			
		||||
 | 
			
		||||
        await page.locator('text=Properties Title Notes >> input[type="text"]').click();
 | 
			
		||||
        await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder1);
 | 
			
		||||
 | 
			
		||||
        // Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184
 | 
			
		||||
        await page.click('form[name="mctForm"] a:has-text("My Items")');
 | 
			
		||||
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            page.waitForNavigation(),
 | 
			
		||||
            page.locator('text=OK').click(),
 | 
			
		||||
            page.waitForSelector('.c-message-banner__message')
 | 
			
		||||
        ]);
 | 
			
		||||
        //Wait until Save Banner is gone
 | 
			
		||||
        await page.locator('.c-message-banner__close-button').click();
 | 
			
		||||
        await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
 | 
			
		||||
 | 
			
		||||
        // Create another folder with a new name at default location, which is currently inside Folder 1
 | 
			
		||||
        let folder2 = "Folder2";
 | 
			
		||||
        await page.locator('button:has-text("Create")').click();
 | 
			
		||||
        await page.locator('li.icon-folder').click();
 | 
			
		||||
        await page.locator('text=Properties Title Notes >> input[type="text"]').click();
 | 
			
		||||
        await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder2);
 | 
			
		||||
 | 
			
		||||
        // Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184
 | 
			
		||||
        await page.click('form[name="mctForm"] a:has-text("My Items")');
 | 
			
		||||
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            page.waitForNavigation(),
 | 
			
		||||
            page.locator('text=OK').click(),
 | 
			
		||||
            page.waitForSelector('.c-message-banner__message')
 | 
			
		||||
        ]);
 | 
			
		||||
        //Wait until Save Banner is gone
 | 
			
		||||
        await page.locator('.c-message-banner__close-button').click();
 | 
			
		||||
        await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
 | 
			
		||||
 | 
			
		||||
        // Move Folder 2 from Folder 1 to My Items
 | 
			
		||||
        await page.locator('text=Open MCT My Items >> span').nth(3).click();
 | 
			
		||||
        await page.locator('.c-tree__scrollable div div:nth-child(2) .c-tree__item .c-tree__item__view-control').click();
 | 
			
		||||
 | 
			
		||||
        await page.locator(`a:has-text("${folder2}")`).click({
 | 
			
		||||
            button: 'right'
 | 
			
		||||
        });
 | 
			
		||||
        await page.locator('li.icon-move').click();
 | 
			
		||||
        await page.locator('form[name="mctForm"] >> text=My Items').click();
 | 
			
		||||
 | 
			
		||||
        await page.locator('text=OK').click();
 | 
			
		||||
 | 
			
		||||
        // Expect that Folder 2 is in My Items, the root folder
 | 
			
		||||
        expect(page.locator(`text=My Items >> nth=0:has(text=${folder2})`)).toBeTruthy();
 | 
			
		||||
    });
 | 
			
		||||
    test('Create a basic object and verify that it cannot be moved to telemetry object without Composition Provider', async ({ page }) => {
 | 
			
		||||
        // Go to Open MCT
 | 
			
		||||
        await page.goto('/');
 | 
			
		||||
 | 
			
		||||
        // Create Telemetry Table
 | 
			
		||||
        let telemetryTable = 'Test Telemetry Table';
 | 
			
		||||
        await page.locator('button:has-text("Create")').click();
 | 
			
		||||
        await page.locator('li:has-text("Telemetry Table")').click();
 | 
			
		||||
        await page.locator('text=Properties Title Notes >> input[type="text"]').click();
 | 
			
		||||
        await page.locator('text=Properties Title Notes >> input[type="text"]').fill(telemetryTable);
 | 
			
		||||
 | 
			
		||||
        // Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184
 | 
			
		||||
        await page.click('form[name="mctForm"] a:has-text("My Items")');
 | 
			
		||||
 | 
			
		||||
        await page.locator('text=OK').click();
 | 
			
		||||
 | 
			
		||||
        // Finish editing and save Telemetry Table
 | 
			
		||||
        await page.locator('.c-button--menu.c-button--major.icon-save').click();
 | 
			
		||||
        await page.locator('text=Save and Finish Editing').click();
 | 
			
		||||
 | 
			
		||||
        // Create New Folder Basic Domain Object
 | 
			
		||||
        let folder = 'Test Folder';
 | 
			
		||||
        await page.locator('button:has-text("Create")').click();
 | 
			
		||||
        await page.locator('li:has-text("Folder")').click();
 | 
			
		||||
        await page.locator('text=Properties Title Notes >> input[type="text"]').click();
 | 
			
		||||
        await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder);
 | 
			
		||||
 | 
			
		||||
        // See if it's possible to put the folder in the Telemetry object during creation (Soft Assert)
 | 
			
		||||
        await page.locator(`form[name="mctForm"] >> text=${telemetryTable}`).click();
 | 
			
		||||
        let okButton = await page.locator('button.c-button.c-button--major:has-text("OK")');
 | 
			
		||||
        let okButtonStateDisabled = await okButton.isDisabled();
 | 
			
		||||
        expect.soft(okButtonStateDisabled).toBeTruthy();
 | 
			
		||||
 | 
			
		||||
        // Continue test regardless of assertion and create it in My Items
 | 
			
		||||
        await page.locator('form[name="mctForm"] >> text=My Items').click();
 | 
			
		||||
        await page.locator('text=OK').click();
 | 
			
		||||
 | 
			
		||||
        // Open My Items
 | 
			
		||||
        await page.locator('text=Open MCT My Items >> span').nth(3).click();
 | 
			
		||||
 | 
			
		||||
        // Select Folder Object and select Move from context menu
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            page.waitForNavigation(),
 | 
			
		||||
            page.locator(`a:has-text("${folder}")`).click()
 | 
			
		||||
        ]);
 | 
			
		||||
        await page.locator('.c-tree__item.is-navigated-object .c-tree__item__label .c-tree__item__type-icon').click({
 | 
			
		||||
            button: 'right'
 | 
			
		||||
        });
 | 
			
		||||
        await page.locator('li.icon-move').click();
 | 
			
		||||
 | 
			
		||||
        // See if it's possible to put the folder in the Telemetry object after creation
 | 
			
		||||
        await page.locator('text=Location Open MCT My Items >> span').nth(3).click();
 | 
			
		||||
        await page.locator(`form[name="mctForm"] >> text=${telemetryTable}`).click();
 | 
			
		||||
        let okButton2 = await page.locator('button.c-button.c-button--major:has-text("OK")');
 | 
			
		||||
        let okButtonStateDisabled2 = await okButton2.isDisabled();
 | 
			
		||||
        expect(okButtonStateDisabled2).toBeTruthy();
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@@ -1,177 +0,0 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2022, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
This test suite is dedicated to performance tests to ensure that testability of performance
 | 
			
		||||
is not broken upstream on Open MCT. Any assumptions made downstream will be tested here
 | 
			
		||||
 | 
			
		||||
TODO:
 | 
			
		||||
 - Update resolution of performance config
 | 
			
		||||
 - Add Performance Observer on init to push all performance marks
 | 
			
		||||
 - Move client CDP connection to before or to a fixture
 | 
			
		||||
 -
 | 
			
		||||
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
const { test, expect } = require('@playwright/test');
 | 
			
		||||
 | 
			
		||||
const filePath = 'e2e/test-data/PerformanceDisplayLayout.json';
 | 
			
		||||
 | 
			
		||||
test.describe('Performance tests', () => {
 | 
			
		||||
    test.beforeEach(async ({ page, browser }, testInfo) => {
 | 
			
		||||
        // Go to baseURL
 | 
			
		||||
        await page.goto('/', { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
        // Click a:has-text("My Items")
 | 
			
		||||
        await page.locator('a:has-text("My Items")').click({
 | 
			
		||||
            button: 'right'
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Click text=Import from JSON
 | 
			
		||||
        await page.locator('text=Import from JSON').click();
 | 
			
		||||
 | 
			
		||||
        // Upload Performance Display Layout.json
 | 
			
		||||
        await page.setInputFiles('#fileElem', filePath);
 | 
			
		||||
 | 
			
		||||
        // Click text=OK
 | 
			
		||||
        await page.locator('text=OK').click();
 | 
			
		||||
 | 
			
		||||
        await expect(page.locator('a:has-text("Performance Display Layout Display Layout")')).toBeVisible();
 | 
			
		||||
 | 
			
		||||
        //Create a Chrome Performance Timeline trace to store as a test artifact
 | 
			
		||||
        console.log("\n==== Devtools: startTracing ====\n");
 | 
			
		||||
        await browser.startTracing(page, {
 | 
			
		||||
            path: `${testInfo.outputPath()}-trace.json`,
 | 
			
		||||
            screenshots: true
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
    test.afterEach(async ({ page, browser}) => {
 | 
			
		||||
        console.log("\n==== Devtools: stopTracing ====\n");
 | 
			
		||||
        await browser.stopTracing();
 | 
			
		||||
 | 
			
		||||
        /* Measurement Section
 | 
			
		||||
        / The following section includes a block of performance measurements.
 | 
			
		||||
        */
 | 
			
		||||
        //Get time difference between viewlarge actionability and evaluate time
 | 
			
		||||
        await page.evaluate(() => (window.performance.measure("machine-time-difference", "viewlarge.start", "viewLarge.start.test")));
 | 
			
		||||
 | 
			
		||||
        //Get StartTime
 | 
			
		||||
        const startTime = await page.evaluate(() => window.performance.timing.navigationStart);
 | 
			
		||||
        console.log('window.performance.timing.navigationStart', startTime);
 | 
			
		||||
 | 
			
		||||
        //Get All Performance Marks
 | 
			
		||||
        const getAllMarksJson = await page.evaluate(() => JSON.stringify(window.performance.getEntriesByType("mark")));
 | 
			
		||||
        const getAllMarks = JSON.parse(getAllMarksJson);
 | 
			
		||||
        console.log('window.performance.getEntriesByType("mark")', getAllMarks);
 | 
			
		||||
 | 
			
		||||
        //Get All Performance Measures
 | 
			
		||||
        const getAllMeasuresJson = await page.evaluate(() => JSON.stringify(window.performance.getEntriesByType("measure")));
 | 
			
		||||
        const getAllMeasures = JSON.parse(getAllMeasuresJson);
 | 
			
		||||
        console.log('window.performance.getEntriesByType("measure")', getAllMeasures);
 | 
			
		||||
 | 
			
		||||
    });
 | 
			
		||||
    /* The following test will navigate to a previously created Performance Display Layout and measure the
 | 
			
		||||
    /  following metrics:
 | 
			
		||||
    /  - ElementResourceTiming
 | 
			
		||||
    /  - Interaction Timing
 | 
			
		||||
    */
 | 
			
		||||
    test('Embedded View Large for Imagery is performant in Fixed Time', async ({ page, browser }) => {
 | 
			
		||||
        const client = await page.context().newCDPSession(page);
 | 
			
		||||
        // Tell the DevTools session to record performance metrics
 | 
			
		||||
        // https://chromedevtools.github.io/devtools-protocol/tot/Performance/#method-getMetrics
 | 
			
		||||
        await client.send('Performance.enable');
 | 
			
		||||
        // Go to baseURL
 | 
			
		||||
        await page.goto('/');
 | 
			
		||||
 | 
			
		||||
        // Search Available after Launch
 | 
			
		||||
        await page.locator('input[type="search"]').click();
 | 
			
		||||
        await page.evaluate(() => window.performance.mark("search-available"));
 | 
			
		||||
        // Fill Search input
 | 
			
		||||
        await page.locator('input[type="search"]').fill('Performance Display Layout');
 | 
			
		||||
        await page.evaluate(() => window.performance.mark("search-entered"));
 | 
			
		||||
        //Search Result Appears and is clicked
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            page.waitForNavigation(),
 | 
			
		||||
            page.locator('a:has-text("Performance Display Layout")').first().click(),
 | 
			
		||||
            page.evaluate(() => window.performance.mark("click-search-result"))
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        //Time to Example Imagery Frame loads within Display Layout
 | 
			
		||||
        await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible'});
 | 
			
		||||
        //Time to Example Imagery object loads
 | 
			
		||||
        await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible'});
 | 
			
		||||
 | 
			
		||||
        //Get background-image url from background-image css prop
 | 
			
		||||
        const backgroundImage = await page.locator('.c-imagery__main-image__background-image');
 | 
			
		||||
        let backgroundImageUrl = await backgroundImage.evaluate((el) => {
 | 
			
		||||
            return window.getComputedStyle(el).getPropertyValue('background-image').match(/url\(([^)]+)\)/)[1];
 | 
			
		||||
        });
 | 
			
		||||
        backgroundImageUrl = backgroundImageUrl.slice(1, -1); //forgive me, padre
 | 
			
		||||
        console.log('backgroundImageurl ' + backgroundImageUrl);
 | 
			
		||||
 | 
			
		||||
        //Get ResourceTiming of background-image jpg
 | 
			
		||||
        const resourceTimingJson = await page.evaluate((bgImageUrl) =>
 | 
			
		||||
            JSON.stringify(window.performance.getEntriesByName(bgImageUrl).pop()),
 | 
			
		||||
        backgroundImageUrl
 | 
			
		||||
        );
 | 
			
		||||
        console.log('resourceTimingJson ' + resourceTimingJson);
 | 
			
		||||
 | 
			
		||||
        //Open Large view
 | 
			
		||||
        await page.locator('button:has-text("Large View")').click(); //This action includes the performance.mark named 'viewLarge.start'
 | 
			
		||||
        await page.evaluate(() => window.performance.mark("viewLarge.start.test")); //This is a mark only to compare evaluate timing
 | 
			
		||||
 | 
			
		||||
        //Time to Imagery Rendered in Large Frame
 | 
			
		||||
        await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible'});
 | 
			
		||||
        await page.evaluate(() => window.performance.mark("background-image-frame"));
 | 
			
		||||
 | 
			
		||||
        //Time to Example Imagery object loads
 | 
			
		||||
        await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible'});
 | 
			
		||||
        await page.evaluate(() => window.performance.mark("background-image-visible"));
 | 
			
		||||
 | 
			
		||||
        // Get Current number of images in thumbstrip
 | 
			
		||||
        await page.waitForSelector('.c-imagery__thumb');
 | 
			
		||||
        const thumbCount = await page.locator('.c-imagery__thumb').count();
 | 
			
		||||
        console.log('number of thumbs rendered ' + thumbCount);
 | 
			
		||||
        await page.locator('.c-imagery__thumb').last().click();
 | 
			
		||||
 | 
			
		||||
        //Get ResourceTiming of all jpg resources
 | 
			
		||||
        const resourceTimingJson2 = await page.evaluate(() =>
 | 
			
		||||
            JSON.stringify(window.performance.getEntriesByType('resource'))
 | 
			
		||||
        );
 | 
			
		||||
        const resourceTiming = JSON.parse(resourceTimingJson2);
 | 
			
		||||
        const jpgResourceTiming = resourceTiming.find((element) =>
 | 
			
		||||
            element.name.includes('.jpg')
 | 
			
		||||
        );
 | 
			
		||||
        console.log('jpgResourceTiming ' + JSON.stringify(jpgResourceTiming));
 | 
			
		||||
 | 
			
		||||
        // Click Close Icon
 | 
			
		||||
        await page.locator('.c-click-icon').click();
 | 
			
		||||
        await page.evaluate(() => window.performance.mark("view-large-close-button"));
 | 
			
		||||
 | 
			
		||||
        //await client.send('HeapProfiler.enable');
 | 
			
		||||
        await client.send('HeapProfiler.collectGarbage');
 | 
			
		||||
 | 
			
		||||
        let performanceMetrics = await client.send('Performance.getMetrics');
 | 
			
		||||
        console.log(performanceMetrics.metrics);
 | 
			
		||||
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@@ -1,136 +0,0 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2022, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
This test suite is an initial example for memory leak testing using performance. This configuration and execution must
 | 
			
		||||
be kept separate from the traditional performance measurements to avoid any "observer" effects associated with tracing
 | 
			
		||||
or profiling playwright and/or the browser.
 | 
			
		||||
 | 
			
		||||
Based on a pattern identified in https://github.com/trentmwillis/devtools-protocol-demos/blob/master/testing-demos/memory-leak-by-heap.js
 | 
			
		||||
and https://github.com/paulirish/automated-chrome-profiling/issues/3
 | 
			
		||||
 | 
			
		||||
Best path forward: https://github.com/cowchimp/headless-devtools/blob/master/src/Memory/example.js
 | 
			
		||||
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
const { test, expect } = require('@playwright/test');
 | 
			
		||||
const { isEqual, differenceWith, intersectionWith } = require('lodash');
 | 
			
		||||
 | 
			
		||||
const filePath = 'e2e/test-data/PerformanceDisplayLayout.json';
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line playwright/no-skipped-test
 | 
			
		||||
test.describe('Memory Performance tests', () => {
 | 
			
		||||
    test.beforeEach(async ({ page, browser }, testInfo) => {
 | 
			
		||||
        // Go to baseURL
 | 
			
		||||
        await page.goto('/', { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
        // Click a:has-text("My Items")
 | 
			
		||||
        await page.locator('a:has-text("My Items")').click({
 | 
			
		||||
            button: 'right'
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Click text=Import from JSON
 | 
			
		||||
        await page.locator('text=Import from JSON').click();
 | 
			
		||||
 | 
			
		||||
        // Upload Performance Display Layout.json
 | 
			
		||||
        await page.setInputFiles('#fileElem', filePath);
 | 
			
		||||
 | 
			
		||||
        // Click text=OK
 | 
			
		||||
        await page.locator('text=OK').click();
 | 
			
		||||
 | 
			
		||||
        await expect(page.locator('a:has-text("Performance Display Layout Display Layout")')).toBeVisible();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test.only('Embedded View Large for Imagery is performant in Fixed Time', async ({ page, browser }) => {
 | 
			
		||||
 | 
			
		||||
        // // To to Search Available after Launch
 | 
			
		||||
        // await page.locator('input[type="search"]').click();
 | 
			
		||||
        // // Fill Search input
 | 
			
		||||
        // await page.locator('input[type="search"]').fill('Performance Display Layout');
 | 
			
		||||
        // //Search Result Appears and is clicked
 | 
			
		||||
        // await Promise.all([
 | 
			
		||||
        //     page.waitForNavigation(),
 | 
			
		||||
        //     page.locator('a:has-text("Performance Display Layout")').first().click()
 | 
			
		||||
        // ]);
 | 
			
		||||
 | 
			
		||||
        // //Time to Example Imagery Frame loads within Display Layout
 | 
			
		||||
        // await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible'});
 | 
			
		||||
        // //Time to Example Imagery object loads
 | 
			
		||||
        // await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible'});
 | 
			
		||||
 | 
			
		||||
        const client = await page.context().newCDPSession(page);
 | 
			
		||||
        await client.send('HeapProfiler.enable');
 | 
			
		||||
        // await client.send('HeapProfiler.startSampling');
 | 
			
		||||
        await client.send('Performance.enable');
 | 
			
		||||
        await client.send('HeapProfiler.collectGarbage');
 | 
			
		||||
 | 
			
		||||
        let performanceMetricsBefore = await client.send('Performance.getMetrics');
 | 
			
		||||
        //console.log(performanceMetricsBefore.metrics);
 | 
			
		||||
 | 
			
		||||
        await expect(page.locator('a:has-text("Performance Display Layout Display Layout")')).toBeVisible();
 | 
			
		||||
        await expect(page.locator('a:has-text("Performance Display Layout Display Layout")')).toBeVisible();
 | 
			
		||||
        await expect(page.locator('a:has-text("Performance Display Layout Display Layout")')).toBeVisible();
 | 
			
		||||
        await expect(page.locator('a:has-text("Performance Display Layout Display Layout")')).toBeVisible();
 | 
			
		||||
        await expect(page.locator('a:has-text("Performance Display Layout Display Layout")')).toBeVisible();
 | 
			
		||||
 | 
			
		||||
        //await client.send('Performance.disable');
 | 
			
		||||
 | 
			
		||||
        //Open Large view
 | 
			
		||||
        // await page.locator('button:has-text("Large View")').click();
 | 
			
		||||
        //await client.send('HeapProfiler.takeHeapSnapshot');
 | 
			
		||||
 | 
			
		||||
        // //Time to Imagery Rendered in Large Frame
 | 
			
		||||
        // await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible'});
 | 
			
		||||
 | 
			
		||||
        // //Time to Example Imagery object loads
 | 
			
		||||
        // await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible'});
 | 
			
		||||
 | 
			
		||||
        // // Click Close Icon
 | 
			
		||||
        // await page.locator('.c-click-icon').click();
 | 
			
		||||
 | 
			
		||||
        // //Time to Example Imagery Frame loads within Display Layout
 | 
			
		||||
        // await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible'});
 | 
			
		||||
        // //Time to Example Imagery object loads
 | 
			
		||||
        // await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible'});
 | 
			
		||||
        await client.send('HeapProfiler.collectGarbage');
 | 
			
		||||
 | 
			
		||||
        let performanceMetricsAfter = await client.send('Performance.getMetrics');
 | 
			
		||||
        console.log('typeOf performanceMetricsAfter ' + typeof performanceMetricsAfter);
 | 
			
		||||
        let JSHeapUsedSizeAfterObject = performanceMetricsAfter.metrics.filter(c => c.name === 'JSHeapUsedSize');
 | 
			
		||||
        console.log('JSHeapUsedSizeAfterObject ' + JSON.stringify(JSHeapUsedSizeAfterObject));
 | 
			
		||||
        console.log(JSHeapUsedSizeAfterObject[0].value);
 | 
			
		||||
        console.log('performanceMetricsAfter.metrics.JSHeapUsedSize ' + performanceMetricsAfter.metrics.JSHeapUsedSize);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        let myDifferences = intersectionWith(performanceMetricsBefore, performanceMetricsAfter, isEqual);
 | 
			
		||||
        console.log(myDifferences);
 | 
			
		||||
        //assert.ok(performanceMetricsAfter.JSHeapUsedSize < performanceMetricsBefore.JSHeapUsedSize * 1.1);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        //console.log(performanceMetricsAfter.entries(JSEventListeners).toString());
 | 
			
		||||
        //performanceMetricsAfter.entries(k1).toString() === Object.entries(k2).toString();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        //await client.send('Performance.disable');
 | 
			
		||||
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@@ -1,158 +0,0 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2022, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
This test suite is dedicated to performance tests to ensure that testability of performance
 | 
			
		||||
is not broken upstream on Open MCT. Any assumptions made downstream will be tested here.
 | 
			
		||||
 | 
			
		||||
TODO:
 | 
			
		||||
 - Update resolution of performance config
 | 
			
		||||
 - Add Performance Observer on init to push all performance marks
 | 
			
		||||
 - Move client CDP connection to before or to a fixture
 | 
			
		||||
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
const { test, expect } = require('@playwright/test');
 | 
			
		||||
 | 
			
		||||
const notebookFilePath = 'e2e/test-data/PerformanceNotebook.json';
 | 
			
		||||
 | 
			
		||||
test.describe('Performance tests', () => {
 | 
			
		||||
    test.beforeEach(async ({ page, browser }, testInfo) => {
 | 
			
		||||
        // Go to baseURL
 | 
			
		||||
        await page.goto('/', { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
        // Click a:has-text("My Items")
 | 
			
		||||
        await page.locator('a:has-text("My Items")').click({
 | 
			
		||||
            button: 'right'
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Click text=Import from JSON
 | 
			
		||||
        await page.locator('text=Import from JSON').click();
 | 
			
		||||
 | 
			
		||||
        // Upload Performance Display Layout.json
 | 
			
		||||
        await page.setInputFiles('#fileElem', notebookFilePath);
 | 
			
		||||
 | 
			
		||||
        // TODO Fix this
 | 
			
		||||
        await page.locator('text=OK >> nth=1').click();
 | 
			
		||||
 | 
			
		||||
        await expect(page.locator('a:has-text("Performance Notebook")')).toBeVisible();
 | 
			
		||||
 | 
			
		||||
        //Create a Chrome Performance Timeline trace to store as a test artifact
 | 
			
		||||
        console.log("\n==== Devtools: startTracing ====\n");
 | 
			
		||||
        await browser.startTracing(page, {
 | 
			
		||||
            path: `${testInfo.outputPath()}-trace.json`,
 | 
			
		||||
            screenshots: true
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
    test.afterEach(async ({ page, browser}) => {
 | 
			
		||||
        console.log("\n==== Devtools: stopTracing ====\n");
 | 
			
		||||
        await browser.stopTracing();
 | 
			
		||||
 | 
			
		||||
        /* Measurement Section
 | 
			
		||||
        / The following section includes a block of performance measurements.
 | 
			
		||||
        */
 | 
			
		||||
        const startTime = await page.evaluate(() => window.performance.timing.navigationStart);
 | 
			
		||||
        console.log('window.performance.timing.navigationStart', startTime);
 | 
			
		||||
 | 
			
		||||
        //Get All Performance Marks
 | 
			
		||||
        const getAllMarksJson = await page.evaluate(() => JSON.stringify(window.performance.getEntriesByType("mark")));
 | 
			
		||||
        const getAllMarks = JSON.parse(getAllMarksJson);
 | 
			
		||||
        console.log('window.performance.getEntriesByType("mark")', getAllMarks);
 | 
			
		||||
 | 
			
		||||
        //Get All Performance Measures
 | 
			
		||||
        const getAllMeasuresJson = await page.evaluate(() => JSON.stringify(window.performance.getEntriesByType("measure")));
 | 
			
		||||
        const getAllMeasures = JSON.parse(getAllMeasuresJson);
 | 
			
		||||
        console.log('window.performance.getEntriesByType("measure")', getAllMeasures);
 | 
			
		||||
 | 
			
		||||
    });
 | 
			
		||||
    /* The following test will navigate to a previously created Performance Display Layout and measure the
 | 
			
		||||
    /  following metrics:
 | 
			
		||||
    /  - ElementResourceTiming
 | 
			
		||||
    /  - Interaction Timing
 | 
			
		||||
    */
 | 
			
		||||
    test('Notebook Search, Add Entry, Update Entry are performant', async ({ page, browser }) => {
 | 
			
		||||
        const client = await page.context().newCDPSession(page);
 | 
			
		||||
        // Tell the DevTools session to record performance metrics
 | 
			
		||||
        // https://chromedevtools.github.io/devtools-protocol/tot/Performance/#method-getMetrics
 | 
			
		||||
        await client.send('Performance.enable');
 | 
			
		||||
        // Go to baseURL
 | 
			
		||||
        await page.goto('/');
 | 
			
		||||
 | 
			
		||||
        // To to Search Available after Launch
 | 
			
		||||
        await page.locator('input[type="search"]').click();
 | 
			
		||||
        await page.evaluate(() => window.performance.mark("search-available"));
 | 
			
		||||
        // Fill Search input
 | 
			
		||||
        await page.locator('input[type="search"]').fill('Performance Notebook');
 | 
			
		||||
        await page.evaluate(() => window.performance.mark("search-entered"));
 | 
			
		||||
        //Search Result Appears and is clicked
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            page.waitForNavigation(),
 | 
			
		||||
            page.locator('a:has-text("Performance Notebook")').first().click(),
 | 
			
		||||
            page.evaluate(() => window.performance.mark("click-search-result"))
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        await page.waitForSelector('.c-tree__item c-tree-and-search__loading loading', {state: 'hidden'});
 | 
			
		||||
        await page.evaluate(() => window.performance.mark("search-spinner-gone"));
 | 
			
		||||
 | 
			
		||||
        await page.waitForSelector('.l-browse-bar__object-name', { state: 'visible'});
 | 
			
		||||
        await page.evaluate(() => window.performance.mark("object-title-appears"));
 | 
			
		||||
 | 
			
		||||
        await page.waitForSelector('.c-notebook__entry >> nth=0', { state: 'visible'});
 | 
			
		||||
        await page.evaluate(() => window.performance.mark("notebook-entry-appears"));
 | 
			
		||||
 | 
			
		||||
        // Click Add new Notebook Entry
 | 
			
		||||
        await page.locator('.c-notebook__drag-area').click();
 | 
			
		||||
        await page.evaluate(() => window.performance.mark("new-notebook-entry-created"));
 | 
			
		||||
 | 
			
		||||
        // Enter Notebook Entry text
 | 
			
		||||
        await page.locator('div.c-ne__text').last().fill('New Entry');
 | 
			
		||||
        await page.keyboard.press('Enter');
 | 
			
		||||
        await page.evaluate(() => window.performance.mark("new-notebook-entry-filled"));
 | 
			
		||||
 | 
			
		||||
        //Individual Notebook Entry Search
 | 
			
		||||
        await page.evaluate(() => window.performance.mark("notebook-search-start"));
 | 
			
		||||
        await page.locator('.c-notebook__search >> input').fill('Existing Entry');
 | 
			
		||||
        await page.evaluate(() => window.performance.mark("notebook-search-filled"));
 | 
			
		||||
        await page.waitForSelector('text=Search Results (3)', { state: 'visible'});
 | 
			
		||||
        await page.evaluate(() => window.performance.mark("notebook-search-processed"));
 | 
			
		||||
        await page.waitForSelector('.c-notebook__entry >> nth=2', { state: 'visible'});
 | 
			
		||||
        await page.evaluate(() => window.performance.mark("notebook-search-processed"));
 | 
			
		||||
 | 
			
		||||
        //Clear Search
 | 
			
		||||
        await page.locator('.c-search.c-notebook__search .c-search__clear-input').click();
 | 
			
		||||
        await page.evaluate(() => window.performance.mark("notebook-search-processed"));
 | 
			
		||||
 | 
			
		||||
        // Hover on Last
 | 
			
		||||
        await page.evaluate(() => window.performance.mark("new-notebook-entry-delete"));
 | 
			
		||||
        await page.locator('div.c-ne__time-and-content').last().hover();
 | 
			
		||||
        await page.locator('button[title="Delete this entry"]').last().click();
 | 
			
		||||
        await page.locator('button:has-text("Ok")').click();
 | 
			
		||||
        await page.waitForSelector('.c-notebook__entry >> nth=3', { state: 'detached'});
 | 
			
		||||
        await page.evaluate(() => window.performance.mark("new-notebook-entry-deleted"));
 | 
			
		||||
 | 
			
		||||
        //await client.send('HeapProfiler.enable');
 | 
			
		||||
        await client.send('HeapProfiler.collectGarbage');
 | 
			
		||||
 | 
			
		||||
        let performanceMetrics = await client.send('Performance.getMetrics');
 | 
			
		||||
        console.log(performanceMetrics.metrics);
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@@ -1,27 +0,0 @@
 | 
			
		||||
(function () {
 | 
			
		||||
    document.addEventListener('DOMContentLoaded', () => {
 | 
			
		||||
        const PERSISTENCE_KEY = 'persistence-tests';
 | 
			
		||||
        const openmct = window.openmct;
 | 
			
		||||
 | 
			
		||||
        openmct.objects.addRoot({
 | 
			
		||||
            namespace: PERSISTENCE_KEY,
 | 
			
		||||
            key: PERSISTENCE_KEY
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        openmct.objects.addProvider(PERSISTENCE_KEY, {
 | 
			
		||||
            get(identifier) {
 | 
			
		||||
                if (identifier.key !== PERSISTENCE_KEY) {
 | 
			
		||||
                    return undefined;
 | 
			
		||||
                } else {
 | 
			
		||||
                    return Promise.resolve({
 | 
			
		||||
                        identifier,
 | 
			
		||||
                        type: 'folder',
 | 
			
		||||
                        name: 'Persistence Testing',
 | 
			
		||||
                        location: 'ROOT',
 | 
			
		||||
                        composition: []
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
}());
 | 
			
		||||
@@ -1,78 +0,0 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2022, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
This test suite is dedicated to tests which verify the basic operations surrounding conditionSets.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
const { test } = require('../../fixtures.js');
 | 
			
		||||
const { expect } = require('@playwright/test');
 | 
			
		||||
const path = require('path');
 | 
			
		||||
 | 
			
		||||
// https://github.com/nasa/openmct/issues/4323#issuecomment-1067282651
 | 
			
		||||
 | 
			
		||||
test.describe('Persistence operations', () => {
 | 
			
		||||
    // add non persistable root item
 | 
			
		||||
    test.beforeEach(async ({ page }) => {
 | 
			
		||||
        // eslint-disable-next-line no-undef
 | 
			
		||||
        await page.addInitScript({ path: path.join(__dirname, 'addNoneditableObject.js') });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('Persistability should be respected in the create form location field', async ({ page }) => {
 | 
			
		||||
        // Go to baseURL
 | 
			
		||||
        await page.goto('/', { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
        // Click the Create button
 | 
			
		||||
        await page.click('button:has-text("Create")');
 | 
			
		||||
 | 
			
		||||
        // Click text=Condition Set
 | 
			
		||||
        await page.click('text=Condition Set');
 | 
			
		||||
 | 
			
		||||
        // Click form[name="mctForm"] >> text=Persistence Testing
 | 
			
		||||
        await page.locator('form[name="mctForm"] >> text=Persistence Testing').click();
 | 
			
		||||
 | 
			
		||||
        // Check that "OK" button is disabled
 | 
			
		||||
        const okButton = page.locator('button:has-text("OK")');
 | 
			
		||||
        await expect(okButton).toBeDisabled();
 | 
			
		||||
    });
 | 
			
		||||
    test('Non-persistable objects should not show persistence related actions', async ({ page }) => {
 | 
			
		||||
        // Go to baseURL
 | 
			
		||||
        await page.goto('/', { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
        // Click text=Persistence Testing >> nth=0
 | 
			
		||||
        await page.locator('text=Persistence Testing').first().click({
 | 
			
		||||
            button: 'right'
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const menuOptions = page.locator('.c-menu ul');
 | 
			
		||||
 | 
			
		||||
        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']);
 | 
			
		||||
    });
 | 
			
		||||
    test.fixme('Cannot move a previously created domain object to non-peristable boject 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
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@@ -1,51 +0,0 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2022, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
This test suite is dedicated to tests which verify the basic operations surrounding exportAsJSON.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
const { test } = require('../../../fixtures.js');
 | 
			
		||||
// FIXME: Remove this eslint exception once tests are implemented
 | 
			
		||||
// eslint-disable-next-line no-unused-vars
 | 
			
		||||
const { expect } = require('@playwright/test');
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@@ -1,49 +0,0 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2022, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
This test suite is dedicated to tests which verify the basic operations surrounding importAsJSON.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
const { test } = require('../../../fixtures.js');
 | 
			
		||||
// FIXME: Remove this eslint exception once tests are implemented
 | 
			
		||||
// eslint-disable-next-line no-unused-vars
 | 
			
		||||
const { expect } = require('@playwright/test');
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@@ -1,67 +0,0 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2022, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
This test suite is dedicated to tests which verify the basic operations surrounding Clock.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
const { test } = require('../../../fixtures.js');
 | 
			
		||||
const { expect } = require('@playwright/test');
 | 
			
		||||
 | 
			
		||||
test.describe('Clock Generator', () => {
 | 
			
		||||
 | 
			
		||||
    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: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
        //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(".optionPreSelected")).toBeVisible();
 | 
			
		||||
        // Click .icon-arrow-down
 | 
			
		||||
        await page.locator('.icon-arrow-down').click();
 | 
			
		||||
 | 
			
		||||
        // Verify clicking on the autocomplete arrow collapses the dropdown
 | 
			
		||||
        await expect(page.locator(".optionPreSelected")).not.toBeVisible();
 | 
			
		||||
 | 
			
		||||
        // Click timezone input to open dropdown
 | 
			
		||||
        await page.locator('.autocompleteInput').click();
 | 
			
		||||
        //verify if the autocomplete dropdown is visible
 | 
			
		||||
        await expect(page.locator(".optionPreSelected")).toBeVisible();
 | 
			
		||||
 | 
			
		||||
        // Verify clicking outside the autocomplete dropdown collapses it
 | 
			
		||||
        await page.locator('text=Timezone').click();
 | 
			
		||||
        // Verify clicking on the autocomplete arrow collapses the dropdown
 | 
			
		||||
        await expect(page.locator(".optionPreSelected")).not.toBeVisible();
 | 
			
		||||
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@@ -21,167 +21,45 @@
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
This test suite is dedicated to tests which verify the basic operations surrounding conditionSets. Note: this
 | 
			
		||||
suite is sharing state between tests which is considered an anti-pattern. Implimenting in this way to
 | 
			
		||||
demonstrate some playwright for test developers. This pattern should not be re-used in other CRUD suites.
 | 
			
		||||
This test suite is dedicated to tests which verify the basic operations surrounding conditionSets.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
const { test } = require('../../../fixtures.js');
 | 
			
		||||
const { expect } = require('@playwright/test');
 | 
			
		||||
const { test, expect } = require('@playwright/test');
 | 
			
		||||
 | 
			
		||||
let conditionSetUrl;
 | 
			
		||||
let getConditionSetIdentifierFromUrl;
 | 
			
		||||
 | 
			
		||||
test('Create new Condition Set object and store @localStorage', async ({ page, context }) => {
 | 
			
		||||
    //Go to baseURL
 | 
			
		||||
    await page.goto('/', { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
    //Click the Create button
 | 
			
		||||
    await page.click('button:has-text("Create")');
 | 
			
		||||
 | 
			
		||||
    // Click text=Condition Set
 | 
			
		||||
    await page.click('text=Condition Set');
 | 
			
		||||
 | 
			
		||||
    // Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184
 | 
			
		||||
    await page.click('form[name="mctForm"] a:has-text("My Items")');
 | 
			
		||||
 | 
			
		||||
    // Click text=OK
 | 
			
		||||
    await Promise.all([
 | 
			
		||||
        page.waitForNavigation(),
 | 
			
		||||
        page.click('text=OK')
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
 | 
			
		||||
    //Save localStorage for future test execution
 | 
			
		||||
    await context.storageState({ path: './e2e/tests/recycled_storage.json' });
 | 
			
		||||
 | 
			
		||||
    //Set object identifier from url
 | 
			
		||||
    conditionSetUrl = await page.url();
 | 
			
		||||
    console.log('conditionSetUrl ' + conditionSetUrl);
 | 
			
		||||
 | 
			
		||||
    getConditionSetIdentifierFromUrl = await conditionSetUrl.split('/').pop().split('?')[0];
 | 
			
		||||
    console.log('getConditionSetIdentifierFromUrl ' + getConditionSetIdentifierFromUrl);
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
 | 
			
		||||
    //Load localStorage for subsequent tests
 | 
			
		||||
    test.use({ storageState: './e2e/tests/recycled_storage.json' });
 | 
			
		||||
 | 
			
		||||
    //Begin suite of tests again localStorage
 | 
			
		||||
    test('Condition set object properties persist in main view and inspector', async ({ page }) => {
 | 
			
		||||
        //Navigate to baseURL with injected localStorage
 | 
			
		||||
        await page.goto(conditionSetUrl, { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
        //Assertions on loaded Condition Set in main view
 | 
			
		||||
        await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
 | 
			
		||||
 | 
			
		||||
        //Assertions on loaded Condition Set in Inspector
 | 
			
		||||
        await expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy;
 | 
			
		||||
 | 
			
		||||
        //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
 | 
			
		||||
        await expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy;
 | 
			
		||||
 | 
			
		||||
    });
 | 
			
		||||
    test('condition set object can be modified on @localStorage', async ({ page }) => {
 | 
			
		||||
        await page.goto(conditionSetUrl, { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
        //Assertions on loaded Condition Set in main view
 | 
			
		||||
        await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
 | 
			
		||||
 | 
			
		||||
        //Update the Condition Set properties
 | 
			
		||||
        // Click Edit Button
 | 
			
		||||
        await page.locator('text=Conditions View Snapshot >> button').nth(3).click();
 | 
			
		||||
 | 
			
		||||
        //Edit Condition Set Name from main view
 | 
			
		||||
        await page.locator('text=Unnamed Condition Set').first().fill('Renamed Condition Set');
 | 
			
		||||
        await page.locator('text=Renamed Condition Set').first().press('Enter');
 | 
			
		||||
        // Click Save Button
 | 
			
		||||
        await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
 | 
			
		||||
        // Click Save and Finish Editing Option
 | 
			
		||||
        await page.locator('text=Save and Finish Editing').click();
 | 
			
		||||
 | 
			
		||||
        //Verify Main section reflects updated Name Property
 | 
			
		||||
        await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Renamed Condition Set');
 | 
			
		||||
 | 
			
		||||
        // Verify Inspector properties
 | 
			
		||||
        // Verify Inspector has updated Name property
 | 
			
		||||
        await expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy();
 | 
			
		||||
        // Verify Inspector Details has updated Name property
 | 
			
		||||
        await expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy();
 | 
			
		||||
 | 
			
		||||
        // Verify Tree reflects updated Name proprety
 | 
			
		||||
        // Expand Tree
 | 
			
		||||
        await page.locator('text=Open MCT My Items >> span >> nth=3').click();
 | 
			
		||||
        // Verify Condition Set Object is renamed in Tree
 | 
			
		||||
        await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
 | 
			
		||||
        // Verify Search Tree reflects renamed Name property
 | 
			
		||||
        await page.locator('input[type="search"]').fill('Renamed');
 | 
			
		||||
        await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
 | 
			
		||||
 | 
			
		||||
        //Reload Page
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            page.reload(),
 | 
			
		||||
            page.waitForLoadState('networkidle')
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        //Verify Main section reflects updated Name Property
 | 
			
		||||
        await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Renamed Condition Set');
 | 
			
		||||
 | 
			
		||||
        // Verify Inspector properties
 | 
			
		||||
        // Verify Inspector has updated Name property
 | 
			
		||||
        await expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy();
 | 
			
		||||
        // Verify Inspector Details has updated Name property
 | 
			
		||||
        await expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy();
 | 
			
		||||
 | 
			
		||||
        // Verify Tree reflects updated Name proprety
 | 
			
		||||
        // Expand Tree
 | 
			
		||||
        await page.locator('text=Open MCT My Items >> span >> nth=3').click();
 | 
			
		||||
        // Verify Condition Set Object is renamed in Tree
 | 
			
		||||
        await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
 | 
			
		||||
        // Verify Search Tree reflects renamed Name property
 | 
			
		||||
        await page.locator('input[type="search"]').fill('Renamed');
 | 
			
		||||
        await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
 | 
			
		||||
    });
 | 
			
		||||
    test('condition set object can be deleted by Search Tree Actions menu on @localStorage', async ({ page }) => {
 | 
			
		||||
        //Navigate to baseURL
 | 
			
		||||
test.describe('Condition Set Operations', () => {
 | 
			
		||||
    test('Create new button `condition set` creates new condition object', async ({ page }) => {
 | 
			
		||||
        //Go to baseURL
 | 
			
		||||
        await page.goto('/', { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
        //Expect Unnamed Condition Set to be visible in Main View
 | 
			
		||||
        await expect(page.locator('a:has-text("Unnamed Condition Set Condition Set")')).toBeVisible();
 | 
			
		||||
        //Click the Create button
 | 
			
		||||
        await page.click('button:has-text("Create")');
 | 
			
		||||
 | 
			
		||||
        // Search for Unnamed Condition Set
 | 
			
		||||
        await page.locator('input[type="search"]').fill('Unnamed Condition Set');
 | 
			
		||||
        // Right Click to Open Actions Menu
 | 
			
		||||
        await page.locator('a:has-text("Unnamed Condition Set")').click({
 | 
			
		||||
            button: 'right'
 | 
			
		||||
        });
 | 
			
		||||
        // Click Remove Action
 | 
			
		||||
        await page.locator('text=Remove').click();
 | 
			
		||||
        // Click text=Condition Set
 | 
			
		||||
        await page.click('text=Condition Set');
 | 
			
		||||
 | 
			
		||||
        await page.locator('text=OK').click();
 | 
			
		||||
        // Click text=OK
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine/dab945d4-5a84-480e-8180-222b4aa730fa?tc.mode=fixed&tc.startBound=1639696164435&tc.endBound=1639697964435&tc.timeSystem=utc&view=conditionSet.view' }*/),
 | 
			
		||||
            page.click('text=OK')
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        //Expect Unnamed Condition Set to be removed in Main View
 | 
			
		||||
        await expect(page.locator('a:has-text("Unnamed Condition Set Condition Set")')).not.toBeVisible();
 | 
			
		||||
 | 
			
		||||
        await page.locator('.c-search__clear-input').click();
 | 
			
		||||
        // Search for Unnamed Condition Set
 | 
			
		||||
        await page.locator('input[type="search"]').fill('Unnamed Condition Set');
 | 
			
		||||
        // Expect Unnamed Condition Set to be removed
 | 
			
		||||
        await expect(page.locator('a:has-text("Unnamed Condition Set")')).not.toBeVisible();
 | 
			
		||||
 | 
			
		||||
        //Feature?
 | 
			
		||||
        //Domain Object is still available by direct URL after delete
 | 
			
		||||
        await page.goto(conditionSetUrl, { waitUntil: 'networkidle' });
 | 
			
		||||
        await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
 | 
			
		||||
 | 
			
		||||
    });
 | 
			
		||||
    test.fixme('condition set object properties exist', async ({ page }) => {
 | 
			
		||||
        //Go to object created in step one
 | 
			
		||||
        //Verify the Condition Set properties persist on Save
 | 
			
		||||
        //Verify the Condition Set properties persist on page.reload()
 | 
			
		||||
    });
 | 
			
		||||
    test.fixme('condition set object can be modified', async ({ page }) => {
 | 
			
		||||
        //Go to object created in step one
 | 
			
		||||
        //Update the Condition Set properties
 | 
			
		||||
        //Verify the Condition Set properties persist on Save
 | 
			
		||||
        //Verify the Condition Set properties persist on page.reload()
 | 
			
		||||
    });
 | 
			
		||||
    test.fixme('condition set object can be deleted', async ({ page }) => {
 | 
			
		||||
        //Go to object created in step one
 | 
			
		||||
        //Verify that Condition Set object can be deleted
 | 
			
		||||
        //Verify the Condition Set object does not exist in Tree
 | 
			
		||||
        //Verify the Condition Set object does not exist with direct navigation to object's URL
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,475 +0,0 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2022, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
This test suite is dedicated to tests which verify the basic operations surrounding imagery,
 | 
			
		||||
but only assume that example imagery is present.
 | 
			
		||||
*/
 | 
			
		||||
/* globals process */
 | 
			
		||||
 | 
			
		||||
const { test } = require('../../../fixtures.js');
 | 
			
		||||
const { expect } = require('@playwright/test');
 | 
			
		||||
 | 
			
		||||
test.describe('Example Imagery', () => {
 | 
			
		||||
 | 
			
		||||
    test.beforeEach(async ({ page }) => {
 | 
			
		||||
        page.on('console', msg => console.log(msg.text()));
 | 
			
		||||
        //Go to baseURL
 | 
			
		||||
        await page.goto('/', { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
        //Click the Create button
 | 
			
		||||
        await page.click('button:has-text("Create")');
 | 
			
		||||
 | 
			
		||||
        // Click text=Example Imagery
 | 
			
		||||
        await page.click('text=Example Imagery');
 | 
			
		||||
 | 
			
		||||
        // Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184
 | 
			
		||||
        await page.click('form[name="mctForm"] a:has-text("My Items")');
 | 
			
		||||
 | 
			
		||||
        // Click text=OK
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            page.waitForNavigation({waitUntil: 'networkidle'}),
 | 
			
		||||
            page.click('text=OK'),
 | 
			
		||||
            //Wait for Save Banner to appear
 | 
			
		||||
            page.waitForSelector('.c-message-banner__message')
 | 
			
		||||
        ]);
 | 
			
		||||
        //Wait until Save Banner is gone
 | 
			
		||||
        await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
 | 
			
		||||
        await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const backgroundImageSelector = '.c-imagery__main-image__background-image';
 | 
			
		||||
    test('Can use Mouse Wheel to zoom in and out of latest image', async ({ page }) => {
 | 
			
		||||
        const bgImageLocator = page.locator(backgroundImageSelector);
 | 
			
		||||
        const deltaYStep = 100; //equivalent to 1x zoom
 | 
			
		||||
        await bgImageLocator.hover();
 | 
			
		||||
        const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox();
 | 
			
		||||
        // zoom in
 | 
			
		||||
        await bgImageLocator.hover();
 | 
			
		||||
        await page.mouse.wheel(0, deltaYStep * 2);
 | 
			
		||||
        // wait for zoom animation to finish
 | 
			
		||||
        await bgImageLocator.hover();
 | 
			
		||||
        const imageMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox();
 | 
			
		||||
        // zoom out
 | 
			
		||||
        await bgImageLocator.hover();
 | 
			
		||||
        await page.mouse.wheel(0, -deltaYStep);
 | 
			
		||||
        // wait for zoom animation to finish
 | 
			
		||||
        await bgImageLocator.hover();
 | 
			
		||||
        const imageMouseZoomedOut = await page.locator(backgroundImageSelector).boundingBox();
 | 
			
		||||
 | 
			
		||||
        expect(imageMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height);
 | 
			
		||||
        expect(imageMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width);
 | 
			
		||||
        expect(imageMouseZoomedOut.height).toBeLessThan(imageMouseZoomedIn.height);
 | 
			
		||||
        expect(imageMouseZoomedOut.width).toBeLessThan(imageMouseZoomedIn.width);
 | 
			
		||||
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('Can use alt+drag to move around image once zoomed in', async ({ page }) => {
 | 
			
		||||
        const deltaYStep = 100; //equivalent to 1x zoom
 | 
			
		||||
        const panHotkey = process.platform === 'linux' ? ['Control', 'Alt'] : ['Alt'];
 | 
			
		||||
 | 
			
		||||
        const bgImageLocator = page.locator(backgroundImageSelector);
 | 
			
		||||
        await bgImageLocator.hover();
 | 
			
		||||
 | 
			
		||||
        // zoom in
 | 
			
		||||
        await page.mouse.wheel(0, deltaYStep * 2);
 | 
			
		||||
        await bgImageLocator.hover();
 | 
			
		||||
        const zoomedBoundingBox = await bgImageLocator.boundingBox();
 | 
			
		||||
        const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;
 | 
			
		||||
        const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
 | 
			
		||||
        // move to the right
 | 
			
		||||
 | 
			
		||||
        // center the mouse pointer
 | 
			
		||||
        await page.mouse.move(imageCenterX, imageCenterY);
 | 
			
		||||
 | 
			
		||||
        //Get Diagnostic info about process environment
 | 
			
		||||
        console.log('process.platform is ' + process.platform);
 | 
			
		||||
        const getUA = await page.evaluate(() => navigator.userAgent);
 | 
			
		||||
        console.log('navigator.userAgent ' + getUA);
 | 
			
		||||
        // Pan Imagery Hints
 | 
			
		||||
        const expectedAltText = process.platform === 'linux' ? 'Ctrl+Alt drag to pan' : 'Alt drag to pan';
 | 
			
		||||
        const imageryHintsText = await page.locator('.c-imagery__hints').innerText();
 | 
			
		||||
        expect(expectedAltText).toEqual(imageryHintsText);
 | 
			
		||||
 | 
			
		||||
        // pan right
 | 
			
		||||
        await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
 | 
			
		||||
        await page.mouse.down();
 | 
			
		||||
        await page.mouse.move(imageCenterX - 200, imageCenterY, 10);
 | 
			
		||||
        await page.mouse.up();
 | 
			
		||||
        await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
 | 
			
		||||
        const afterRightPanBoundingBox = await bgImageLocator.boundingBox();
 | 
			
		||||
        expect(zoomedBoundingBox.x).toBeGreaterThan(afterRightPanBoundingBox.x);
 | 
			
		||||
 | 
			
		||||
        // pan left
 | 
			
		||||
        await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
 | 
			
		||||
        await page.mouse.down();
 | 
			
		||||
        await page.mouse.move(imageCenterX, imageCenterY, 10);
 | 
			
		||||
        await page.mouse.up();
 | 
			
		||||
        await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
 | 
			
		||||
        const afterLeftPanBoundingBox = await bgImageLocator.boundingBox();
 | 
			
		||||
        expect(afterRightPanBoundingBox.x).toBeLessThan(afterLeftPanBoundingBox.x);
 | 
			
		||||
 | 
			
		||||
        // pan up
 | 
			
		||||
        await page.mouse.move(imageCenterX, imageCenterY);
 | 
			
		||||
        await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
 | 
			
		||||
        await page.mouse.down();
 | 
			
		||||
        await page.mouse.move(imageCenterX, imageCenterY + 200, 10);
 | 
			
		||||
        await page.mouse.up();
 | 
			
		||||
        await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
 | 
			
		||||
        const afterUpPanBoundingBox = await bgImageLocator.boundingBox();
 | 
			
		||||
        expect(afterUpPanBoundingBox.y).toBeGreaterThan(afterLeftPanBoundingBox.y);
 | 
			
		||||
 | 
			
		||||
        // pan down
 | 
			
		||||
        await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
 | 
			
		||||
        await page.mouse.down();
 | 
			
		||||
        await page.mouse.move(imageCenterX, imageCenterY - 200, 10);
 | 
			
		||||
        await page.mouse.up();
 | 
			
		||||
        await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
 | 
			
		||||
        const afterDownPanBoundingBox = await bgImageLocator.boundingBox();
 | 
			
		||||
        expect(afterDownPanBoundingBox.y).toBeLessThan(afterUpPanBoundingBox.y);
 | 
			
		||||
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('Can use + - buttons to zoom on the image', async ({ page }) => {
 | 
			
		||||
        const bgImageLocator = page.locator(backgroundImageSelector);
 | 
			
		||||
        await bgImageLocator.hover();
 | 
			
		||||
        const zoomInBtn = page.locator('.t-btn-zoom-in');
 | 
			
		||||
        const zoomOutBtn = page.locator('.t-btn-zoom-out');
 | 
			
		||||
        const initialBoundingBox = await bgImageLocator.boundingBox();
 | 
			
		||||
 | 
			
		||||
        await zoomInBtn.click();
 | 
			
		||||
        await zoomInBtn.click();
 | 
			
		||||
        // wait for zoom animation to finish
 | 
			
		||||
        await bgImageLocator.hover();
 | 
			
		||||
        const zoomedInBoundingBox = await bgImageLocator.boundingBox();
 | 
			
		||||
        expect(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
 | 
			
		||||
        expect(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);
 | 
			
		||||
 | 
			
		||||
        await zoomOutBtn.click();
 | 
			
		||||
        // wait for zoom animation to finish
 | 
			
		||||
        await bgImageLocator.hover();
 | 
			
		||||
        const zoomedOutBoundingBox = await bgImageLocator.boundingBox();
 | 
			
		||||
        expect(zoomedOutBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height);
 | 
			
		||||
        expect(zoomedOutBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width);
 | 
			
		||||
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('Can use the reset button to reset the image', async ({ page }) => {
 | 
			
		||||
        const bgImageLocator = page.locator(backgroundImageSelector);
 | 
			
		||||
        // wait for zoom animation to finish
 | 
			
		||||
        await bgImageLocator.hover();
 | 
			
		||||
 | 
			
		||||
        const zoomInBtn = page.locator('.t-btn-zoom-in');
 | 
			
		||||
        const zoomResetBtn = page.locator('.t-btn-zoom-reset');
 | 
			
		||||
        const initialBoundingBox = await bgImageLocator.boundingBox();
 | 
			
		||||
 | 
			
		||||
        await zoomInBtn.click();
 | 
			
		||||
        // wait for zoom animation to finish
 | 
			
		||||
        await bgImageLocator.hover();
 | 
			
		||||
        await zoomInBtn.click();
 | 
			
		||||
        // wait for zoom animation to finish
 | 
			
		||||
        await bgImageLocator.hover();
 | 
			
		||||
 | 
			
		||||
        const zoomedInBoundingBox = await bgImageLocator.boundingBox();
 | 
			
		||||
        expect.soft(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
 | 
			
		||||
        expect.soft(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);
 | 
			
		||||
 | 
			
		||||
        await zoomResetBtn.click();
 | 
			
		||||
        // wait for zoom animation to finish
 | 
			
		||||
        await bgImageLocator.hover();
 | 
			
		||||
 | 
			
		||||
        const resetBoundingBox = await bgImageLocator.boundingBox();
 | 
			
		||||
        expect.soft(resetBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height);
 | 
			
		||||
        expect.soft(resetBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width);
 | 
			
		||||
 | 
			
		||||
        expect.soft(resetBoundingBox.height).toEqual(initialBoundingBox.height);
 | 
			
		||||
        expect(resetBoundingBox.width).toEqual(initialBoundingBox.width);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('Using the zoom features does not pause telemetry', async ({ page }) => {
 | 
			
		||||
        const bgImageLocator = page.locator(backgroundImageSelector);
 | 
			
		||||
        const pausePlayButton = page.locator('.c-button.pause-play');
 | 
			
		||||
        // wait for zoom animation to finish
 | 
			
		||||
        await bgImageLocator.hover();
 | 
			
		||||
 | 
			
		||||
        // open the time conductor drop down
 | 
			
		||||
        await page.locator('.c-conductor__controls button.c-mode-button').click();
 | 
			
		||||
        // Click local clock
 | 
			
		||||
        await page.locator('.icon-clock >> text=Local Clock').click();
 | 
			
		||||
 | 
			
		||||
        await expect.soft(pausePlayButton).not.toHaveClass(/is-paused/);
 | 
			
		||||
        const zoomInBtn = page.locator('.t-btn-zoom-in');
 | 
			
		||||
        await zoomInBtn.click();
 | 
			
		||||
        // wait for zoom animation to finish
 | 
			
		||||
        await bgImageLocator.hover();
 | 
			
		||||
 | 
			
		||||
        return expect(pausePlayButton).not.toHaveClass(/is-paused/);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// The following test case will cover these scenarios
 | 
			
		||||
// ('Can use Mouse Wheel to zoom in and out of previous image');
 | 
			
		||||
// ('Can use alt+drag to move around image once zoomed in');
 | 
			
		||||
// ('Clicking on the left arrow should pause the imagery and go to previous image');
 | 
			
		||||
// ('If the imagery view is in pause mode, it should not be updated when new images come in');
 | 
			
		||||
// ('If the imagery view is not in pause mode, it should be updated when new images come in');
 | 
			
		||||
const backgroundImageSelector = '.c-imagery__main-image__background-image';
 | 
			
		||||
test('Example Imagery in Display layout', async ({ page }) => {
 | 
			
		||||
    test.info().annotations.push({
 | 
			
		||||
        type: 'issue',
 | 
			
		||||
        description: 'https://github.com/nasa/openmct/issues/5265'
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Go to baseURL
 | 
			
		||||
    await page.goto('/', { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
    // Click the Create button
 | 
			
		||||
    await page.click('button:has-text("Create")');
 | 
			
		||||
 | 
			
		||||
    // Click text=Example Imagery
 | 
			
		||||
    await page.click('text=Example Imagery');
 | 
			
		||||
 | 
			
		||||
    // Clear and set Image load delay to minimum value
 | 
			
		||||
    // FIXME: Update the value to 5000 ms when this bug is fixed.
 | 
			
		||||
    // See: https://github.com/nasa/openmct/issues/5265
 | 
			
		||||
    await page.locator('input[type="number"]').fill('');
 | 
			
		||||
    await page.locator('input[type="number"]').fill('0');
 | 
			
		||||
 | 
			
		||||
    // Click text=OK
 | 
			
		||||
    await Promise.all([
 | 
			
		||||
        page.waitForNavigation({waitUntil: 'networkidle'}),
 | 
			
		||||
        page.click('text=OK'),
 | 
			
		||||
        //Wait for Save Banner to appear
 | 
			
		||||
        page.waitForSelector('.c-message-banner__message')
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    // Wait until Save Banner is gone
 | 
			
		||||
    await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
 | 
			
		||||
    await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');
 | 
			
		||||
    const bgImageLocator = page.locator(backgroundImageSelector);
 | 
			
		||||
    await bgImageLocator.hover();
 | 
			
		||||
 | 
			
		||||
    // Click previous image button
 | 
			
		||||
    const previousImageButton = page.locator('.c-nav--prev');
 | 
			
		||||
    await previousImageButton.click();
 | 
			
		||||
 | 
			
		||||
    // Verify previous image
 | 
			
		||||
    const selectedImage = page.locator('.selected');
 | 
			
		||||
    await expect(selectedImage).toBeVisible();
 | 
			
		||||
 | 
			
		||||
    // Zoom in
 | 
			
		||||
    const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox();
 | 
			
		||||
    await bgImageLocator.hover();
 | 
			
		||||
    const deltaYStep = 100; // equivalent to 1x zoom
 | 
			
		||||
    await page.mouse.wheel(0, deltaYStep * 2);
 | 
			
		||||
    const zoomedBoundingBox = await bgImageLocator.boundingBox();
 | 
			
		||||
    const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;
 | 
			
		||||
    const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
 | 
			
		||||
 | 
			
		||||
    // Wait for zoom animation to finish
 | 
			
		||||
    await bgImageLocator.hover();
 | 
			
		||||
    const imageMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox();
 | 
			
		||||
    expect(imageMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height);
 | 
			
		||||
    expect(imageMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width);
 | 
			
		||||
 | 
			
		||||
    // Center the mouse pointer
 | 
			
		||||
    await page.mouse.move(imageCenterX, imageCenterY);
 | 
			
		||||
 | 
			
		||||
    // Pan Imagery Hints
 | 
			
		||||
    const expectedAltText = process.platform === 'linux' ? 'Ctrl+Alt drag to pan' : 'Alt drag to pan';
 | 
			
		||||
    const imageryHintsText = await page.locator('.c-imagery__hints').innerText();
 | 
			
		||||
    expect(expectedAltText).toEqual(imageryHintsText);
 | 
			
		||||
 | 
			
		||||
    // Click next image button
 | 
			
		||||
    const nextImageButton = page.locator('.c-nav--next');
 | 
			
		||||
    await nextImageButton.click();
 | 
			
		||||
 | 
			
		||||
    // Click time conductor mode button
 | 
			
		||||
    await page.locator('.c-mode-button').click();
 | 
			
		||||
 | 
			
		||||
    // Select local clock mode
 | 
			
		||||
    await page.locator('[data-testid=conductor-modeOption-realtime]').click();
 | 
			
		||||
 | 
			
		||||
    // Zoom in on next image
 | 
			
		||||
    await bgImageLocator.hover();
 | 
			
		||||
    await page.mouse.wheel(0, deltaYStep * 2);
 | 
			
		||||
 | 
			
		||||
    // Wait for zoom animation to finish
 | 
			
		||||
    await bgImageLocator.hover();
 | 
			
		||||
    const imageNextMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox();
 | 
			
		||||
    expect(imageNextMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height);
 | 
			
		||||
    expect(imageNextMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width);
 | 
			
		||||
 | 
			
		||||
    // Click previous image button
 | 
			
		||||
    await previousImageButton.click();
 | 
			
		||||
 | 
			
		||||
    // Verify previous image
 | 
			
		||||
    await expect(selectedImage).toBeVisible();
 | 
			
		||||
 | 
			
		||||
    const imageCount = await page.locator('.c-imagery__thumb').count();
 | 
			
		||||
    await expect.poll(async () => {
 | 
			
		||||
        const newImageCount = await page.locator('.c-imagery__thumb').count();
 | 
			
		||||
 | 
			
		||||
        return newImageCount;
 | 
			
		||||
    }, {
 | 
			
		||||
        message: "verify that new images still stream in",
 | 
			
		||||
        timeout: 6 * 1000
 | 
			
		||||
    }).toBeGreaterThan(imageCount);
 | 
			
		||||
 | 
			
		||||
    // Verify selected image is still displayed
 | 
			
		||||
    await expect(selectedImage).toBeVisible();
 | 
			
		||||
 | 
			
		||||
    // Unpause imagery
 | 
			
		||||
    await page.locator('.pause-play').click();
 | 
			
		||||
 | 
			
		||||
    //Get background-image url from background-image css prop
 | 
			
		||||
    const backgroundImage = page.locator('.c-imagery__main-image__background-image');
 | 
			
		||||
    let backgroundImageUrl = await backgroundImage.evaluate((el) => {
 | 
			
		||||
        return window.getComputedStyle(el).getPropertyValue('background-image').match(/url\(([^)]+)\)/)[1];
 | 
			
		||||
    });
 | 
			
		||||
    let backgroundImageUrl1 = backgroundImageUrl.slice(1, -1); //forgive me, padre
 | 
			
		||||
    console.log('backgroundImageUrl1 ' + backgroundImageUrl1);
 | 
			
		||||
 | 
			
		||||
    let backgroundImageUrl2;
 | 
			
		||||
    await expect.poll(async () => {
 | 
			
		||||
        // Verify next image has updated
 | 
			
		||||
        let backgroundImageUrlNext = await backgroundImage.evaluate((el) => {
 | 
			
		||||
            return window.getComputedStyle(el).getPropertyValue('background-image').match(/url\(([^)]+)\)/)[1];
 | 
			
		||||
        });
 | 
			
		||||
        backgroundImageUrl2 = backgroundImageUrlNext.slice(1, -1); //forgive me, padre
 | 
			
		||||
 | 
			
		||||
        return backgroundImageUrl2;
 | 
			
		||||
    }, {
 | 
			
		||||
        message: "verify next image has updated",
 | 
			
		||||
        timeout: 6 * 1000
 | 
			
		||||
    }).not.toBe(backgroundImageUrl1);
 | 
			
		||||
    console.log('backgroundImageUrl2 ' + backgroundImageUrl2);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test.describe('Example imagery thumbnails resize in display layouts', () => {
 | 
			
		||||
 | 
			
		||||
    test('Resizing the layout changes thumbnail visibility and size', async ({ page }) => {
 | 
			
		||||
        await page.goto('/', { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
        const thumbsWrapperLocator = page.locator('.c-imagery__thumbs-wrapper');
 | 
			
		||||
        // Click button:has-text("Create")
 | 
			
		||||
        await page.locator('button:has-text("Create")').click();
 | 
			
		||||
 | 
			
		||||
        // Click li:has-text("Display Layout")
 | 
			
		||||
        await page.locator('li:has-text("Display Layout")').click();
 | 
			
		||||
        const displayLayoutTitleField = page.locator('text=Properties Title Notes Horizontal grid (px) Vertical grid (px) Horizontal size ( >> input[type="text"]');
 | 
			
		||||
        await displayLayoutTitleField.click();
 | 
			
		||||
 | 
			
		||||
        await displayLayoutTitleField.fill('Thumbnail Display Layout');
 | 
			
		||||
 | 
			
		||||
        // Click text=OK
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            page.waitForNavigation(),
 | 
			
		||||
            page.locator('text=OK').click()
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        // Click text=Snapshot Save and Finish Editing Save and Continue Editing >> button >> nth=1
 | 
			
		||||
        await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
 | 
			
		||||
 | 
			
		||||
        // Click text=Save and Finish Editing
 | 
			
		||||
        await page.locator('text=Save and Finish Editing').click();
 | 
			
		||||
 | 
			
		||||
        // Click button:has-text("Create")
 | 
			
		||||
        await page.locator('button:has-text("Create")').click();
 | 
			
		||||
 | 
			
		||||
        // Click li:has-text("Example Imagery")
 | 
			
		||||
        await page.locator('li:has-text("Example Imagery")').click();
 | 
			
		||||
 | 
			
		||||
        const imageryTitleField = page.locator('text=Properties Title Notes Images url list (comma separated) Image load delay (milli >> input[type="text"]');
 | 
			
		||||
        // Click text=Properties Title Notes Images url list (comma separated) Image load delay (milli >> input[type="text"]
 | 
			
		||||
        await imageryTitleField.click();
 | 
			
		||||
 | 
			
		||||
        // Fill text=Properties Title Notes Images url list (comma separated) Image load delay (milli >> input[type="text"]
 | 
			
		||||
        await imageryTitleField.fill('Thumbnail Example Imagery');
 | 
			
		||||
 | 
			
		||||
        // Click text=OK
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            page.waitForNavigation(),
 | 
			
		||||
            page.locator('text=OK').click()
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        // Click text=Thumbnail Example Imagery Imagery Layout Snapshot >> button >> nth=0
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            page.waitForNavigation(),
 | 
			
		||||
            page.locator('text=Thumbnail Example Imagery Imagery Layout Snapshot >> button').first().click()
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        // Edit mode
 | 
			
		||||
        await page.locator('text=Thumbnail Display Layout Snapshot >> button').nth(3).click();
 | 
			
		||||
 | 
			
		||||
        // Click on example imagery to expose toolbar
 | 
			
		||||
        await page.locator('text=Thumbnail Example Imagery Snapshot Large View').click();
 | 
			
		||||
 | 
			
		||||
        // expect thumbnails not be visible when first added
 | 
			
		||||
        expect.soft(thumbsWrapperLocator.isHidden()).toBeTruthy();
 | 
			
		||||
 | 
			
		||||
        // Resize the example imagery vertically to change the thumbnail visibility
 | 
			
		||||
        /*
 | 
			
		||||
        The following arbitrary values are added to observe the separate visual
 | 
			
		||||
        conditions of the thumbnails (hidden, small thumbnails, regular thumbnails).
 | 
			
		||||
        Specifically, height is set to 50px for small thumbs and 100px for regular
 | 
			
		||||
        */
 | 
			
		||||
        // Click #mct-input-id-103
 | 
			
		||||
        await page.locator('#mct-input-id-103').click();
 | 
			
		||||
 | 
			
		||||
        // Fill #mct-input-id-103
 | 
			
		||||
        await page.locator('#mct-input-id-103').fill('50');
 | 
			
		||||
 | 
			
		||||
        expect(thumbsWrapperLocator.isVisible()).toBeTruthy();
 | 
			
		||||
        await expect(thumbsWrapperLocator).toHaveClass(/is-small-thumbs/);
 | 
			
		||||
 | 
			
		||||
        // Resize the example imagery vertically to change the thumbnail visibility
 | 
			
		||||
        // Click #mct-input-id-103
 | 
			
		||||
        await page.locator('#mct-input-id-103').click();
 | 
			
		||||
 | 
			
		||||
        // Fill #mct-input-id-103
 | 
			
		||||
        await page.locator('#mct-input-id-103').fill('100');
 | 
			
		||||
 | 
			
		||||
        expect(thumbsWrapperLocator.isVisible()).toBeTruthy();
 | 
			
		||||
        await expect(thumbsWrapperLocator).not.toHaveClass(/is-small-thumbs/);
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test.describe('Example Imagery in Flexible layout', () => {
 | 
			
		||||
    test.fixme('Can use Mouse Wheel to zoom in and out of previous image');
 | 
			
		||||
    test.fixme('Can use alt+drag to move around image once zoomed in');
 | 
			
		||||
    test.fixme('Can zoom into the latest image and the real-time/fixed-time imagery will pause');
 | 
			
		||||
    test.fixme('Clicking on the left arrow should pause the imagery and go to previous image');
 | 
			
		||||
    test.fixme('If the imagery view is in pause mode, it should not be updated when new images come in');
 | 
			
		||||
    test.fixme('If the imagery view is not in pause mode, it should be updated when new images come in');
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test.describe('Example Imagery in Tabs view', () => {
 | 
			
		||||
    test.fixme('Can use Mouse Wheel to zoom in and out of previous image');
 | 
			
		||||
    test.fixme('Can use alt+drag to move around image once zoomed in');
 | 
			
		||||
    test.fixme('Can zoom into the latest image and the real-time/fixed-time imagery will pause');
 | 
			
		||||
    test.fixme('Can zoom into a previous image from thumbstrip in real-time or fixed-time');
 | 
			
		||||
    test.fixme('Clicking on the left arrow should pause the imagery and go to previous image');
 | 
			
		||||
    test.fixme('If the imagery view is in pause mode, it should not be updated when new images come in');
 | 
			
		||||
    test.fixme('If the imagery view is not in pause mode, it should be updated when new images come in');
 | 
			
		||||
});
 | 
			
		||||
@@ -1,198 +0,0 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2022, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
Testsuite for plot autoscale.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
const { test: _test } = require('../../../fixtures.js');
 | 
			
		||||
const { expect } = require('@playwright/test');
 | 
			
		||||
 | 
			
		||||
// create a new `test` API that will not append platform details to snapshot
 | 
			
		||||
// file names, only for the tests in this file, so that the same snapshots will
 | 
			
		||||
// be used for all platforms.
 | 
			
		||||
const test = _test.extend({
 | 
			
		||||
    _autoSnapshotSuffix: [
 | 
			
		||||
        async ({}, use, testInfo) => {
 | 
			
		||||
            testInfo.snapshotSuffix = '';
 | 
			
		||||
            await use();
 | 
			
		||||
        },
 | 
			
		||||
        { auto: true }
 | 
			
		||||
    ]
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test.use({
 | 
			
		||||
    viewport: {
 | 
			
		||||
        width: 1280,
 | 
			
		||||
        height: 720
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test.describe('ExportAsJSON', () => {
 | 
			
		||||
    test('User can set autoscale with a valid range @snapshot', async ({ page }) => {
 | 
			
		||||
        //This is necessary due to the size of the test suite.
 | 
			
		||||
        await test.setTimeout(120 * 1000);
 | 
			
		||||
 | 
			
		||||
        await page.goto('/', { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
        await setTimeRange(page);
 | 
			
		||||
 | 
			
		||||
        await createSinewaveOverlayPlot(page);
 | 
			
		||||
 | 
			
		||||
        await testYTicks(page, ['-1.00', '-0.50', '0.00', '0.50', '1.00']);
 | 
			
		||||
 | 
			
		||||
        await turnOffAutoscale(page);
 | 
			
		||||
 | 
			
		||||
        const canvas = page.locator('canvas').nth(1);
 | 
			
		||||
 | 
			
		||||
        // Make sure that after turning off autoscale, the user selected range values start at the same values the plot had prior.
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            testYTicks(page, ['-1.00', '-0.50', '0.00', '0.50', '1.00']),
 | 
			
		||||
            new Promise(r => setTimeout(r, 100))
 | 
			
		||||
                .then(() => canvas.screenshot())
 | 
			
		||||
                .then(shot => expect(shot).toMatchSnapshot('autoscale-canvas-prepan.png', { maxDiffPixels: 40 }))
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        await page.keyboard.down('Alt');
 | 
			
		||||
 | 
			
		||||
        await canvas.dragTo(canvas, {
 | 
			
		||||
            sourcePosition: {
 | 
			
		||||
                x: 200,
 | 
			
		||||
                y: 200
 | 
			
		||||
            },
 | 
			
		||||
            targetPosition: {
 | 
			
		||||
                x: 400,
 | 
			
		||||
                y: 400
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        await page.keyboard.up('Alt');
 | 
			
		||||
 | 
			
		||||
        // Ensure the drag worked.
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            testYTicks(page, ['0.00', '0.50', '1.00', '1.50', '2.00']),
 | 
			
		||||
            new Promise(r => setTimeout(r, 100))
 | 
			
		||||
                .then(() => canvas.screenshot())
 | 
			
		||||
                .then(shot => expect(shot).toMatchSnapshot('autoscale-canvas-panned.png', { maxDiffPixels: 40 }))
 | 
			
		||||
        ]);
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {import('@playwright/test').Page} page
 | 
			
		||||
 * @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.
 | 
			
		||||
 | 
			
		||||
    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);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {import('@playwright/test').Page} page
 | 
			
		||||
 */
 | 
			
		||||
async function createSinewaveOverlayPlot(page) {
 | 
			
		||||
    // click create button
 | 
			
		||||
    await page.locator('button:has-text("Create")').click();
 | 
			
		||||
 | 
			
		||||
    // add overlay plot with defaults
 | 
			
		||||
    await page.locator('li:has-text("Overlay Plot")').click();
 | 
			
		||||
    await Promise.all([
 | 
			
		||||
        page.waitForNavigation(),
 | 
			
		||||
        page.locator('text=OK').click(),
 | 
			
		||||
        //Wait for Save Banner to appear1
 | 
			
		||||
        page.waitForSelector('.c-message-banner__message')
 | 
			
		||||
    ]);
 | 
			
		||||
    //Wait until Save Banner is gone
 | 
			
		||||
    await page.locator('.c-message-banner__close-button').click();
 | 
			
		||||
    await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
 | 
			
		||||
 | 
			
		||||
    // save (exit edit mode)
 | 
			
		||||
    await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
 | 
			
		||||
    await page.locator('text=Save and Finish Editing').click();
 | 
			
		||||
 | 
			
		||||
    // click create button
 | 
			
		||||
    await page.locator('button:has-text("Create")').click();
 | 
			
		||||
 | 
			
		||||
    // add sine wave generator with defaults
 | 
			
		||||
    await page.locator('li:has-text("Sine Wave Generator")').click();
 | 
			
		||||
    await Promise.all([
 | 
			
		||||
        page.waitForNavigation(),
 | 
			
		||||
        page.locator('text=OK').click(),
 | 
			
		||||
        //Wait for Save Banner to appear1
 | 
			
		||||
        page.waitForSelector('.c-message-banner__message')
 | 
			
		||||
    ]);
 | 
			
		||||
    //Wait until Save Banner is gone
 | 
			
		||||
    await page.locator('.c-message-banner__close-button').click();
 | 
			
		||||
    await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
 | 
			
		||||
 | 
			
		||||
    // focus the overlay plot
 | 
			
		||||
    await page.locator('text=Open MCT My Items >> span').nth(3).click();
 | 
			
		||||
    await Promise.all([
 | 
			
		||||
        page.waitForNavigation(),
 | 
			
		||||
        page.locator('text=Unnamed Overlay Plot').first().click()
 | 
			
		||||
    ]);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {import('@playwright/test').Page} page
 | 
			
		||||
 */
 | 
			
		||||
async function turnOffAutoscale(page) {
 | 
			
		||||
    // enter edit mode
 | 
			
		||||
    await page.locator('text=Unnamed Overlay Plot Snapshot >> button').nth(3).click();
 | 
			
		||||
 | 
			
		||||
    // uncheck autoscale
 | 
			
		||||
    await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"] >> nth=1').uncheck();
 | 
			
		||||
 | 
			
		||||
    // save
 | 
			
		||||
    await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
 | 
			
		||||
    await Promise.all([
 | 
			
		||||
        page.locator('text=Save and Finish Editing').click(),
 | 
			
		||||
        //Wait for Save Banner to appear
 | 
			
		||||
        page.waitForSelector('.c-message-banner__message')
 | 
			
		||||
    ]);
 | 
			
		||||
    //Wait until Save Banner is gone
 | 
			
		||||
    await page.locator('.c-message-banner__close-button').click();
 | 
			
		||||
    await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @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))];
 | 
			
		||||
 | 
			
		||||
    for (let i = 0, l = values.length; i < l; i += 1) {
 | 
			
		||||
        promises.push(expect(yTicks.nth(i)).toHaveText(values[i])); // eslint-disable-line
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await Promise.all(promises);
 | 
			
		||||
}
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 18 KiB  | 
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 19 KiB  | 
@@ -1,312 +0,0 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2022, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
Tests to verify log plot functionality. 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 { test } = require('../../../fixtures.js');
 | 
			
		||||
const { expect } = require('@playwright/test');
 | 
			
		||||
 | 
			
		||||
test.describe('Log plot tests', () => {
 | 
			
		||||
    test('Log Plot ticks are functionally correct in regular and log mode and after refresh', async ({ page }) => {
 | 
			
		||||
        //This is necessary due to the size of the test suite.
 | 
			
		||||
        await test.setTimeout(120 * 1000);
 | 
			
		||||
 | 
			
		||||
        await makeOverlayPlot(page);
 | 
			
		||||
        await testRegularTicks(page);
 | 
			
		||||
        await enableEditMode(page);
 | 
			
		||||
        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 testLogPlotPixels(page);
 | 
			
		||||
 | 
			
		||||
        // FIXME: Get rid of the waitForTimeout() and lint warning exception.
 | 
			
		||||
        // eslint-disable-next-line playwright/no-wait-for-timeout
 | 
			
		||||
        await page.waitForTimeout(1 * 1000);
 | 
			
		||||
 | 
			
		||||
        // refresh page and wait for charts and ticks to load
 | 
			
		||||
        await page.reload({ waitUntil: 'networkidle'});
 | 
			
		||||
        await page.waitForSelector('.gl-plot-chart-area');
 | 
			
		||||
        await page.waitForSelector('.gl-plot-y-tick-label');
 | 
			
		||||
 | 
			
		||||
        // test log ticks hold up after refresh
 | 
			
		||||
        await testLogTicks(page);
 | 
			
		||||
        //await testLogPlotPixels(page);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Leaving test as 'TODO' for now.
 | 
			
		||||
    // NOTE: Not eligible for community contributions.
 | 
			
		||||
    test.fixme('Verify that log mode option is reflected in import/export JSON', async ({ page }) => {
 | 
			
		||||
        await makeOverlayPlot(page);
 | 
			
		||||
        await enableEditMode(page);
 | 
			
		||||
        await enableLogMode(page);
 | 
			
		||||
        await saveOverlayPlot(page);
 | 
			
		||||
 | 
			
		||||
        // TODO ...export, delete the overlay, then import it...
 | 
			
		||||
 | 
			
		||||
        //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);
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Makes an overlay plot with a sine wave generator and clicks on the overlay plot in the sidebar so it is the active thing displayed.
 | 
			
		||||
 * @param {import('@playwright/test').Page} page
 | 
			
		||||
 */
 | 
			
		||||
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' });
 | 
			
		||||
 | 
			
		||||
    // 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');
 | 
			
		||||
 | 
			
		||||
    await timeInputs.nth(1).click();
 | 
			
		||||
    await timeInputs.nth(1).fill('2022-03-29 22:00:30.000Z');
 | 
			
		||||
 | 
			
		||||
    // create overlay plot
 | 
			
		||||
 | 
			
		||||
    await page.locator('button.c-create-button').click();
 | 
			
		||||
    await page.locator('li:has-text("Overlay Plot")').click();
 | 
			
		||||
    await Promise.all([
 | 
			
		||||
        page.waitForNavigation({ waitUntil: 'networkidle'}),
 | 
			
		||||
        page.locator('text=OK').click(),
 | 
			
		||||
        //Wait for Save Banner to appear
 | 
			
		||||
        page.waitForSelector('.c-message-banner__message')
 | 
			
		||||
    ]);
 | 
			
		||||
    //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:has-text("Sine Wave Generator")').click();
 | 
			
		||||
 | 
			
		||||
    // set amplitude to 6, offset 4, period 2
 | 
			
		||||
 | 
			
		||||
    await page.locator('div:nth-child(5) .form-row .c-form-row__controls .form-control .field input').click();
 | 
			
		||||
    await page.locator('div:nth-child(5) .form-row .c-form-row__controls .form-control .field input').fill('6');
 | 
			
		||||
 | 
			
		||||
    await page.locator('div:nth-child(6) .form-row .c-form-row__controls .form-control .field input').click();
 | 
			
		||||
    await page.locator('div:nth-child(6) .form-row .c-form-row__controls .form-control .field input').fill('4');
 | 
			
		||||
 | 
			
		||||
    await page.locator('div:nth-child(7) .form-row .c-form-row__controls .form-control .field input').click();
 | 
			
		||||
    await page.locator('div:nth-child(7) .form-row .c-form-row__controls .form-control .field input').fill('2');
 | 
			
		||||
 | 
			
		||||
    // Click OK to make generator
 | 
			
		||||
 | 
			
		||||
    await Promise.all([
 | 
			
		||||
        page.waitForNavigation({ waitUntil: 'networkidle'}),
 | 
			
		||||
        page.locator('text=OK').click(),
 | 
			
		||||
        //Wait for Save Banner to appear
 | 
			
		||||
        page.waitForSelector('.c-message-banner__message')
 | 
			
		||||
    ]);
 | 
			
		||||
    //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()
 | 
			
		||||
    ]);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {import('@playwright/test').Page} page
 | 
			
		||||
 */
 | 
			
		||||
async function testRegularTicks(page) {
 | 
			
		||||
    const yTicks = await 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 = await page.locator('.gl-plot-y-tick-label');
 | 
			
		||||
    expect(await yTicks.count()).toBe(28);
 | 
			
		||||
    await expect(yTicks.nth(0)).toHaveText('-2.98');
 | 
			
		||||
    await expect(yTicks.nth(1)).toHaveText('-2.50');
 | 
			
		||||
    await expect(yTicks.nth(2)).toHaveText('-2.00');
 | 
			
		||||
    await expect(yTicks.nth(3)).toHaveText('-1.51');
 | 
			
		||||
    await expect(yTicks.nth(4)).toHaveText('-1.20');
 | 
			
		||||
    await expect(yTicks.nth(5)).toHaveText('-1.00');
 | 
			
		||||
    await expect(yTicks.nth(6)).toHaveText('-0.80');
 | 
			
		||||
    await expect(yTicks.nth(7)).toHaveText('-0.58');
 | 
			
		||||
    await expect(yTicks.nth(8)).toHaveText('-0.40');
 | 
			
		||||
    await expect(yTicks.nth(9)).toHaveText('-0.20');
 | 
			
		||||
    await expect(yTicks.nth(10)).toHaveText('-0.00');
 | 
			
		||||
    await expect(yTicks.nth(11)).toHaveText('0.20');
 | 
			
		||||
    await expect(yTicks.nth(12)).toHaveText('0.40');
 | 
			
		||||
    await expect(yTicks.nth(13)).toHaveText('0.58');
 | 
			
		||||
    await expect(yTicks.nth(14)).toHaveText('0.80');
 | 
			
		||||
    await expect(yTicks.nth(15)).toHaveText('1.00');
 | 
			
		||||
    await expect(yTicks.nth(16)).toHaveText('1.20');
 | 
			
		||||
    await expect(yTicks.nth(17)).toHaveText('1.51');
 | 
			
		||||
    await expect(yTicks.nth(18)).toHaveText('2.00');
 | 
			
		||||
    await expect(yTicks.nth(19)).toHaveText('2.50');
 | 
			
		||||
    await expect(yTicks.nth(20)).toHaveText('2.98');
 | 
			
		||||
    await expect(yTicks.nth(21)).toHaveText('3.50');
 | 
			
		||||
    await expect(yTicks.nth(22)).toHaveText('4.00');
 | 
			
		||||
    await expect(yTicks.nth(23)).toHaveText('4.50');
 | 
			
		||||
    await expect(yTicks.nth(24)).toHaveText('5.31');
 | 
			
		||||
    await expect(yTicks.nth(25)).toHaveText('7.00');
 | 
			
		||||
    await expect(yTicks.nth(26)).toHaveText('8.00');
 | 
			
		||||
    await expect(yTicks.nth(27)).toHaveText('9.00');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {import('@playwright/test').Page} page
 | 
			
		||||
 */
 | 
			
		||||
async function enableEditMode(page) {
 | 
			
		||||
    // turn on edit mode
 | 
			
		||||
    await page.locator('text=Unnamed Overlay Plot Snapshot >> button').nth(3).click();
 | 
			
		||||
    await expect(await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1)).toBeVisible();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {import('@playwright/test').Page} page
 | 
			
		||||
 */
 | 
			
		||||
async function enableLogMode(page) {
 | 
			
		||||
    // turn on log mode
 | 
			
		||||
    await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"]').first().check();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {import('@playwright/test').Page} page
 | 
			
		||||
 */
 | 
			
		||||
async function disableLogMode(page) {
 | 
			
		||||
    // turn off log mode
 | 
			
		||||
    await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"]').first().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();
 | 
			
		||||
 | 
			
		||||
    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' });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {import('@playwright/test').Page} page
 | 
			
		||||
 */
 | 
			
		||||
// FIXME: Remove this eslint exception once implemented
 | 
			
		||||
// eslint-disable-next-line no-unused-vars
 | 
			
		||||
async function testLogPlotPixels(page) {
 | 
			
		||||
    const pixelsMatch = await page.evaluate(async () => {
 | 
			
		||||
        // TODO get canvas pixels at a few locations to make sure they're the correct color, to test that the plot comes out as expected.
 | 
			
		||||
 | 
			
		||||
        await new Promise((r) => setTimeout(r, 5 * 1000));
 | 
			
		||||
 | 
			
		||||
        // 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],
 | 
			
		||||
            [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');
 | 
			
		||||
 | 
			
		||||
        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;
 | 
			
		||||
 | 
			
		||||
            // #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;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    expect(pixelsMatch).toBe(true);
 | 
			
		||||
}
 | 
			
		||||
@@ -20,12 +20,11 @@
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
const { test } = require('../../../fixtures.js');
 | 
			
		||||
const { expect } = require('@playwright/test');
 | 
			
		||||
const { test, expect } = require('@playwright/test');
 | 
			
		||||
 | 
			
		||||
test.describe('Time conductor operations', () => {
 | 
			
		||||
test.describe('Time counductor operations', () => {
 | 
			
		||||
    test('validate start time does not exceeds end time', async ({ page }) => {
 | 
			
		||||
        // Go to baseURL
 | 
			
		||||
        //Go to baseURL
 | 
			
		||||
        await page.goto('/', { waitUntil: 'networkidle' });
 | 
			
		||||
        const year = new Date().getFullYear();
 | 
			
		||||
 | 
			
		||||
@@ -68,168 +67,3 @@ test.describe('Time conductor operations', () => {
 | 
			
		||||
        expect(endDateValidityStatus).not.toBeTruthy();
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Testing instructions:
 | 
			
		||||
// Try to change the realtime offsets when in realtime (local clock) mode.
 | 
			
		||||
test.describe('Time conductor input fields real-time mode', () => {
 | 
			
		||||
    test('validate input fields in real-time mode', async ({ page }) => {
 | 
			
		||||
        const startOffset = {
 | 
			
		||||
            secs: '23'
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const endOffset = {
 | 
			
		||||
            secs: '31'
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Go to baseURL
 | 
			
		||||
        await page.goto('/', { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
        // Switch to real-time mode
 | 
			
		||||
        await setRealTimeMode(page);
 | 
			
		||||
 | 
			
		||||
        // Set start time offset
 | 
			
		||||
        await setStartOffset(page, startOffset);
 | 
			
		||||
 | 
			
		||||
        // Verify time was updated on time offset button
 | 
			
		||||
        await expect(page.locator('data-testid=conductor-start-offset-button')).toContainText('00:30:23');
 | 
			
		||||
 | 
			
		||||
        // Set end time offset
 | 
			
		||||
        await setEndOffset(page, endOffset);
 | 
			
		||||
 | 
			
		||||
        // Verify time was updated on preceding time offset button
 | 
			
		||||
        await expect(page.locator('data-testid=conductor-end-offset-button')).toContainText('00:00:31');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Verify that offsets and url params are preserved when switching
 | 
			
		||||
     * between fixed timespan and real-time mode.
 | 
			
		||||
     */
 | 
			
		||||
    test('preserve offsets and url params when switching between fixed and real-time mode', async ({ page }) => {
 | 
			
		||||
        const startOffset = {
 | 
			
		||||
            mins: '30',
 | 
			
		||||
            secs: '23'
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const endOffset = {
 | 
			
		||||
            secs: '01'
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Convert offsets to milliseconds
 | 
			
		||||
        const startDelta = (30 * 60 * 1000) + (23 * 1000);
 | 
			
		||||
        const endDelta = (1 * 1000);
 | 
			
		||||
 | 
			
		||||
        // Go to baseURL
 | 
			
		||||
        await page.goto('/', { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
        // Switch to real-time mode
 | 
			
		||||
        await setRealTimeMode(page);
 | 
			
		||||
 | 
			
		||||
        // Set start time offset
 | 
			
		||||
        await setStartOffset(page, startOffset);
 | 
			
		||||
 | 
			
		||||
        // Set end time offset
 | 
			
		||||
        await setEndOffset(page, endOffset);
 | 
			
		||||
 | 
			
		||||
        // Switch to fixed timespan mode
 | 
			
		||||
        await setFixedTimeMode(page);
 | 
			
		||||
 | 
			
		||||
        // Switch back to real-time mode
 | 
			
		||||
        await setRealTimeMode(page);
 | 
			
		||||
 | 
			
		||||
        // Verify updated start time offset persists after mode switch
 | 
			
		||||
        await expect(page.locator('data-testid=conductor-start-offset-button')).toContainText('00:30:23');
 | 
			
		||||
 | 
			
		||||
        // Verify updated end time offset persists after mode switch
 | 
			
		||||
        await expect(page.locator('data-testid=conductor-end-offset-button')).toContainText('00:00:01');
 | 
			
		||||
 | 
			
		||||
        // Verify url parameters persist after mode switch
 | 
			
		||||
        await page.waitForNavigation();
 | 
			
		||||
        expect(page.url()).toContain(`startDelta=${startDelta}`);
 | 
			
		||||
        expect(page.url()).toContain(`endDelta=${endDelta}`);
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @typedef {Object} OffsetValues
 | 
			
		||||
 * @property {string | undefined} hours
 | 
			
		||||
 * @property {string | undefined} mins
 | 
			
		||||
 * @property {string | undefined} secs
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Set the values (hours, mins, secs) for the start time offset when in realtime mode
 | 
			
		||||
 * @param {import('@playwright/test').Page} page
 | 
			
		||||
 * @param {OffsetValues} offset
 | 
			
		||||
 */
 | 
			
		||||
async function setStartOffset(page, offset) {
 | 
			
		||||
    const startOffsetButton = page.locator('data-testid=conductor-start-offset-button');
 | 
			
		||||
    await setTimeConductorOffset(page, offset, startOffsetButton);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Set the values (hours, mins, secs) for the end time offset when in realtime mode
 | 
			
		||||
 * @param {import('@playwright/test').Page} page
 | 
			
		||||
 * @param {OffsetValues} offset
 | 
			
		||||
 */
 | 
			
		||||
async function setEndOffset(page, offset) {
 | 
			
		||||
    const endOffsetButton = page.locator('data-testid=conductor-end-offset-button');
 | 
			
		||||
    await setTimeConductorOffset(page, offset, endOffsetButton);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Set the time conductor to fixed timespan mode
 | 
			
		||||
 * @param {import('@playwright/test').Page} page
 | 
			
		||||
 */
 | 
			
		||||
async function setFixedTimeMode(page) {
 | 
			
		||||
    await setTimeConductorMode(page, true);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Set the time conductor to realtime mode
 | 
			
		||||
 * @param {import('@playwright/test').Page} page
 | 
			
		||||
 */
 | 
			
		||||
async function setRealTimeMode(page) {
 | 
			
		||||
    await setTimeConductorMode(page, false);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Set the values (hours, mins, secs) for the TimeConductor offsets when in realtime mode
 | 
			
		||||
 * @param {import('@playwright/test').Page} page
 | 
			
		||||
 * @param {OffsetValues} offset
 | 
			
		||||
 * @param {import('@playwright/test').Locator} offsetButton
 | 
			
		||||
 */
 | 
			
		||||
async function setTimeConductorOffset(page, {hours, mins, secs}, offsetButton) {
 | 
			
		||||
    await offsetButton.click();
 | 
			
		||||
 | 
			
		||||
    if (hours) {
 | 
			
		||||
        await page.fill('.pr-time-controls__hrs', hours);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (mins) {
 | 
			
		||||
        await page.fill('.pr-time-controls__mins', mins);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (secs) {
 | 
			
		||||
        await page.fill('.pr-time-controls__secs', secs);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Click the check button
 | 
			
		||||
    await page.locator('.icon-check').click();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Set the time conductor mode to either fixed timespan or realtime mode.
 | 
			
		||||
 * @param {import('@playwright/test').Page} page
 | 
			
		||||
 * @param {boolean} [isFixedTimespan=true] true for fixed timespan mode, false for realtime mode; default is true
 | 
			
		||||
 */
 | 
			
		||||
async function setTimeConductorMode(page, isFixedTimespan = true) {
 | 
			
		||||
    // Click 'mode' button
 | 
			
		||||
    await page.locator('.c-mode-button').click();
 | 
			
		||||
 | 
			
		||||
    // Switch time conductor mode
 | 
			
		||||
    if (isFixedTimespan) {
 | 
			
		||||
        await page.locator('data-testid=conductor-modeOption-fixed').click();
 | 
			
		||||
    } else {
 | 
			
		||||
        await page.locator('data-testid=conductor-modeOption-realtime').click();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,22 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "cookies": [],
 | 
			
		||||
  "origins": [
 | 
			
		||||
    {
 | 
			
		||||
      "origin": "http://localhost:8080",
 | 
			
		||||
      "localStorage": [
 | 
			
		||||
        {
 | 
			
		||||
          "name": "tcHistory",
 | 
			
		||||
          "value": "{\"utc\":[{\"start\":1651513945533,\"end\":1651515745533}]}"
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "name": "mct",
 | 
			
		||||
          "value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"c0f99e39-85e7-4ef7-99b1-ef52d4ed69b2\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1651515746374,\"modified\":1651515746374},\"c0f99e39-85e7-4ef7-99b1-ef52d4ed69b2\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"c0f99e39-85e7-4ef7-99b1-ef52d4ed69b2\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"e35a066b-eb0e-4b05-a4c9-cc31dc202572\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1651515746373,\"location\":\"mine\",\"persisted\":1651515746373}}"
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "name": "mct-tree-expanded",
 | 
			
		||||
          "value": "[]"
 | 
			
		||||
        }
 | 
			
		||||
      ]
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
@@ -33,8 +33,7 @@ comfortable running this test during a live mission?" Avoid creating or deleting
 | 
			
		||||
Make no assumptions about the order that elements appear in the DOM.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
const { test } = require('../fixtures.js');
 | 
			
		||||
const { expect } = require('@playwright/test');
 | 
			
		||||
const { test, expect } = require('@playwright/test');
 | 
			
		||||
 | 
			
		||||
test('Verify that the create button appears and that the Folder Domain Object is available for selection', async ({ page }) => {
 | 
			
		||||
 | 
			
		||||
@@ -22,14 +22,14 @@
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
Collection of Visual Tests set to run in a default context. The tests within this suite
 | 
			
		||||
are only meant to run against openmct's app.js started by `npm run start` within the
 | 
			
		||||
are only meant to run against openmct's app.js started by `npm run start` within the 
 | 
			
		||||
`./e2e/playwright-visual.config.js` file.
 | 
			
		||||
 | 
			
		||||
These should only use functional expect statements to verify assumptions about the state
 | 
			
		||||
These should only use functional expect statements to verify assumptions about the state 
 | 
			
		||||
in a test and not for functional verification of correctness. Visual tests are not supposed
 | 
			
		||||
to "fail" on assertions. Instead, they should be used to detect changes between builds or branches.
 | 
			
		||||
 | 
			
		||||
Note: Larger testsuite sizes are OK due to the setup time associated with these tests.
 | 
			
		||||
Note: Larger testsuite sizes are OK due to the setup time associated with these tests. 
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
const { test, expect } = require('@playwright/test');
 | 
			
		||||
@@ -47,10 +47,7 @@ test.beforeEach(async ({ context }) => {
 | 
			
		||||
        path: path.join(__dirname, '../../..', './node_modules/sinon/pkg/sinon.js')
 | 
			
		||||
    });
 | 
			
		||||
    await context.addInitScript(() => {
 | 
			
		||||
        window.__clock = sinon.useFakeTimers({
 | 
			
		||||
            now: 0,
 | 
			
		||||
            shouldAdvanceTime: true
 | 
			
		||||
        }); //Set browser clock to UNIX Epoch
 | 
			
		||||
        window.__clock = sinon.useFakeTimers(); //Set browser clock to UNIX Epoch
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@@ -59,7 +56,8 @@ test('Visual - Root and About', async ({ page }) => {
 | 
			
		||||
    await page.goto('/', { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
    // Verify that Create button is actionable
 | 
			
		||||
    await expect(page.locator('button:has-text("Create")')).toBeEnabled();
 | 
			
		||||
    const createButtonLocator = page.locator('button:has-text("Create")');
 | 
			
		||||
    await expect(createButtonLocator).toBeEnabled();
 | 
			
		||||
 | 
			
		||||
    // Take a snapshot of the Dashboard
 | 
			
		||||
    await page.waitForTimeout(VISUAL_GRACE_PERIOD);
 | 
			
		||||
@@ -173,24 +171,3 @@ test('Visual - Sine Wave Generator Form', async ({ page }) => {
 | 
			
		||||
    await page.waitForTimeout(VISUAL_GRACE_PERIOD);
 | 
			
		||||
    await percySnapshot(page, 'removed amplitude property value');
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test('Visual - Save Successful Banner', async ({ page }) => {
 | 
			
		||||
    //Go to baseURL
 | 
			
		||||
    await page.goto('/', { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
    //Click the Create button
 | 
			
		||||
    await page.click('button:has-text("Create")');
 | 
			
		||||
 | 
			
		||||
    //NOTE Something other than example imagery
 | 
			
		||||
    await page.click('text=Timer');
 | 
			
		||||
 | 
			
		||||
    // Click text=OK
 | 
			
		||||
    await page.click('text=OK');
 | 
			
		||||
    await page.locator('.c-message-banner__message').hover({ trial: true });
 | 
			
		||||
    await percySnapshot(page, 'Banner message shown');
 | 
			
		||||
 | 
			
		||||
    //Wait until Save Banner is gone
 | 
			
		||||
    await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
 | 
			
		||||
    await percySnapshot(page, 'Banner message gone');
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
@@ -33,7 +33,7 @@ class EventTelemetryProvider {
 | 
			
		||||
 | 
			
		||||
    generateData(firstObservedTime, count, startTime, duration, name) {
 | 
			
		||||
        const millisecondsSinceStart = startTime - firstObservedTime;
 | 
			
		||||
        const utc = startTime + (count * duration);
 | 
			
		||||
        const utc = Math.floor(startTime / duration) * duration;
 | 
			
		||||
        const ind = count % messages.length;
 | 
			
		||||
        const message = messages[ind] + " - [" + millisecondsSinceStart + "]";
 | 
			
		||||
 | 
			
		||||
@@ -75,7 +75,7 @@ class EventTelemetryProvider {
 | 
			
		||||
        const duration = domainObject.telemetry.duration * 1000;
 | 
			
		||||
        const size = options.size ? options.size : this.defaultSize;
 | 
			
		||||
        const data = [];
 | 
			
		||||
        const firstObservedTime = options.start;
 | 
			
		||||
        const firstObservedTime = Date.now();
 | 
			
		||||
        let count = 0;
 | 
			
		||||
 | 
			
		||||
        if (options.strategy === 'latest' || options.size === 1) {
 | 
			
		||||
@@ -83,7 +83,7 @@ class EventTelemetryProvider {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        while (start <= end && data.length < size) {
 | 
			
		||||
            const startTime = options.start + count;
 | 
			
		||||
            const startTime = Date.now() + count;
 | 
			
		||||
            data.push(this.generateData(firstObservedTime, count, startTime, duration, domainObject.name));
 | 
			
		||||
            start += duration;
 | 
			
		||||
            count += 1;
 | 
			
		||||
 
 | 
			
		||||
@@ -35,7 +35,6 @@ describe('the plugin', () => {
 | 
			
		||||
        telemetry: {
 | 
			
		||||
            duration: 0
 | 
			
		||||
        },
 | 
			
		||||
        options: {},
 | 
			
		||||
        type: 'eventGenerator'
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
@@ -62,13 +61,7 @@ describe('the plugin', () => {
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("supports requests without start/end defined", async () => {
 | 
			
		||||
            const telemetry = await openmct.telemetry.request(mockDomainObject);
 | 
			
		||||
            expect(telemetry[0].message).toContain('CC: Eagle, Houston');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("supports requests with arbitrary start time in the past", async () => {
 | 
			
		||||
            mockDomainObject.options.start = 100000000000; // Mar 03 1973
 | 
			
		||||
        it("supports requests", async () => {
 | 
			
		||||
            const telemetry = await openmct.telemetry.request(mockDomainObject);
 | 
			
		||||
            expect(telemetry[0].message).toContain('CC: Eagle, Houston');
 | 
			
		||||
        });
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,7 @@
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
import EventEmitter from 'EventEmitter';
 | 
			
		||||
import { v4 as uuid } from 'uuid';
 | 
			
		||||
import uuid from 'uuid';
 | 
			
		||||
import createExampleUser from './exampleUserCreator';
 | 
			
		||||
 | 
			
		||||
export default class ExampleUserProvider extends EventEmitter {
 | 
			
		||||
 
 | 
			
		||||
@@ -26,7 +26,7 @@ import {
 | 
			
		||||
} from '../../src/utils/testing';
 | 
			
		||||
import ExampleUserProvider from './ExampleUserProvider';
 | 
			
		||||
 | 
			
		||||
xdescribe("The Example User Plugin", () => {
 | 
			
		||||
describe("The Example User Plugin", () => {
 | 
			
		||||
    let openmct;
 | 
			
		||||
 | 
			
		||||
    beforeEach(() => {
 | 
			
		||||
 
 | 
			
		||||
@@ -35,8 +35,8 @@ define([
 | 
			
		||||
        phase: 0
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    function GeneratorProvider(openmct) {
 | 
			
		||||
        this.workerInterface = new WorkerInterface(openmct);
 | 
			
		||||
    function GeneratorProvider() {
 | 
			
		||||
        this.workerInterface = new WorkerInterface();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    GeneratorProvider.prototype.canProvideTelemetry = function (domainObject) {
 | 
			
		||||
 
 | 
			
		||||
@@ -21,13 +21,20 @@
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
define([
 | 
			
		||||
    'raw-loader!./generatorWorker.js',
 | 
			
		||||
    'uuid'
 | 
			
		||||
], function (
 | 
			
		||||
    workerText,
 | 
			
		||||
    uuid
 | 
			
		||||
) {
 | 
			
		||||
    function WorkerInterface(openmct) {
 | 
			
		||||
        // eslint-disable-next-line no-undef
 | 
			
		||||
        const workerUrl = `${openmct.getAssetPath()}${__OPENMCT_ROOT_RELATIVE__}generatorWorker.js`;
 | 
			
		||||
 | 
			
		||||
    var workerBlob = new Blob(
 | 
			
		||||
        [workerText],
 | 
			
		||||
        {type: 'application/javascript'}
 | 
			
		||||
    );
 | 
			
		||||
    var workerUrl = URL.createObjectURL(workerBlob);
 | 
			
		||||
 | 
			
		||||
    function WorkerInterface() {
 | 
			
		||||
        this.worker = new Worker(workerUrl);
 | 
			
		||||
        this.worker.onmessage = this.onMessage.bind(this);
 | 
			
		||||
        this.callbacks = {};
 | 
			
		||||
 
 | 
			
		||||
@@ -146,7 +146,7 @@ define([
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        openmct.telemetry.addProvider(new GeneratorProvider(openmct));
 | 
			
		||||
        openmct.telemetry.addProvider(new GeneratorProvider());
 | 
			
		||||
        openmct.telemetry.addProvider(new GeneratorMetadataProvider());
 | 
			
		||||
        openmct.telemetry.addProvider(new SinewaveLimitProvider());
 | 
			
		||||
    };
 | 
			
		||||
 
 | 
			
		||||
@@ -77,7 +77,7 @@
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        openmct.install(openmct.plugins.LocalStorage());
 | 
			
		||||
 | 
			
		||||
      
 | 
			
		||||
        openmct.install(openmct.plugins.example.Generator());
 | 
			
		||||
        openmct.install(openmct.plugins.example.EventGeneratorPlugin());
 | 
			
		||||
        openmct.install(openmct.plugins.example.ExampleImagery());
 | 
			
		||||
@@ -195,9 +195,6 @@
 | 
			
		||||
        ));
 | 
			
		||||
        openmct.install(openmct.plugins.Clock({ enableClockIndicator: true }));
 | 
			
		||||
        openmct.install(openmct.plugins.Timer());
 | 
			
		||||
        openmct.install(openmct.plugins.Timelist());
 | 
			
		||||
        openmct.install(openmct.plugins.BarChart());
 | 
			
		||||
        openmct.install(openmct.plugins.ScatterPlot());
 | 
			
		||||
        openmct.start();
 | 
			
		||||
    </script>
 | 
			
		||||
</html>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,2 +1,3 @@
 | 
			
		||||
const testsContext = require.context('.', true, /^\.\/(src|example)\/.*Spec.js$/);
 | 
			
		||||
const testsContext = require.context('.', true, /\/(src|platform|\.\/example)\/.*Spec.js$/);
 | 
			
		||||
 | 
			
		||||
testsContext.keys().forEach(testsContext);
 | 
			
		||||
 
 | 
			
		||||
@@ -22,9 +22,29 @@
 | 
			
		||||
 | 
			
		||||
/*global module,process*/
 | 
			
		||||
 | 
			
		||||
const browsers = [process.env.NODE_ENV === 'debug' ? 'ChromeDebugging' : 'ChromeHeadless'];
 | 
			
		||||
const coverageEnabled = process.env.COVERAGE === 'true';
 | 
			
		||||
const reporters = ['spec', 'junit'];
 | 
			
		||||
 | 
			
		||||
if (coverageEnabled) {
 | 
			
		||||
    reporters.push('coverage-istanbul');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = (config) => {
 | 
			
		||||
    const webpackConfig = require('./webpack.coverage.js');
 | 
			
		||||
    const webpackConfig = require('./webpack.dev.js');
 | 
			
		||||
    delete webpackConfig.output;
 | 
			
		||||
    if (coverageEnabled) {
 | 
			
		||||
        webpackConfig.module.rules.push({
 | 
			
		||||
            test: /\.js$/,
 | 
			
		||||
            exclude: /node_modules|e2e|example|lib|dist|\.*.*Spec\.js/,
 | 
			
		||||
            use: {
 | 
			
		||||
                loader: 'istanbul-instrumenter-loader',
 | 
			
		||||
                options: {
 | 
			
		||||
                    esModules: true
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    config.set({
 | 
			
		||||
        basePath: '',
 | 
			
		||||
@@ -38,15 +58,11 @@ module.exports = (config) => {
 | 
			
		||||
            {
 | 
			
		||||
                pattern: 'dist/inMemorySearchWorker.js*',
 | 
			
		||||
                included: false
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                pattern: 'dist/generatorWorker.js*',
 | 
			
		||||
                included: false
 | 
			
		||||
            }
 | 
			
		||||
        ],
 | 
			
		||||
        port: 9876,
 | 
			
		||||
        reporters: ['spec', 'junit', 'coverage-istanbul'],
 | 
			
		||||
        browsers: [process.env.NODE_ENV === 'debug' ? 'ChromeDebugging' : 'ChromeHeadless'],
 | 
			
		||||
        reporters: reporters,
 | 
			
		||||
        browsers: browsers,
 | 
			
		||||
        client: {
 | 
			
		||||
            jasmine: {
 | 
			
		||||
                random: false,
 | 
			
		||||
@@ -67,6 +83,12 @@ module.exports = (config) => {
 | 
			
		||||
        colors: true,
 | 
			
		||||
        logLevel: config.LOG_INFO,
 | 
			
		||||
        autoWatch: true,
 | 
			
		||||
        // HTML test reporting.
 | 
			
		||||
        // htmlReporter: {
 | 
			
		||||
        //    outputDir: "dist/reports/tests",
 | 
			
		||||
        //    preserveDescribeNesting: true,
 | 
			
		||||
        //    foldAll: false
 | 
			
		||||
        // },
 | 
			
		||||
        junitReporter: {
 | 
			
		||||
            outputDir: "dist/reports/tests",
 | 
			
		||||
            outputFile: "test-results.xml",
 | 
			
		||||
@@ -74,7 +96,9 @@ module.exports = (config) => {
 | 
			
		||||
        },
 | 
			
		||||
        coverageIstanbulReporter: {
 | 
			
		||||
            fixWebpackSourcePaths: true,
 | 
			
		||||
            dir: "dist/reports/coverage",
 | 
			
		||||
            dir: process.env.CIRCLE_ARTIFACTS
 | 
			
		||||
                ? process.env.CIRCLE_ARTIFACTS + '/coverage'
 | 
			
		||||
                : "dist/reports/coverage",
 | 
			
		||||
            reports: ['lcovonly', 'text-summary'],
 | 
			
		||||
            thresholds: {
 | 
			
		||||
                global: {
 | 
			
		||||
@@ -96,7 +120,8 @@ module.exports = (config) => {
 | 
			
		||||
        },
 | 
			
		||||
        webpack: webpackConfig,
 | 
			
		||||
        webpackMiddleware: {
 | 
			
		||||
            stats: 'errors-warnings'
 | 
			
		||||
            stats: 'errors-only',
 | 
			
		||||
            logLevel: 'warn'
 | 
			
		||||
        },
 | 
			
		||||
        concurrency: 1,
 | 
			
		||||
        singleRun: true,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										113
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										113
									
								
								package.json
									
									
									
									
									
								
							@@ -1,102 +1,97 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "openmct",
 | 
			
		||||
  "version": "2.0.5-SNAPSHOT",
 | 
			
		||||
  "version": "2.0.1-SNAPSHOT",
 | 
			
		||||
  "description": "The Open MCT core platform",
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@babel/eslint-parser": "7.16.3",
 | 
			
		||||
    "@braintree/sanitize-url": "6.0.0",
 | 
			
		||||
    "@percy/cli": "1.2.1",
 | 
			
		||||
    "@percy/playwright": "1.0.4",
 | 
			
		||||
    "@playwright/test": "1.21.1",
 | 
			
		||||
    "@types/eventemitter3": "^1.0.0",
 | 
			
		||||
    "@types/jasmine": "^4.0.1",
 | 
			
		||||
    "@types/karma": "^6.3.2",
 | 
			
		||||
    "@types/lodash": "^4.14.178",
 | 
			
		||||
    "@types/mocha": "^9.1.0",
 | 
			
		||||
    "babel-loader": "8.2.3",
 | 
			
		||||
    "babel-plugin-istanbul": "6.1.1",
 | 
			
		||||
    "@percy/cli": "1.0.0-beta.75",
 | 
			
		||||
    "@percy/playwright": "1.0.1",
 | 
			
		||||
    "@playwright/test": "1.19.2",
 | 
			
		||||
    "allure-playwright": "2.0.0-beta.15",
 | 
			
		||||
    "babel-eslint": "10.1.0",
 | 
			
		||||
    "comma-separated-values": "3.6.4",
 | 
			
		||||
    "copy-webpack-plugin": "11.0.0",
 | 
			
		||||
    "copy-webpack-plugin": "10.2.0",
 | 
			
		||||
    "core-js": "3.20.3",
 | 
			
		||||
    "cross-env": "7.0.3",
 | 
			
		||||
    "css-loader": "4.0.0",
 | 
			
		||||
    "d3-axis": "3.0.0",
 | 
			
		||||
    "d3-scale": "3.3.0",
 | 
			
		||||
    "d3-selection": "3.0.0",
 | 
			
		||||
    "eslint": "8.13.0",
 | 
			
		||||
    "eslint-plugin-compat": "4.0.2",
 | 
			
		||||
    "eslint-plugin-playwright": "0.9.0",
 | 
			
		||||
    "eslint-plugin-vue": "8.5.0",
 | 
			
		||||
    "d3-axis": "1.0.x",
 | 
			
		||||
    "d3-scale": "1.0.x",
 | 
			
		||||
    "d3-selection": "1.3.x",
 | 
			
		||||
    "eslint": "7.0.0",
 | 
			
		||||
    "eslint-plugin-playwright": "0.8.0",
 | 
			
		||||
    "eslint-plugin-vue": "7.5.0",
 | 
			
		||||
    "eslint-plugin-you-dont-need-lodash-underscore": "6.12.0",
 | 
			
		||||
    "eventemitter3": "1.2.0",
 | 
			
		||||
    "exports-loader": "0.7.0",
 | 
			
		||||
    "express": "4.13.1",
 | 
			
		||||
    "file-loader": "6.1.0",
 | 
			
		||||
    "file-saver": "2.0.5",
 | 
			
		||||
    "git-rev-sync": "3.0.2",
 | 
			
		||||
    "git-rev-sync": "1.4.0",
 | 
			
		||||
    "html-loader": "0.5.5",
 | 
			
		||||
    "html2canvas": "1.4.1",
 | 
			
		||||
    "imports-loader": "0.8.0",
 | 
			
		||||
    "jasmine-core": "4.1.1",
 | 
			
		||||
    "istanbul-instrumenter-loader": "3.0.1",
 | 
			
		||||
    "jasmine-core": "4.0.0",
 | 
			
		||||
    "jsdoc": "3.5.5",
 | 
			
		||||
    "karma": "6.3.20",
 | 
			
		||||
    "karma-chrome-launcher": "3.1.1",
 | 
			
		||||
    "karma": "6.3.15",
 | 
			
		||||
    "karma-chrome-launcher": "3.1.0",
 | 
			
		||||
    "karma-cli": "2.0.0",
 | 
			
		||||
    "karma-coverage": "2.2.0",
 | 
			
		||||
    "karma-coverage": "2.1.1",
 | 
			
		||||
    "karma-coverage-istanbul-reporter": "3.0.3",
 | 
			
		||||
    "karma-firefox-launcher": "2.1.2",
 | 
			
		||||
    "karma-jasmine": "4.0.1",
 | 
			
		||||
    "karma-junit-reporter": "2.0.1",
 | 
			
		||||
    "karma-sourcemap-loader": "0.3.8",
 | 
			
		||||
    "karma-spec-reporter": "0.0.34",
 | 
			
		||||
    "karma-spec-reporter": "0.0.33",
 | 
			
		||||
    "karma-webpack": "5.0.0",
 | 
			
		||||
    "lighthouse": "9.6.1",
 | 
			
		||||
    "location-bar": "3.0.1",
 | 
			
		||||
    "lodash": "4.17.21",
 | 
			
		||||
    "mini-css-extract-plugin": "2.6.0",
 | 
			
		||||
    "moment": "2.29.3",
 | 
			
		||||
    "moment-duration-format": "2.3.2",
 | 
			
		||||
    "moment-timezone": "0.5.34",
 | 
			
		||||
    "lodash": "4.17.12",
 | 
			
		||||
    "mini-css-extract-plugin": "2.4.5",
 | 
			
		||||
    "moment": "2.29.1",
 | 
			
		||||
    "moment-duration-format": "2.2.2",
 | 
			
		||||
    "moment-timezone": "0.5.28",
 | 
			
		||||
    "node-bourbon": "4.2.3",
 | 
			
		||||
    "painterro": "1.2.56",
 | 
			
		||||
    "plotly.js-basic-dist": "2.12.0",
 | 
			
		||||
    "plotly.js-gl2d-dist": "2.12.0",
 | 
			
		||||
    "printj": "1.3.1",
 | 
			
		||||
    "request": "2.88.2",
 | 
			
		||||
    "resolve-url-loader": "5.0.0",
 | 
			
		||||
    "sass": "1.49.9",
 | 
			
		||||
    "sass-loader": "12.6.0",
 | 
			
		||||
    "sinon": "14.0.0",
 | 
			
		||||
    "plotly.js-basic-dist": "2.5.0",
 | 
			
		||||
    "plotly.js-gl2d-dist": "2.5.0",
 | 
			
		||||
    "printj": "1.2.1",
 | 
			
		||||
    "raw-loader": "0.5.1",
 | 
			
		||||
    "request": "2.69.0",
 | 
			
		||||
    "resolve-url-loader": "4.0.0",
 | 
			
		||||
    "sass": "1.49.0",
 | 
			
		||||
    "sass-loader": "12.4.0",
 | 
			
		||||
    "sinon": "13.0.1",
 | 
			
		||||
    "style-loader": "^1.0.1",
 | 
			
		||||
    "uuid": "8.3.2",
 | 
			
		||||
    "uuid": "3.3.3",
 | 
			
		||||
    "vue": "2.6.14",
 | 
			
		||||
    "vue-eslint-parser": "8.3.0",
 | 
			
		||||
    "vue-eslint-parser": "8.2.0",
 | 
			
		||||
    "vue-loader": "15.9.8",
 | 
			
		||||
    "vue-template-compiler": "2.6.14",
 | 
			
		||||
    "webpack": "5.68.0",
 | 
			
		||||
    "webpack-cli": "4.9.2",
 | 
			
		||||
    "webpack-dev-middleware": "5.3.3",
 | 
			
		||||
    "webpack-hot-middleware": "2.25.1",
 | 
			
		||||
    "webpack-dev-middleware": "3.7.3",
 | 
			
		||||
    "webpack-hot-middleware": "2.22.3",
 | 
			
		||||
    "webpack-merge": "5.8.0",
 | 
			
		||||
    "zepto": "1.2.0"
 | 
			
		||||
  },
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "clean": "rm -rf ./dist ./node_modules ./package-lock.json",
 | 
			
		||||
    "clean": "rm -rf ./dist ./node_modules; rm package-lock.json",
 | 
			
		||||
    "clean-test-lint": "npm run clean; npm install; npm run test; npm run lint",
 | 
			
		||||
    "start": "node app.js",
 | 
			
		||||
    "lint": "eslint example src e2e --ext .js,.vue openmct.js --max-warnings=0",
 | 
			
		||||
    "lint:fix": "eslint example src e2e --ext .js,.vue openmct.js --fix",
 | 
			
		||||
    "lint": "eslint example src --ext .js,.vue openmct.js",
 | 
			
		||||
    "lint:fix": "eslint example src --ext .js,.vue openmct.js --fix",
 | 
			
		||||
    "build:prod": "cross-env webpack --config webpack.prod.js",
 | 
			
		||||
    "build:dev": "webpack --config webpack.dev.js",
 | 
			
		||||
    "build:coverage": "webpack --config webpack.coverage.js",
 | 
			
		||||
    "build:watch": "webpack --config webpack.dev.js --watch",
 | 
			
		||||
    "info": "npx envinfo --system --browsers --npmPackages --binaries --languages --markdown",
 | 
			
		||||
    "test": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run",
 | 
			
		||||
    "test:firefox": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run --browsers=FirefoxHeadless",
 | 
			
		||||
    "test:debug": "cross-env NODE_ENV=debug karma start --no-single-run",
 | 
			
		||||
    "test:e2e:ci": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome smoke branding default condition timeConductor clock exampleImagery notebook persistence performance",
 | 
			
		||||
    "test:coverage": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" COVERAGE=true karma start --single-run",
 | 
			
		||||
    "test:coverage:firefox": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run --browsers=FirefoxHeadless",
 | 
			
		||||
    "test:e2e:ci": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome smoke default condition timeConductor",
 | 
			
		||||
    "test:e2e:local": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome",
 | 
			
		||||
    "test:e2e:updatesnapshots": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome --grep @snapshot --update-snapshots",
 | 
			
		||||
    "test:e2e:visual": "percy exec --config ./e2e/.percy.yml -- npx playwright test --config=e2e/playwright-visual.config.js",
 | 
			
		||||
    "test:e2e:visual": "percy exec --config ./e2e/.percy.yml -- npx playwright test --config=e2e/playwright-visual.config.js default",
 | 
			
		||||
    "test:e2e:full": "npx playwright test --config=e2e/playwright-ci.config.js",
 | 
			
		||||
    "test:perf": "npx playwright test --config=e2e/playwright-performance.config.js",
 | 
			
		||||
    "test:watch": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --no-single-run",
 | 
			
		||||
    "jsdoc": "jsdoc -c jsdoc.json -R API.md -r -d dist/docs/api",
 | 
			
		||||
    "update-about-dialog-copyright": "perl -pi -e 's/20\\d\\d\\-202\\d/2014\\-2022/gm' ./src/ui/layout/AboutDialog.vue",
 | 
			
		||||
@@ -110,18 +105,8 @@
 | 
			
		||||
    "url": "https://github.com/nasa/openmct.git"
 | 
			
		||||
  },
 | 
			
		||||
  "engines": {
 | 
			
		||||
    "node": ">=14.19.1"
 | 
			
		||||
    "node": ">=12.22.0"
 | 
			
		||||
  },
 | 
			
		||||
  "overrides": {
 | 
			
		||||
    "core-js": "3.21.1"
 | 
			
		||||
  },
 | 
			
		||||
  "browserslist": [
 | 
			
		||||
    "Firefox ESR",
 | 
			
		||||
    "not IE 11",
 | 
			
		||||
    "last 2 Chrome versions",
 | 
			
		||||
    "unreleased Chrome versions",
 | 
			
		||||
    "ios_saf > 15"
 | 
			
		||||
  ],
 | 
			
		||||
  "author": "",
 | 
			
		||||
  "license": "Apache-2.0",
 | 
			
		||||
  "private": true
 | 
			
		||||
 
 | 
			
		||||
@@ -241,7 +241,9 @@ define([
 | 
			
		||||
        this.branding = BrandingAPI.default;
 | 
			
		||||
 | 
			
		||||
        // Plugins that are installed by default
 | 
			
		||||
 | 
			
		||||
        this.install(this.plugins.Plot());
 | 
			
		||||
        this.install(this.plugins.Chart());
 | 
			
		||||
        this.install(this.plugins.TelemetryTable.default());
 | 
			
		||||
        this.install(PreviewPlugin.default());
 | 
			
		||||
        this.install(LicensesPlugin.default());
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
import AutoCompleteField from './components/controls/AutoCompleteField.vue';
 | 
			
		||||
import ClockDisplayFormatField from './components/controls/ClockDisplayFormatField.vue';
 | 
			
		||||
import CheckBoxField from './components/controls/CheckBoxField.vue';
 | 
			
		||||
import Datetime from './components/controls/Datetime.vue';
 | 
			
		||||
import FileInput from './components/controls/FileInput.vue';
 | 
			
		||||
import Locator from './components/controls/Locator.vue';
 | 
			
		||||
@@ -8,13 +7,11 @@ import NumberField from './components/controls/NumberField.vue';
 | 
			
		||||
import SelectField from './components/controls/SelectField.vue';
 | 
			
		||||
import TextAreaField from './components/controls/TextAreaField.vue';
 | 
			
		||||
import TextField from './components/controls/TextField.vue';
 | 
			
		||||
import ToggleSwitchField from './components/controls/ToggleSwitchField.vue';
 | 
			
		||||
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
 | 
			
		||||
export const DEFAULT_CONTROLS_MAP = {
 | 
			
		||||
    'autocomplete': AutoCompleteField,
 | 
			
		||||
    'checkbox': CheckBoxField,
 | 
			
		||||
    'composite': ClockDisplayFormatField,
 | 
			
		||||
    'datetime': Datetime,
 | 
			
		||||
    'file-input': FileInput,
 | 
			
		||||
@@ -22,8 +19,7 @@ export const DEFAULT_CONTROLS_MAP = {
 | 
			
		||||
    'numberfield': NumberField,
 | 
			
		||||
    'select': SelectField,
 | 
			
		||||
    'textarea': TextAreaField,
 | 
			
		||||
    'textfield': TextField,
 | 
			
		||||
    'toggleSwitch': ToggleSwitchField
 | 
			
		||||
    'textfield': TextField
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default class FormControl {
 | 
			
		||||
@@ -69,11 +65,10 @@ export default class FormControl {
 | 
			
		||||
     */
 | 
			
		||||
    _getControlViewProvider(control) {
 | 
			
		||||
        const self = this;
 | 
			
		||||
        let rowComponent;
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
            show(element, model, onChange) {
 | 
			
		||||
                rowComponent = new Vue({
 | 
			
		||||
                const rowComponent = new Vue({
 | 
			
		||||
                    el: element,
 | 
			
		||||
                    components: {
 | 
			
		||||
                        FormControlComponent: DEFAULT_CONTROLS_MAP[control]
 | 
			
		||||
@@ -91,10 +86,8 @@ export default class FormControl {
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                return rowComponent;
 | 
			
		||||
            },
 | 
			
		||||
            destroy() {
 | 
			
		||||
                rowComponent.$destroy();
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -23,13 +23,10 @@
 | 
			
		||||
import FormController from './FormController';
 | 
			
		||||
import FormProperties from './components/FormProperties.vue';
 | 
			
		||||
 | 
			
		||||
import EventEmitter from 'EventEmitter';
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
 | 
			
		||||
export default class FormsAPI extends EventEmitter {
 | 
			
		||||
export default class FormsAPI {
 | 
			
		||||
    constructor(openmct) {
 | 
			
		||||
        super();
 | 
			
		||||
 | 
			
		||||
        this.openmct = openmct;
 | 
			
		||||
        this.formController = new FormController(openmct);
 | 
			
		||||
    }
 | 
			
		||||
@@ -110,8 +107,6 @@ export default class FormsAPI extends EventEmitter {
 | 
			
		||||
        let onDismiss;
 | 
			
		||||
        let onSave;
 | 
			
		||||
 | 
			
		||||
        const self = this;
 | 
			
		||||
 | 
			
		||||
        const promise = new Promise((resolve, reject) => {
 | 
			
		||||
            onSave = onFormSave(resolve);
 | 
			
		||||
            onDismiss = onFormDismiss(reject);
 | 
			
		||||
@@ -120,7 +115,7 @@ export default class FormsAPI extends EventEmitter {
 | 
			
		||||
        const vm = new Vue({
 | 
			
		||||
            components: { FormProperties },
 | 
			
		||||
            provide: {
 | 
			
		||||
                openmct: self.openmct
 | 
			
		||||
                openmct: this.openmct
 | 
			
		||||
            },
 | 
			
		||||
            data() {
 | 
			
		||||
                return {
 | 
			
		||||
@@ -137,7 +132,7 @@ export default class FormsAPI extends EventEmitter {
 | 
			
		||||
        if (element) {
 | 
			
		||||
            element.append(formElement);
 | 
			
		||||
        } else {
 | 
			
		||||
            overlay = self.openmct.overlays.overlay({
 | 
			
		||||
            overlay = this.openmct.overlays.overlay({
 | 
			
		||||
                element: vm.$el,
 | 
			
		||||
                size: 'small',
 | 
			
		||||
                onDestroy: () => vm.$destroy()
 | 
			
		||||
@@ -145,7 +140,6 @@ export default class FormsAPI extends EventEmitter {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        function onFormPropertyChange(data) {
 | 
			
		||||
            self.emit('onFormPropertyChange', data);
 | 
			
		||||
            if (onChange) {
 | 
			
		||||
                onChange(data);
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,157 +0,0 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2022, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
import { createOpenMct, resetApplicationState } from '../../utils/testing';
 | 
			
		||||
 | 
			
		||||
describe('The Forms API', () => {
 | 
			
		||||
    let openmct;
 | 
			
		||||
    let element;
 | 
			
		||||
 | 
			
		||||
    beforeEach((done) => {
 | 
			
		||||
        element = document.createElement('div');
 | 
			
		||||
        element.style.display = 'block';
 | 
			
		||||
        element.style.width = '1920px';
 | 
			
		||||
        element.style.height = '1080px';
 | 
			
		||||
 | 
			
		||||
        openmct = createOpenMct();
 | 
			
		||||
        openmct.on('start', done);
 | 
			
		||||
 | 
			
		||||
        openmct.startHeadless(element);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(() => {
 | 
			
		||||
        return resetApplicationState(openmct);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('openmct supports form API', () => {
 | 
			
		||||
        expect(openmct.forms).not.toBe(null);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('check default form controls exists', () => {
 | 
			
		||||
        it('autocomplete', () => {
 | 
			
		||||
            const control = openmct.forms.getFormControl('autocomplete');
 | 
			
		||||
            expect(control).not.toBe(null);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('clock', () => {
 | 
			
		||||
            const control = openmct.forms.getFormControl('composite');
 | 
			
		||||
            expect(control).not.toBe(null);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('datetime', () => {
 | 
			
		||||
            const control = openmct.forms.getFormControl('datetime');
 | 
			
		||||
            expect(control).not.toBe(null);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('file-input', () => {
 | 
			
		||||
            const control = openmct.forms.getFormControl('file-input');
 | 
			
		||||
            expect(control).not.toBe(null);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('locator', () => {
 | 
			
		||||
            const control = openmct.forms.getFormControl('locator');
 | 
			
		||||
            expect(control).not.toBe(null);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('numberfield', () => {
 | 
			
		||||
            const control = openmct.forms.getFormControl('numberfield');
 | 
			
		||||
            expect(control).not.toBe(null);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('select', () => {
 | 
			
		||||
            const control = openmct.forms.getFormControl('select');
 | 
			
		||||
            expect(control).not.toBe(null);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('textarea', () => {
 | 
			
		||||
            const control = openmct.forms.getFormControl('textarea');
 | 
			
		||||
            expect(control).not.toBe(null);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('textfield', () => {
 | 
			
		||||
            const control = openmct.forms.getFormControl('textfield');
 | 
			
		||||
            expect(control).not.toBe(null);
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('supports user defined form controls', () => {
 | 
			
		||||
        const newFormControl = {
 | 
			
		||||
            show: () => {
 | 
			
		||||
                console.log('show new control');
 | 
			
		||||
            },
 | 
			
		||||
            destroy: () => {
 | 
			
		||||
                console.log('destroy');
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
        openmct.forms.addNewFormControl('newFormControl', newFormControl);
 | 
			
		||||
        const control = openmct.forms.getFormControl('newFormControl');
 | 
			
		||||
        expect(control).not.toBe(null);
 | 
			
		||||
        expect(control.show).not.toBe(null);
 | 
			
		||||
        expect(control.destroy).not.toBe(null);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('show form on UI', () => {
 | 
			
		||||
        let formStructure;
 | 
			
		||||
 | 
			
		||||
        beforeEach(() => {
 | 
			
		||||
            formStructure = {
 | 
			
		||||
                title: 'Test Show Form',
 | 
			
		||||
                sections: [
 | 
			
		||||
                    {
 | 
			
		||||
                        rows: [
 | 
			
		||||
                            {
 | 
			
		||||
                                key: 'name',
 | 
			
		||||
                                control: 'textfield',
 | 
			
		||||
                                name: 'Title',
 | 
			
		||||
                                pattern: '\\S+',
 | 
			
		||||
                                required: false,
 | 
			
		||||
                                cssClass: 'l-input-lg',
 | 
			
		||||
                                value: 'Test Name'
 | 
			
		||||
                            }
 | 
			
		||||
                        ]
 | 
			
		||||
                    }
 | 
			
		||||
                ]
 | 
			
		||||
            };
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('when container element is provided', (done) => {
 | 
			
		||||
            openmct.forms.showForm(formStructure, { element }).catch(() => {
 | 
			
		||||
                done();
 | 
			
		||||
            });
 | 
			
		||||
            const titleElement = element.querySelector('.c-overlay__dialog-title');
 | 
			
		||||
            expect(titleElement.textContent).toBe(formStructure.title);
 | 
			
		||||
 | 
			
		||||
            element.querySelector('.js-cancel-button').click();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('when container element is not provided', (done) => {
 | 
			
		||||
            openmct.forms.showForm(formStructure).catch(() => {
 | 
			
		||||
                done();
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const titleElement = document.querySelector('.c-overlay__dialog-title');
 | 
			
		||||
            const title = titleElement.textContent;
 | 
			
		||||
 | 
			
		||||
            expect(title).toBe(formStructure.title);
 | 
			
		||||
            document.querySelector('.js-cancel-button').click();
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@@ -21,57 +21,50 @@
 | 
			
		||||
*****************************************************************************/
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
<div class="c-form js-form">
 | 
			
		||||
<div class="c-form">
 | 
			
		||||
    <div class="c-overlay__top-bar c-form__top-bar">
 | 
			
		||||
        <div class="c-overlay__dialog-title js-form-title">{{ model.title }}</div>
 | 
			
		||||
        <div class="c-overlay__dialog-title">{{ model.title }}</div>
 | 
			
		||||
        <div class="c-overlay__dialog-hint hint">All fields marked <span class="req icon-asterisk"></span> are required.</div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <form
 | 
			
		||||
        name="mctForm"
 | 
			
		||||
        class="c-form__contents"
 | 
			
		||||
        autocomplete="off"
 | 
			
		||||
        @submit.prevent
 | 
			
		||||
    <form name="mctForm"
 | 
			
		||||
          class="c-form__contents"
 | 
			
		||||
          autocomplete="off"
 | 
			
		||||
          @submit.prevent
 | 
			
		||||
    >
 | 
			
		||||
        <div
 | 
			
		||||
            v-for="section in formSections"
 | 
			
		||||
            :key="section.id"
 | 
			
		||||
            class="c-form__section"
 | 
			
		||||
            :class="section.cssClass"
 | 
			
		||||
        <div v-for="section in formSections"
 | 
			
		||||
             :key="section.id"
 | 
			
		||||
             class="c-form__section"
 | 
			
		||||
             :class="section.cssClass"
 | 
			
		||||
        >
 | 
			
		||||
            <h2
 | 
			
		||||
                v-if="section.name"
 | 
			
		||||
            <h2 v-if="section.name"
 | 
			
		||||
                class="c-form__section-header"
 | 
			
		||||
            >
 | 
			
		||||
                {{ section.name }}
 | 
			
		||||
            </h2>
 | 
			
		||||
            <div
 | 
			
		||||
                v-for="(row, index) in section.rows"
 | 
			
		||||
                :key="row.id"
 | 
			
		||||
                class="u-contents"
 | 
			
		||||
            <div v-for="(row, index) in section.rows"
 | 
			
		||||
                 :key="row.id"
 | 
			
		||||
                 class="u-contents"
 | 
			
		||||
            >
 | 
			
		||||
                <FormRow
 | 
			
		||||
                    :css-class="section.cssClass"
 | 
			
		||||
                    :first="index < 1"
 | 
			
		||||
                    :row="row"
 | 
			
		||||
                    @onChange="onChange"
 | 
			
		||||
                <FormRow :css-class="section.cssClass"
 | 
			
		||||
                         :first="index < 1"
 | 
			
		||||
                         :row="row"
 | 
			
		||||
                         @onChange="onChange"
 | 
			
		||||
                />
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </form>
 | 
			
		||||
 | 
			
		||||
    <div class="mct-form__controls c-overlay__button-bar c-form__bottom-bar">
 | 
			
		||||
        <button
 | 
			
		||||
            tabindex="0"
 | 
			
		||||
            :disabled="isInvalid"
 | 
			
		||||
            class="c-button c-button--major"
 | 
			
		||||
            @click="onSave"
 | 
			
		||||
        <button tabindex="0"
 | 
			
		||||
                :disabled="isInvalid"
 | 
			
		||||
                class="c-button c-button--major"
 | 
			
		||||
                @click="onSave"
 | 
			
		||||
        >
 | 
			
		||||
            {{ submitLabel }}
 | 
			
		||||
        </button>
 | 
			
		||||
        <button
 | 
			
		||||
            tabindex="0"
 | 
			
		||||
            class="c-button js-cancel-button"
 | 
			
		||||
            @click="onDismiss"
 | 
			
		||||
        <button tabindex="0"
 | 
			
		||||
                class="c-button"
 | 
			
		||||
                @click="onDismiss"
 | 
			
		||||
        >
 | 
			
		||||
            {{ cancelLabel }}
 | 
			
		||||
        </button>
 | 
			
		||||
@@ -81,7 +74,7 @@
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import FormRow from "@/api/forms/components/FormRow.vue";
 | 
			
		||||
import { v4 as uuid } from 'uuid';
 | 
			
		||||
import uuid from 'uuid';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    components: {
 | 
			
		||||
 
 | 
			
		||||
@@ -21,25 +21,21 @@
 | 
			
		||||
*****************************************************************************/
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
<div
 | 
			
		||||
    class="form-row c-form__row"
 | 
			
		||||
    :class="[{ 'first': first }]"
 | 
			
		||||
    @onChange="onChange"
 | 
			
		||||
<div class="form-row c-form__row"
 | 
			
		||||
     :class="[{ 'first': first }]"
 | 
			
		||||
     @onChange="onChange"
 | 
			
		||||
>
 | 
			
		||||
    <div
 | 
			
		||||
        class="c-form-row__label"
 | 
			
		||||
        :title="row.description"
 | 
			
		||||
    <div class="c-form-row__label"
 | 
			
		||||
         :title="row.description"
 | 
			
		||||
    >
 | 
			
		||||
        {{ row.name }}
 | 
			
		||||
    </div>
 | 
			
		||||
    <div
 | 
			
		||||
        class="c-form-row__state-indicator"
 | 
			
		||||
        :class="rowClass"
 | 
			
		||||
    <div class="c-form-row__state-indicator"
 | 
			
		||||
         :class="rowClass"
 | 
			
		||||
    >
 | 
			
		||||
    </div>
 | 
			
		||||
    <div
 | 
			
		||||
        v-if="row.control"
 | 
			
		||||
        class="c-form-row__controls"
 | 
			
		||||
    <div v-if="row.control"
 | 
			
		||||
         class="c-form-row__controls"
 | 
			
		||||
    >
 | 
			
		||||
        <div ref="rowElement"></div>
 | 
			
		||||
    </div>
 | 
			
		||||
@@ -79,12 +75,10 @@ export default {
 | 
			
		||||
        rowClass() {
 | 
			
		||||
            let cssClass = this.cssClass;
 | 
			
		||||
 | 
			
		||||
            if (!this.row.required) {
 | 
			
		||||
                return;
 | 
			
		||||
            if (this.row.required) {
 | 
			
		||||
                cssClass = `${cssClass} req`;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            cssClass = `${cssClass} req`;
 | 
			
		||||
 | 
			
		||||
            if (this.visited && this.valid !== undefined) {
 | 
			
		||||
                if (this.valid === true) {
 | 
			
		||||
                    cssClass = `${cssClass} valid`;
 | 
			
		||||
 
 | 
			
		||||
@@ -22,26 +22,20 @@
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
<div class="form-control autocomplete">
 | 
			
		||||
    <span class="autocompleteInputAndArrow">
 | 
			
		||||
        <input
 | 
			
		||||
            v-model="field"
 | 
			
		||||
            class="autocompleteInput"
 | 
			
		||||
            type="text"
 | 
			
		||||
            @click="inputClicked()"
 | 
			
		||||
            @keydown="keyDown($event)"
 | 
			
		||||
        >
 | 
			
		||||
        <span
 | 
			
		||||
            class="icon-arrow-down"
 | 
			
		||||
            @click="arrowClicked()"
 | 
			
		||||
        ></span>
 | 
			
		||||
    </span>
 | 
			
		||||
    <div
 | 
			
		||||
        class="autocompleteOptions"
 | 
			
		||||
        @blur="hideOptions = true"
 | 
			
		||||
    <input v-model="field"
 | 
			
		||||
           class="autocompleteInput"
 | 
			
		||||
           type="text"
 | 
			
		||||
           @click="inputClicked()"
 | 
			
		||||
           @keydown="keyDown($event)"
 | 
			
		||||
    >
 | 
			
		||||
    <span class="icon-arrow-down"
 | 
			
		||||
          @click="arrowClicked()"
 | 
			
		||||
    ></span>
 | 
			
		||||
    <div class="autocompleteOptions"
 | 
			
		||||
         @blur="hideOptions = true"
 | 
			
		||||
    >
 | 
			
		||||
        <ul v-if="!hideOptions">
 | 
			
		||||
            <li
 | 
			
		||||
                v-for="opt in filteredOptions"
 | 
			
		||||
            <li v-for="opt in filteredOptions"
 | 
			
		||||
                :key="opt.optionId"
 | 
			
		||||
                :class="{'optionPreSelected': optionIndex === opt.optionId}"
 | 
			
		||||
                @click="fillInputWithString(opt.name)"
 | 
			
		||||
@@ -110,21 +104,10 @@ export default {
 | 
			
		||||
 | 
			
		||||
                this.$emit('onChange', data);
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        hideOptions(newValue) {
 | 
			
		||||
            if (!newValue) {
 | 
			
		||||
                // adding a event listener when the hideOpntions is false (dropdown is visible)
 | 
			
		||||
                // handleoutsideclick can collapse the dropdown when clicked outside autocomplete
 | 
			
		||||
                document.body.addEventListener('click', this.handleOutsideClick);
 | 
			
		||||
            } else {
 | 
			
		||||
                //removing event listener when hideOptions become true (dropdown is collapsed)
 | 
			
		||||
                document.body.removeEventListener('click', this.handleOutsideClick);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    mounted() {
 | 
			
		||||
        this.options = this.model.options;
 | 
			
		||||
        this.autocompleteInputAndArrow = this.$el.getElementsByClassName('autocompleteInputAndArrow')[0];
 | 
			
		||||
        this.autocompleteInputElement = this.$el.getElementsByClassName('autocompleteInput')[0];
 | 
			
		||||
        if (this.options[0].name) {
 | 
			
		||||
        // If "options" include name, value pair
 | 
			
		||||
@@ -136,9 +119,6 @@ export default {
 | 
			
		||||
            this.optionNames = this.options;
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    destroyed() {
 | 
			
		||||
        document.body.removeEventListener('click', this.handleOutsideClick);
 | 
			
		||||
    },
 | 
			
		||||
    methods: {
 | 
			
		||||
        decrementOptionIndex() {
 | 
			
		||||
            if (this.optionIndex === 0) {
 | 
			
		||||
@@ -192,21 +172,7 @@ export default {
 | 
			
		||||
            // to show them all the options
 | 
			
		||||
            this.showFilteredOptions = false;
 | 
			
		||||
            this.autocompleteInputElement.select();
 | 
			
		||||
 | 
			
		||||
            if (this.hideOptions) {
 | 
			
		||||
                this.showOptions();
 | 
			
		||||
            } else {
 | 
			
		||||
                this.hideOptions = true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        },
 | 
			
		||||
        handleOutsideClick(event) {
 | 
			
		||||
            // if click event is detected outside autocomplete (both input & arrow) while the
 | 
			
		||||
            // dropdown is visible, this will collapse the dropdown.
 | 
			
		||||
            const clickedInsideAutocomplete = this.autocompleteInputAndArrow.contains(event.target);
 | 
			
		||||
            if (!clickedInsideAutocomplete && !this.hideOptions) {
 | 
			
		||||
                this.hideOptions = true;
 | 
			
		||||
            }
 | 
			
		||||
            this.showOptions();
 | 
			
		||||
        },
 | 
			
		||||
        optionMouseover(optionId) {
 | 
			
		||||
            this.optionIndex = optionId;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,55 +0,0 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
* Open MCT, Copyright (c) 2014-2022, United States Government
 | 
			
		||||
* as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
* Administration. All rights reserved.
 | 
			
		||||
*
 | 
			
		||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
* "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
* You may obtain a copy of the License at
 | 
			
		||||
* http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
*
 | 
			
		||||
* Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
* License for the specific language governing permissions and limitations
 | 
			
		||||
* under the License.
 | 
			
		||||
*
 | 
			
		||||
* Open MCT includes source code licensed under additional open source
 | 
			
		||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
* this source code distribution or the Licensing information page available
 | 
			
		||||
* at runtime from the About dialog for additional information.
 | 
			
		||||
*****************************************************************************/
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
<span class="form-control shell">
 | 
			
		||||
    <span
 | 
			
		||||
        class="field control"
 | 
			
		||||
        :class="model.cssClass"
 | 
			
		||||
    >
 | 
			
		||||
        <input
 | 
			
		||||
            type="checkbox"
 | 
			
		||||
            :checked="isChecked"
 | 
			
		||||
            @input="toggleCheckBox"
 | 
			
		||||
        >
 | 
			
		||||
    </span>
 | 
			
		||||
</span>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import toggleMixin from '../../toggle-check-box-mixin';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    mixins: [toggleMixin],
 | 
			
		||||
    props: {
 | 
			
		||||
        model: {
 | 
			
		||||
            type: Object,
 | 
			
		||||
            required: true
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    data() {
 | 
			
		||||
        return {
 | 
			
		||||
            isChecked: this.model.value
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
@@ -22,11 +22,10 @@
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
<div class="c-form-control--clock-display-format-fields">
 | 
			
		||||
    <SelectField
 | 
			
		||||
        v-for="item in items"
 | 
			
		||||
        :key="item.key"
 | 
			
		||||
        :model="item"
 | 
			
		||||
        @onChange="onChange"
 | 
			
		||||
    <SelectField v-for="item in items"
 | 
			
		||||
                 :key="item.key"
 | 
			
		||||
                 :model="item"
 | 
			
		||||
                 @onChange="onChange"
 | 
			
		||||
    />
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 
 | 
			
		||||
@@ -22,13 +22,12 @@
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
<span>
 | 
			
		||||
    <CompositeItem
 | 
			
		||||
        v-for="(item, index) in model.items"
 | 
			
		||||
        :key="item.name"
 | 
			
		||||
        :first="index < 1"
 | 
			
		||||
        :value="JSON.stringify(model.value[index])"
 | 
			
		||||
        :item="item"
 | 
			
		||||
        @onChange="onChange"
 | 
			
		||||
    <CompositeItem v-for="(item, index) in model.items"
 | 
			
		||||
                   :key="item.name"
 | 
			
		||||
                   :first="index < 1"
 | 
			
		||||
                   :value="JSON.stringify(model.value[index])"
 | 
			
		||||
                   :item="item"
 | 
			
		||||
                   @onChange="onChange"
 | 
			
		||||
    />
 | 
			
		||||
</span>
 | 
			
		||||
</template>
 | 
			
		||||
 
 | 
			
		||||
@@ -22,11 +22,10 @@
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
<div :class="compositeCssClass">
 | 
			
		||||
    <FormRow
 | 
			
		||||
        :css-class="item.cssClass"
 | 
			
		||||
        :first="first"
 | 
			
		||||
        :row="row"
 | 
			
		||||
        @onChange="onChange"
 | 
			
		||||
    <FormRow :css-class="item.cssClass"
 | 
			
		||||
             :first="first"
 | 
			
		||||
             :row="row"
 | 
			
		||||
             @onChange="onChange"
 | 
			
		||||
    />
 | 
			
		||||
    <span class="composite-control-label">
 | 
			
		||||
        {{ item.name }}
 | 
			
		||||
 
 | 
			
		||||
@@ -27,55 +27,50 @@
 | 
			
		||||
    <div class="hint time sm">Min</div>
 | 
			
		||||
    <div class="hint time sm">Sec</div>
 | 
			
		||||
    <div class="hint timezone">Timezone</div>
 | 
			
		||||
    <form
 | 
			
		||||
        ref="dateTimeForm"
 | 
			
		||||
        prevent
 | 
			
		||||
        class="u-contents"
 | 
			
		||||
    <form ref="dateTimeForm"
 | 
			
		||||
          prevent
 | 
			
		||||
          class="u-contents"
 | 
			
		||||
    >
 | 
			
		||||
        <div class="field control date">
 | 
			
		||||
            <input
 | 
			
		||||
                v-model="date"
 | 
			
		||||
                :pattern="/\d{4}-\d{2}-\d{2}/"
 | 
			
		||||
                :placeholder="format"
 | 
			
		||||
                type="date"
 | 
			
		||||
                name="date"
 | 
			
		||||
                @change="onChange"
 | 
			
		||||
            <input v-model="date"
 | 
			
		||||
                   :pattern="/\d{4}-\d{2}-\d{2}/"
 | 
			
		||||
                   :placeholder="format"
 | 
			
		||||
                   type="date"
 | 
			
		||||
                   name="date"
 | 
			
		||||
                   @change="onChange"
 | 
			
		||||
            >
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="field control hour sm">
 | 
			
		||||
            <input
 | 
			
		||||
                v-model="hour"
 | 
			
		||||
                :pattern="/\d+/"
 | 
			
		||||
                type="number"
 | 
			
		||||
                name="hour"
 | 
			
		||||
                maxlength="10"
 | 
			
		||||
                min="0"
 | 
			
		||||
                max="23"
 | 
			
		||||
                @change="onChange"
 | 
			
		||||
            <input v-model="hour"
 | 
			
		||||
                   :pattern="/\d+/"
 | 
			
		||||
                   type="number"
 | 
			
		||||
                   name="hour"
 | 
			
		||||
                   maxlength="10"
 | 
			
		||||
                   min="0"
 | 
			
		||||
                   max="23"
 | 
			
		||||
                   @change="onChange"
 | 
			
		||||
            >
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="field control min sm">
 | 
			
		||||
            <input
 | 
			
		||||
                v-model="min"
 | 
			
		||||
                :pattern="/\d+/"
 | 
			
		||||
                type="number"
 | 
			
		||||
                name="min"
 | 
			
		||||
                maxlength="2"
 | 
			
		||||
                min="0"
 | 
			
		||||
                max="59"
 | 
			
		||||
                @change="onChange"
 | 
			
		||||
            <input v-model="min"
 | 
			
		||||
                   :pattern="/\d+/"
 | 
			
		||||
                   type="number"
 | 
			
		||||
                   name="min"
 | 
			
		||||
                   maxlength="2"
 | 
			
		||||
                   min="0"
 | 
			
		||||
                   max="59"
 | 
			
		||||
                   @change="onChange"
 | 
			
		||||
            >
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="field control sec sm">
 | 
			
		||||
            <input
 | 
			
		||||
                v-model="sec"
 | 
			
		||||
                :pattern="/\d+/"
 | 
			
		||||
                type="number"
 | 
			
		||||
                name="sec"
 | 
			
		||||
                maxlength="2"
 | 
			
		||||
                min="0"
 | 
			
		||||
                max="59"
 | 
			
		||||
                @change="onChange"
 | 
			
		||||
            <input v-model="sec"
 | 
			
		||||
                   :pattern="/\d+/"
 | 
			
		||||
                   type="number"
 | 
			
		||||
                   name="sec"
 | 
			
		||||
                   maxlength="2"
 | 
			
		||||
                   min="0"
 | 
			
		||||
                   max="59"
 | 
			
		||||
                   @change="onChange"
 | 
			
		||||
            >
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="field control timezone">
 | 
			
		||||
 
 | 
			
		||||
@@ -22,30 +22,21 @@
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
<span class="form-control shell">
 | 
			
		||||
    <span
 | 
			
		||||
        class="field control"
 | 
			
		||||
        :class="model.cssClass"
 | 
			
		||||
    <span class="field control"
 | 
			
		||||
          :class="model.cssClass"
 | 
			
		||||
    >
 | 
			
		||||
        <input
 | 
			
		||||
            id="fileElem"
 | 
			
		||||
            ref="fileInput"
 | 
			
		||||
            type="file"
 | 
			
		||||
            accept=".json"
 | 
			
		||||
            style="display:none"
 | 
			
		||||
        <input id="fileElem"
 | 
			
		||||
               ref="fileInput"
 | 
			
		||||
               type="file"
 | 
			
		||||
               accept=".json"
 | 
			
		||||
               style="display:none"
 | 
			
		||||
        >
 | 
			
		||||
        <button
 | 
			
		||||
            id="fileSelect"
 | 
			
		||||
            class="c-button"
 | 
			
		||||
            @click="selectFile"
 | 
			
		||||
        <button id="fileSelect"
 | 
			
		||||
                class="c-button"
 | 
			
		||||
                @click="selectFile"
 | 
			
		||||
        >
 | 
			
		||||
            {{ name }}
 | 
			
		||||
        </button>
 | 
			
		||||
        <button
 | 
			
		||||
            v-if="removable"
 | 
			
		||||
            class="c-button icon-trash"
 | 
			
		||||
            title="Remove file"
 | 
			
		||||
            @click="removeFile"
 | 
			
		||||
        ></button>
 | 
			
		||||
    </span>
 | 
			
		||||
</span>
 | 
			
		||||
</template>
 | 
			
		||||
@@ -69,9 +60,6 @@ export default {
 | 
			
		||||
            const fileInfo = this.fileInfo || this.model.value;
 | 
			
		||||
 | 
			
		||||
            return fileInfo && fileInfo.name || this.model.text;
 | 
			
		||||
        },
 | 
			
		||||
        removable() {
 | 
			
		||||
            return (this.fileInfo || this.model.value) && this.model.removable;
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    mounted() {
 | 
			
		||||
@@ -106,15 +94,6 @@ export default {
 | 
			
		||||
        },
 | 
			
		||||
        selectFile() {
 | 
			
		||||
            this.$refs.fileInput.click();
 | 
			
		||||
        },
 | 
			
		||||
        removeFile() {
 | 
			
		||||
            this.model.value = undefined;
 | 
			
		||||
            this.fileInfo = undefined;
 | 
			
		||||
            const data = {
 | 
			
		||||
                model: this.model,
 | 
			
		||||
                value: undefined
 | 
			
		||||
            };
 | 
			
		||||
            this.$emit('onChange', data);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -22,17 +22,15 @@
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
<span class="form-control shell">
 | 
			
		||||
    <span
 | 
			
		||||
        class="field control"
 | 
			
		||||
        :class="model.cssClass"
 | 
			
		||||
    <span class="field control"
 | 
			
		||||
          :class="model.cssClass"
 | 
			
		||||
    >
 | 
			
		||||
        <input
 | 
			
		||||
            v-model="field"
 | 
			
		||||
            type="number"
 | 
			
		||||
            :min="model.min"
 | 
			
		||||
            :max="model.max"
 | 
			
		||||
            :step="model.step"
 | 
			
		||||
            @input="updateText()"
 | 
			
		||||
        <input v-model="field"
 | 
			
		||||
               type="number"
 | 
			
		||||
               :min="model.min"
 | 
			
		||||
               :max="model.max"
 | 
			
		||||
               :step="model.step"
 | 
			
		||||
               @input="updateText()"
 | 
			
		||||
        >
 | 
			
		||||
    </span>
 | 
			
		||||
</span>
 | 
			
		||||
@@ -58,6 +56,7 @@ export default {
 | 
			
		||||
    },
 | 
			
		||||
    methods: {
 | 
			
		||||
        updateText() {
 | 
			
		||||
            console.log('updateText', this.field);
 | 
			
		||||
            const data = {
 | 
			
		||||
                model: this.model,
 | 
			
		||||
                value: this.field
 | 
			
		||||
 
 | 
			
		||||
@@ -22,16 +22,14 @@
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
<div class="form-control select-field">
 | 
			
		||||
    <select
 | 
			
		||||
        v-model="selected"
 | 
			
		||||
        required="model.required"
 | 
			
		||||
        name="mctControl"
 | 
			
		||||
        @change="onChange($event)"
 | 
			
		||||
    <select v-model="selected"
 | 
			
		||||
            required="model.required"
 | 
			
		||||
            name="mctControl"
 | 
			
		||||
            @change="onChange($event)"
 | 
			
		||||
    >
 | 
			
		||||
        <option
 | 
			
		||||
            v-for="option in model.options"
 | 
			
		||||
            :key="option.name"
 | 
			
		||||
            :value="option.value"
 | 
			
		||||
        <option v-for="option in model.options"
 | 
			
		||||
                :key="option.name"
 | 
			
		||||
                :value="option.value"
 | 
			
		||||
        >
 | 
			
		||||
            {{ option.name }}
 | 
			
		||||
        </option>
 | 
			
		||||
 
 | 
			
		||||
@@ -22,15 +22,13 @@
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
<span class="form-control shell">
 | 
			
		||||
    <span
 | 
			
		||||
        class="field control"
 | 
			
		||||
        :class="model.cssClass"
 | 
			
		||||
    <span class="field control"
 | 
			
		||||
          :class="model.cssClass"
 | 
			
		||||
    >
 | 
			
		||||
        <textarea
 | 
			
		||||
            v-model="field"
 | 
			
		||||
            type="text"
 | 
			
		||||
            :size="model.size"
 | 
			
		||||
            @input="updateText()"
 | 
			
		||||
        <textarea v-model="field"
 | 
			
		||||
                  type="text"
 | 
			
		||||
                  :size="model.size"
 | 
			
		||||
                  @input="updateText()"
 | 
			
		||||
        >
 | 
			
		||||
        </textarea>
 | 
			
		||||
    </span>
 | 
			
		||||
 
 | 
			
		||||
@@ -22,15 +22,13 @@
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
<span class="form-control shell">
 | 
			
		||||
    <span
 | 
			
		||||
        class="field control"
 | 
			
		||||
        :class="model.cssClass"
 | 
			
		||||
    <span class="field control"
 | 
			
		||||
          :class="model.cssClass"
 | 
			
		||||
    >
 | 
			
		||||
        <input
 | 
			
		||||
            v-model="field"
 | 
			
		||||
            type="text"
 | 
			
		||||
            :size="model.size"
 | 
			
		||||
            @input="updateText()"
 | 
			
		||||
        <input v-model="field"
 | 
			
		||||
               type="text"
 | 
			
		||||
               :size="model.size"
 | 
			
		||||
               @input="updateText()"
 | 
			
		||||
        >
 | 
			
		||||
    </span>
 | 
			
		||||
</span>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,62 +0,0 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
* Open MCT, Copyright (c) 2014-2022, United States Government
 | 
			
		||||
* as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
* Administration. All rights reserved.
 | 
			
		||||
*
 | 
			
		||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
* "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
* You may obtain a copy of the License at
 | 
			
		||||
* http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
*
 | 
			
		||||
* Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
* License for the specific language governing permissions and limitations
 | 
			
		||||
* under the License.
 | 
			
		||||
*
 | 
			
		||||
* Open MCT includes source code licensed under additional open source
 | 
			
		||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
* this source code distribution or the Licensing information page available
 | 
			
		||||
* at runtime from the About dialog for additional information.
 | 
			
		||||
*****************************************************************************/
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
<span class="form-control shell">
 | 
			
		||||
    <span
 | 
			
		||||
        class="field control"
 | 
			
		||||
        :class="model.cssClass"
 | 
			
		||||
    >
 | 
			
		||||
        <ToggleSwitch
 | 
			
		||||
            id="switchId"
 | 
			
		||||
            :checked="isChecked"
 | 
			
		||||
            @change="toggleCheckBox"
 | 
			
		||||
        />
 | 
			
		||||
    </span>
 | 
			
		||||
</span>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import toggleMixin from '../../toggle-check-box-mixin';
 | 
			
		||||
import ToggleSwitch from '@/ui/components/ToggleSwitch.vue';
 | 
			
		||||
 | 
			
		||||
import { v4 as uuid } from 'uuid';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    components: {
 | 
			
		||||
        ToggleSwitch
 | 
			
		||||
    },
 | 
			
		||||
    mixins: [toggleMixin],
 | 
			
		||||
    props: {
 | 
			
		||||
        model: {
 | 
			
		||||
            type: Object,
 | 
			
		||||
            required: true
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    data() {
 | 
			
		||||
        return {
 | 
			
		||||
            switchId: `toggleSwitch-${uuid}`,
 | 
			
		||||
            isChecked: this.model.value
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
@@ -1,19 +0,0 @@
 | 
			
		||||
export default {
 | 
			
		||||
    data() {
 | 
			
		||||
        return {
 | 
			
		||||
            isChecked: false
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
    methods: {
 | 
			
		||||
        toggleCheckBox(event) {
 | 
			
		||||
            this.isChecked = !this.isChecked;
 | 
			
		||||
 | 
			
		||||
            const data = {
 | 
			
		||||
                model: this.model,
 | 
			
		||||
                value: this.isChecked
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            this.$emit('onChange', data);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
@@ -26,31 +26,29 @@ import { createOpenMct, createMouseEvent, resetApplicationState } from '../../ut
 | 
			
		||||
 | 
			
		||||
describe ('The Menu API', () => {
 | 
			
		||||
    let openmct;
 | 
			
		||||
    let appHolder;
 | 
			
		||||
    let element;
 | 
			
		||||
    let menuAPI;
 | 
			
		||||
    let actionsArray;
 | 
			
		||||
    let x;
 | 
			
		||||
    let y;
 | 
			
		||||
    let result;
 | 
			
		||||
    let menuElement;
 | 
			
		||||
 | 
			
		||||
    const x = 8;
 | 
			
		||||
    const y = 16;
 | 
			
		||||
 | 
			
		||||
    const menuOptions = {
 | 
			
		||||
        onDestroy: () => {
 | 
			
		||||
            console.log('default onDestroy');
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
    let onDestroy;
 | 
			
		||||
 | 
			
		||||
    beforeEach((done) => {
 | 
			
		||||
        appHolder = document.createElement('div');
 | 
			
		||||
        const appHolder = document.createElement('div');
 | 
			
		||||
        appHolder.style.display = 'block';
 | 
			
		||||
        appHolder.style.width = '1920px';
 | 
			
		||||
        appHolder.style.height = '1080px';
 | 
			
		||||
 | 
			
		||||
        openmct = createOpenMct();
 | 
			
		||||
 | 
			
		||||
        element = document.createElement('div');
 | 
			
		||||
        element.style.display = 'block';
 | 
			
		||||
        element.style.width = '1920px';
 | 
			
		||||
        element.style.height = '1080px';
 | 
			
		||||
 | 
			
		||||
        openmct.on('start', done);
 | 
			
		||||
        openmct.startHeadless();
 | 
			
		||||
        openmct.startHeadless(appHolder);
 | 
			
		||||
 | 
			
		||||
        menuAPI = new MenuAPI(openmct);
 | 
			
		||||
        actionsArray = [
 | 
			
		||||
@@ -58,7 +56,7 @@ describe ('The Menu API', () => {
 | 
			
		||||
                key: 'test-css-class-1',
 | 
			
		||||
                name: 'Test Action 1',
 | 
			
		||||
                cssClass: 'icon-clock',
 | 
			
		||||
                description: 'This is a test action 1',
 | 
			
		||||
                description: 'This is a test action',
 | 
			
		||||
                onItemClicked: () => {
 | 
			
		||||
                    result = 'Test Action 1 Invoked';
 | 
			
		||||
                }
 | 
			
		||||
@@ -67,165 +65,149 @@ describe ('The Menu API', () => {
 | 
			
		||||
                key: 'test-css-class-2',
 | 
			
		||||
                name: 'Test Action 2',
 | 
			
		||||
                cssClass: 'icon-clock',
 | 
			
		||||
                description: 'This is a test action 2',
 | 
			
		||||
                description: 'This is a test action',
 | 
			
		||||
                onItemClicked: () => {
 | 
			
		||||
                    result = 'Test Action 2 Invoked';
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        ];
 | 
			
		||||
        x = 8;
 | 
			
		||||
        y = 16;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(() => {
 | 
			
		||||
        return resetApplicationState(openmct);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('showMenu method', () => {
 | 
			
		||||
        beforeAll(() => {
 | 
			
		||||
            spyOn(menuOptions, 'onDestroy').and.callThrough();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('creates an instance of Menu when invoked', (done) => {
 | 
			
		||||
            menuOptions.onDestroy = done;
 | 
			
		||||
 | 
			
		||||
            menuAPI.showMenu(x, y, actionsArray, menuOptions);
 | 
			
		||||
    describe("showMenu method", () => {
 | 
			
		||||
        it("creates an instance of Menu when invoked", () => {
 | 
			
		||||
            menuAPI.showMenu(x, y, actionsArray);
 | 
			
		||||
 | 
			
		||||
            expect(menuAPI.menuComponent).toBeInstanceOf(Menu);
 | 
			
		||||
            document.body.click();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        describe('creates a menu component', () => {
 | 
			
		||||
            it('with all the actions passed in', (done) => {
 | 
			
		||||
                menuOptions.onDestroy = done;
 | 
			
		||||
        describe("creates a menu component", () => {
 | 
			
		||||
            let menuComponent;
 | 
			
		||||
            let vueComponent;
 | 
			
		||||
 | 
			
		||||
            beforeEach(() => {
 | 
			
		||||
                onDestroy = jasmine.createSpy('onDestroy');
 | 
			
		||||
 | 
			
		||||
                const menuOptions = {
 | 
			
		||||
                    onDestroy
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
                menuAPI.showMenu(x, y, actionsArray, menuOptions);
 | 
			
		||||
                menuElement = document.querySelector('.c-menu');
 | 
			
		||||
                expect(menuElement).toBeDefined();
 | 
			
		||||
                vueComponent = menuAPI.menuComponent.component;
 | 
			
		||||
                menuComponent = document.querySelector(".c-menu");
 | 
			
		||||
 | 
			
		||||
                const listItems = menuElement.children[0].children;
 | 
			
		||||
                spyOn(vueComponent, '$destroy');
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            it("renders a menu component in the expected x and y coordinates", () => {
 | 
			
		||||
                let boundingClientRect = menuComponent.getBoundingClientRect();
 | 
			
		||||
                let left = boundingClientRect.left;
 | 
			
		||||
                let top = boundingClientRect.top;
 | 
			
		||||
 | 
			
		||||
                expect(left).toEqual(x);
 | 
			
		||||
                expect(top).toEqual(y);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            it("with all the actions passed in", () => {
 | 
			
		||||
                expect(menuComponent).toBeDefined();
 | 
			
		||||
 | 
			
		||||
                let listItems = menuComponent.children[0].children;
 | 
			
		||||
 | 
			
		||||
                expect(listItems.length).toEqual(actionsArray.length);
 | 
			
		||||
                document.body.click();
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            it('with click-able menu items, that will invoke the correct callBack', (done) => {
 | 
			
		||||
                menuOptions.onDestroy = done;
 | 
			
		||||
 | 
			
		||||
                menuAPI.showMenu(x, y, actionsArray, menuOptions);
 | 
			
		||||
 | 
			
		||||
                menuElement = document.querySelector('.c-menu');
 | 
			
		||||
                const listItem1 = menuElement.children[0].children[0];
 | 
			
		||||
            it("with click-able menu items, that will invoke the correct callBacks", () => {
 | 
			
		||||
                let listItem1 = menuComponent.children[0].children[0];
 | 
			
		||||
 | 
			
		||||
                listItem1.click();
 | 
			
		||||
 | 
			
		||||
                expect(result).toEqual('Test Action 1 Invoked');
 | 
			
		||||
                expect(result).toEqual("Test Action 1 Invoked");
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            it('dismisses the menu when action is clicked on', (done) => {
 | 
			
		||||
                menuOptions.onDestroy = done;
 | 
			
		||||
            it("dismisses the menu when action is clicked on", () => {
 | 
			
		||||
                let listItem1 = menuComponent.children[0].children[0];
 | 
			
		||||
 | 
			
		||||
                menuAPI.showMenu(x, y, actionsArray, menuOptions);
 | 
			
		||||
 | 
			
		||||
                menuElement = document.querySelector('.c-menu');
 | 
			
		||||
                const listItem1 = menuElement.children[0].children[0];
 | 
			
		||||
                listItem1.click();
 | 
			
		||||
 | 
			
		||||
                menuElement = document.querySelector('.c-menu');
 | 
			
		||||
                let menu = document.querySelector('.c-menu');
 | 
			
		||||
 | 
			
		||||
                expect(menuElement).toBeNull();
 | 
			
		||||
                expect(menu).toBeNull();
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            it('invokes the destroy method when menu is dismissed', (done) => {
 | 
			
		||||
                menuOptions.onDestroy = done;
 | 
			
		||||
 | 
			
		||||
                menuAPI.showMenu(x, y, actionsArray, menuOptions);
 | 
			
		||||
 | 
			
		||||
                const vueComponent = menuAPI.menuComponent.component;
 | 
			
		||||
                spyOn(vueComponent, '$destroy');
 | 
			
		||||
 | 
			
		||||
            it("invokes the destroy method when menu is dismissed", () => {
 | 
			
		||||
                document.body.click();
 | 
			
		||||
 | 
			
		||||
                expect(vueComponent.$destroy).toHaveBeenCalled();
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            it('invokes the onDestroy callback if passed in', (done) => {
 | 
			
		||||
                let count = 0;
 | 
			
		||||
                menuOptions.onDestroy = () => {
 | 
			
		||||
                    count++;
 | 
			
		||||
                    expect(count).toEqual(1);
 | 
			
		||||
                    done();
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
                menuAPI.showMenu(x, y, actionsArray, menuOptions);
 | 
			
		||||
 | 
			
		||||
            it("invokes the onDestroy callback if passed in", () => {
 | 
			
		||||
                document.body.click();
 | 
			
		||||
 | 
			
		||||
                expect(onDestroy).toHaveBeenCalled();
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('superMenu method', () => {
 | 
			
		||||
        it('creates a superMenu', (done) => {
 | 
			
		||||
            menuOptions.onDestroy = done;
 | 
			
		||||
    describe("superMenu method", () => {
 | 
			
		||||
        it("creates a superMenu", () => {
 | 
			
		||||
            menuAPI.showSuperMenu(x, y, actionsArray);
 | 
			
		||||
 | 
			
		||||
            menuAPI.showSuperMenu(x, y, actionsArray, menuOptions);
 | 
			
		||||
            menuElement = document.querySelector('.c-super-menu__menu');
 | 
			
		||||
            const superMenu = document.querySelector('.c-super-menu__menu');
 | 
			
		||||
 | 
			
		||||
            expect(menuElement).not.toBeNull();
 | 
			
		||||
            document.body.click();
 | 
			
		||||
            expect(superMenu).not.toBeNull();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('Mouse over a superMenu shows correct description', (done) => {
 | 
			
		||||
            menuOptions.onDestroy = done;
 | 
			
		||||
        it("Mouse over a superMenu shows correct description", (done) => {
 | 
			
		||||
            menuAPI.showSuperMenu(x, y, actionsArray);
 | 
			
		||||
 | 
			
		||||
            menuAPI.showSuperMenu(x, y, actionsArray, menuOptions);
 | 
			
		||||
            menuElement = document.querySelector('.c-super-menu__menu');
 | 
			
		||||
 | 
			
		||||
            const superMenuItem = menuElement.querySelector('li');
 | 
			
		||||
            const superMenu = document.querySelector('.c-super-menu__menu');
 | 
			
		||||
            const superMenuItem = superMenu.querySelector('li');
 | 
			
		||||
            const mouseOverEvent = createMouseEvent('mouseover');
 | 
			
		||||
 | 
			
		||||
            superMenuItem.dispatchEvent(mouseOverEvent);
 | 
			
		||||
            const itemDescription = document.querySelector('.l-item-description__description');
 | 
			
		||||
 | 
			
		||||
            menuAPI.menuComponent.component.$nextTick(() => {
 | 
			
		||||
                expect(menuElement).not.toBeNull();
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
                expect(itemDescription.innerText).toEqual(actionsArray[0].description);
 | 
			
		||||
 | 
			
		||||
                document.body.click();
 | 
			
		||||
            });
 | 
			
		||||
                expect(superMenu).not.toBeNull();
 | 
			
		||||
                done();
 | 
			
		||||
            }, 300);
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('Menu Placements', () => {
 | 
			
		||||
        it('default menu position BOTTOM_RIGHT', (done) => {
 | 
			
		||||
            menuOptions.onDestroy = done;
 | 
			
		||||
    describe("Menu Placements", () => {
 | 
			
		||||
        it("default menu position BOTTOM_RIGHT", () => {
 | 
			
		||||
            menuAPI.showMenu(x, y, actionsArray);
 | 
			
		||||
 | 
			
		||||
            menuAPI.showMenu(x, y, actionsArray, menuOptions);
 | 
			
		||||
            menuElement = document.querySelector('.c-menu');
 | 
			
		||||
            const menu = document.querySelector('.c-menu');
 | 
			
		||||
 | 
			
		||||
            const boundingClientRect = menuElement.getBoundingClientRect();
 | 
			
		||||
            const boundingClientRect = menu.getBoundingClientRect();
 | 
			
		||||
            const left = boundingClientRect.left;
 | 
			
		||||
            const top = boundingClientRect.top;
 | 
			
		||||
 | 
			
		||||
            expect(left).toEqual(x);
 | 
			
		||||
            expect(top).toEqual(y);
 | 
			
		||||
 | 
			
		||||
            document.body.click();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it('menu position BOTTOM_RIGHT', (done) => {
 | 
			
		||||
            menuOptions.onDestroy = done;
 | 
			
		||||
            menuOptions.placement = openmct.menus.menuPlacement.BOTTOM_RIGHT;
 | 
			
		||||
        it("menu position BOTTOM_RIGHT", () => {
 | 
			
		||||
            const menuOptions = {
 | 
			
		||||
                placement: openmct.menus.menuPlacement.BOTTOM_RIGHT
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            menuAPI.showMenu(x, y, actionsArray, menuOptions);
 | 
			
		||||
            menuElement = document.querySelector('.c-menu');
 | 
			
		||||
 | 
			
		||||
            const boundingClientRect = menuElement.getBoundingClientRect();
 | 
			
		||||
            const menu = document.querySelector('.c-menu');
 | 
			
		||||
            const boundingClientRect = menu.getBoundingClientRect();
 | 
			
		||||
            const left = boundingClientRect.left;
 | 
			
		||||
            const top = boundingClientRect.top;
 | 
			
		||||
 | 
			
		||||
            expect(left).toEqual(x);
 | 
			
		||||
            expect(top).toEqual(y);
 | 
			
		||||
 | 
			
		||||
            document.body.click();
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div
 | 
			
		||||
    class="c-menu"
 | 
			
		||||
    :class="options.menuClass"
 | 
			
		||||
<div class="c-menu"
 | 
			
		||||
     :class="options.menuClass"
 | 
			
		||||
>
 | 
			
		||||
    <ul v-if="options.actions.length && options.actions[0].length">
 | 
			
		||||
        <template
 | 
			
		||||
@@ -12,7 +11,6 @@
 | 
			
		||||
                :key="action.name"
 | 
			
		||||
                :class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
 | 
			
		||||
                :title="action.description"
 | 
			
		||||
                :data-testid="action.testId || false"
 | 
			
		||||
                @click="action.onItemClicked"
 | 
			
		||||
            >
 | 
			
		||||
                {{ action.name }}
 | 
			
		||||
@@ -38,7 +36,6 @@
 | 
			
		||||
            :key="action.name"
 | 
			
		||||
            :class="action.cssClass"
 | 
			
		||||
            :title="action.description"
 | 
			
		||||
            :data-testid="action.testId || false"
 | 
			
		||||
            @click="action.onItemClicked"
 | 
			
		||||
        >
 | 
			
		||||
            {{ action.name }}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,8 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div
 | 
			
		||||
    class="c-menu"
 | 
			
		||||
    :class="[options.menuClass, 'c-super-menu']"
 | 
			
		||||
<div class="c-menu"
 | 
			
		||||
     :class="[options.menuClass, 'c-super-menu']"
 | 
			
		||||
>
 | 
			
		||||
    <ul
 | 
			
		||||
        v-if="options.actions.length && options.actions[0].length"
 | 
			
		||||
    <ul v-if="options.actions.length && options.actions[0].length"
 | 
			
		||||
        class="c-super-menu__menu"
 | 
			
		||||
    >
 | 
			
		||||
        <template
 | 
			
		||||
@@ -15,7 +13,6 @@
 | 
			
		||||
                :key="action.name"
 | 
			
		||||
                :class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
 | 
			
		||||
                :title="action.description"
 | 
			
		||||
                :data-testid="action.testId || false"
 | 
			
		||||
                @click="action.onItemClicked"
 | 
			
		||||
                @mouseover="toggleItemDescription(action)"
 | 
			
		||||
                @mouseleave="toggleItemDescription()"
 | 
			
		||||
@@ -37,8 +34,7 @@
 | 
			
		||||
        </template>
 | 
			
		||||
    </ul>
 | 
			
		||||
 | 
			
		||||
    <ul
 | 
			
		||||
        v-else
 | 
			
		||||
    <ul v-else
 | 
			
		||||
        class="c-super-menu__menu"
 | 
			
		||||
    >
 | 
			
		||||
        <li
 | 
			
		||||
@@ -46,7 +42,6 @@
 | 
			
		||||
            :key="action.name"
 | 
			
		||||
            :class="action.cssClass"
 | 
			
		||||
            :title="action.description"
 | 
			
		||||
            :data-testid="action.testId || false"
 | 
			
		||||
            @click="action.onItemClicked"
 | 
			
		||||
            @mouseover="toggleItemDescription(action)"
 | 
			
		||||
            @mouseleave="toggleItemDescription()"
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,7 @@
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
import { v4 as uuid } from 'uuid';
 | 
			
		||||
import uuid from 'uuid';
 | 
			
		||||
 | 
			
		||||
class InMemorySearchProvider {
 | 
			
		||||
    /**
 | 
			
		||||
@@ -36,14 +36,13 @@ class InMemorySearchProvider {
 | 
			
		||||
         */
 | 
			
		||||
        this.MAX_CONCURRENT_REQUESTS = 100;
 | 
			
		||||
        /**
 | 
			
		||||
         * If max results is not specified in query, use this as default.
 | 
			
		||||
         */
 | 
			
		||||
        * If max results is not specified in query, use this as default.
 | 
			
		||||
        */
 | 
			
		||||
        this.DEFAULT_MAX_RESULTS = 100;
 | 
			
		||||
 | 
			
		||||
        this.openmct = openmct;
 | 
			
		||||
 | 
			
		||||
        this.indexedIds = {};
 | 
			
		||||
        this.indexedCompositions = {};
 | 
			
		||||
        this.idsToIndex = [];
 | 
			
		||||
        this.pendingIndex = {};
 | 
			
		||||
        this.pendingRequests = 0;
 | 
			
		||||
@@ -59,6 +58,7 @@ class InMemorySearchProvider {
 | 
			
		||||
        this.onWorkerMessageError = this.onWorkerMessageError.bind(this);
 | 
			
		||||
        this.onerror = this.onWorkerError.bind(this);
 | 
			
		||||
        this.startIndexing = this.startIndexing.bind(this);
 | 
			
		||||
        this.onMutationOfIndexedObject = this.onMutationOfIndexedObject.bind(this);
 | 
			
		||||
 | 
			
		||||
        this.openmct.on('start', this.startIndexing);
 | 
			
		||||
        this.openmct.on('destroy', () => {
 | 
			
		||||
@@ -68,9 +68,6 @@ class InMemorySearchProvider {
 | 
			
		||||
                this.worker.port.onmessageerror = null;
 | 
			
		||||
                this.worker.port.close();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.destroyObservers(this.indexedIds);
 | 
			
		||||
            this.destroyObservers(this.indexedCompositions);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -140,7 +137,7 @@ class InMemorySearchProvider {
 | 
			
		||||
        };
 | 
			
		||||
        modelResults.hits = await Promise.all(event.data.results.map(async (hit) => {
 | 
			
		||||
            const identifier = this.openmct.objects.parseKeyString(hit.keyString);
 | 
			
		||||
            const domainObject = await this.openmct.objects.get(identifier);
 | 
			
		||||
            const domainObject = await this.openmct.objects.get(identifier.key);
 | 
			
		||||
 | 
			
		||||
            return domainObject;
 | 
			
		||||
        }));
 | 
			
		||||
@@ -216,52 +213,29 @@ class InMemorySearchProvider {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    onNameMutation(domainObject, name) {
 | 
			
		||||
    onMutationOfIndexedObject(domainObject) {
 | 
			
		||||
        const provider = this;
 | 
			
		||||
 | 
			
		||||
        domainObject.name = name;
 | 
			
		||||
        provider.index(domainObject);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    onCompositionMutation(domainObject, composition) {
 | 
			
		||||
        const provider = this;
 | 
			
		||||
        const indexedComposition = domainObject.composition;
 | 
			
		||||
        const identifiersToIndex = composition
 | 
			
		||||
            .filter(identifier => !indexedComposition
 | 
			
		||||
                .some(indexedIdentifier => this.openmct.objects
 | 
			
		||||
                    .areIdsEqual([identifier, indexedIdentifier])));
 | 
			
		||||
 | 
			
		||||
        identifiersToIndex.forEach(identifier => {
 | 
			
		||||
            this.openmct.objects.get(identifier).then(objectToIndex => provider.index(objectToIndex));
 | 
			
		||||
        });
 | 
			
		||||
        provider.index(domainObject.identifier, domainObject);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Pass a domainObject to the worker to be indexed.
 | 
			
		||||
     * If the object has composition, schedule those ids for later indexing.
 | 
			
		||||
     * Watch for object changes and re-index object and children if so
 | 
			
		||||
     * Pass an id and model to the worker to be indexed.  If the model has
 | 
			
		||||
     * composition, schedule those ids for later indexing.
 | 
			
		||||
     *
 | 
			
		||||
     * @private
 | 
			
		||||
     * @param domainObject a domainObject
 | 
			
		||||
     * @param id a model id
 | 
			
		||||
     * @param model a model
 | 
			
		||||
     */
 | 
			
		||||
    async index(domainObject) {
 | 
			
		||||
    async index(id, domainObject) {
 | 
			
		||||
        const provider = this;
 | 
			
		||||
        const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
 | 
			
		||||
 | 
			
		||||
        const keyString = this.openmct.objects.makeKeyString(id);
 | 
			
		||||
        if (!this.indexedIds[keyString]) {
 | 
			
		||||
            this.indexedIds[keyString] = this.openmct.objects.observe(
 | 
			
		||||
                domainObject,
 | 
			
		||||
                'name',
 | 
			
		||||
                this.onNameMutation.bind(this, domainObject)
 | 
			
		||||
            );
 | 
			
		||||
            this.indexedCompositions[keyString] = this.openmct.objects.observe(
 | 
			
		||||
                domainObject,
 | 
			
		||||
                'composition',
 | 
			
		||||
                this.onCompositionMutation.bind(this, domainObject)
 | 
			
		||||
            );
 | 
			
		||||
            this.openmct.objects.observe(domainObject, `*`, this.onMutationOfIndexedObject);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ((keyString !== 'ROOT')) {
 | 
			
		||||
        this.indexedIds[keyString] = true;
 | 
			
		||||
 | 
			
		||||
        if ((id.key !== 'ROOT')) {
 | 
			
		||||
            if (this.worker) {
 | 
			
		||||
                this.worker.port.postMessage({
 | 
			
		||||
                    request: 'index',
 | 
			
		||||
@@ -273,12 +247,15 @@ class InMemorySearchProvider {
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const composition = this.openmct.composition.get(domainObject);
 | 
			
		||||
        const composition = this.openmct.composition.registry.find(foundComposition => {
 | 
			
		||||
            return foundComposition.appliesTo(domainObject);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (composition !== undefined) {
 | 
			
		||||
            const children = await composition.load();
 | 
			
		||||
 | 
			
		||||
            children.forEach(child => provider.scheduleForIndexing(child.identifier));
 | 
			
		||||
        if (composition) {
 | 
			
		||||
            const childIdentifiers = await composition.load(domainObject);
 | 
			
		||||
            childIdentifiers.forEach(function (childIdentifier) {
 | 
			
		||||
                provider.scheduleForIndexing(childIdentifier);
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -294,12 +271,12 @@ class InMemorySearchProvider {
 | 
			
		||||
        const provider = this;
 | 
			
		||||
 | 
			
		||||
        this.pendingRequests += 1;
 | 
			
		||||
        const domainObject = await this.openmct.objects.get(keyString);
 | 
			
		||||
        const identifier = await this.openmct.objects.parseKeyString(keyString);
 | 
			
		||||
        const domainObject = await this.openmct.objects.get(identifier.key);
 | 
			
		||||
        delete provider.pendingIndex[keyString];
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            if (domainObject) {
 | 
			
		||||
                await provider.index(domainObject);
 | 
			
		||||
                await provider.index(identifier, domainObject);
 | 
			
		||||
            }
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.warn('Failed to index domain object ' + keyString, error);
 | 
			
		||||
@@ -328,9 +305,9 @@ class InMemorySearchProvider {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * A local version of the same SharedWorker function
 | 
			
		||||
     * if we don't have SharedWorkers available (e.g., iOS)
 | 
			
		||||
     */
 | 
			
		||||
    * A local version of the same SharedWorker function
 | 
			
		||||
    * if we don't have SharedWorkers available (e.g., iOS)
 | 
			
		||||
    */
 | 
			
		||||
    localIndexItem(keyString, model) {
 | 
			
		||||
        this.localIndexedItems[keyString] = {
 | 
			
		||||
            type: model.type,
 | 
			
		||||
@@ -370,16 +347,6 @@ class InMemorySearchProvider {
 | 
			
		||||
        };
 | 
			
		||||
        this.onWorkerMessage(eventToReturn);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    destroyObservers(observers) {
 | 
			
		||||
        Object.entries(observers).forEach(([keyString, unobserve]) => {
 | 
			
		||||
            if (typeof unobserve === 'function') {
 | 
			
		||||
                unobserve();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            delete observers[keyString];
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default InMemorySearchProvider;
 | 
			
		||||
 
 | 
			
		||||
@@ -105,18 +105,13 @@ describe("The Object API Search Function", () => {
 | 
			
		||||
 | 
			
		||||
        beforeEach((done) => {
 | 
			
		||||
            openmct = createOpenMct();
 | 
			
		||||
            const defaultObjectProvider = openmct.objects.getProvider({
 | 
			
		||||
                key: '',
 | 
			
		||||
                namespace: ''
 | 
			
		||||
            });
 | 
			
		||||
            openmct.objects.addProvider('foo', defaultObjectProvider);
 | 
			
		||||
            spyOn(openmct.objects.inMemorySearchProvider, "query").and.callThrough();
 | 
			
		||||
            spyOn(openmct.objects.inMemorySearchProvider, "localSearch").and.callThrough();
 | 
			
		||||
 | 
			
		||||
            openmct.on('start', async () => {
 | 
			
		||||
                mockIdentifier1 = {
 | 
			
		||||
                    key: 'some-object',
 | 
			
		||||
                    namespace: 'foo'
 | 
			
		||||
                    namespace: 'some-namespace'
 | 
			
		||||
                };
 | 
			
		||||
                mockDomainObject1 = {
 | 
			
		||||
                    type: 'clock',
 | 
			
		||||
@@ -125,7 +120,7 @@ describe("The Object API Search Function", () => {
 | 
			
		||||
                };
 | 
			
		||||
                mockIdentifier2 = {
 | 
			
		||||
                    key: 'some-other-object',
 | 
			
		||||
                    namespace: 'foo'
 | 
			
		||||
                    namespace: 'some-namespace'
 | 
			
		||||
                };
 | 
			
		||||
                mockDomainObject2 = {
 | 
			
		||||
                    type: 'clock',
 | 
			
		||||
@@ -134,16 +129,16 @@ describe("The Object API Search Function", () => {
 | 
			
		||||
                };
 | 
			
		||||
                mockIdentifier3 = {
 | 
			
		||||
                    key: 'yet-another-object',
 | 
			
		||||
                    namespace: 'foo'
 | 
			
		||||
                    namespace: 'some-namespace'
 | 
			
		||||
                };
 | 
			
		||||
                mockDomainObject3 = {
 | 
			
		||||
                    type: 'clock',
 | 
			
		||||
                    name: 'redBear',
 | 
			
		||||
                    identifier: mockIdentifier3
 | 
			
		||||
                };
 | 
			
		||||
                await openmct.objects.inMemorySearchProvider.index(mockDomainObject1);
 | 
			
		||||
                await openmct.objects.inMemorySearchProvider.index(mockDomainObject2);
 | 
			
		||||
                await openmct.objects.inMemorySearchProvider.index(mockDomainObject3);
 | 
			
		||||
                await openmct.objects.inMemorySearchProvider.index(mockIdentifier1, mockDomainObject1);
 | 
			
		||||
                await openmct.objects.inMemorySearchProvider.index(mockIdentifier2, mockDomainObject2);
 | 
			
		||||
                await openmct.objects.inMemorySearchProvider.index(mockIdentifier3, mockDomainObject3);
 | 
			
		||||
                done();
 | 
			
		||||
            });
 | 
			
		||||
            openmct.startHeadless();
 | 
			
		||||
@@ -180,9 +175,9 @@ describe("The Object API Search Function", () => {
 | 
			
		||||
            beforeEach(async () => {
 | 
			
		||||
                openmct.objects.inMemorySearchProvider.worker = null;
 | 
			
		||||
                // reindex locally
 | 
			
		||||
                await openmct.objects.inMemorySearchProvider.index(mockDomainObject1);
 | 
			
		||||
                await openmct.objects.inMemorySearchProvider.index(mockDomainObject2);
 | 
			
		||||
                await openmct.objects.inMemorySearchProvider.index(mockDomainObject3);
 | 
			
		||||
                await openmct.objects.inMemorySearchProvider.index(mockIdentifier1, mockDomainObject1);
 | 
			
		||||
                await openmct.objects.inMemorySearchProvider.index(mockIdentifier2, mockDomainObject2);
 | 
			
		||||
                await openmct.objects.inMemorySearchProvider.index(mockIdentifier3, mockDomainObject3);
 | 
			
		||||
            });
 | 
			
		||||
            it("calls local search", () => {
 | 
			
		||||
                openmct.objects.search('foo');
 | 
			
		||||
 
 | 
			
		||||
@@ -22,14 +22,12 @@
 | 
			
		||||
 | 
			
		||||
export default class Transaction {
 | 
			
		||||
    constructor(objectAPI) {
 | 
			
		||||
        this.dirtyObjects = {};
 | 
			
		||||
        this.dirtyObjects = new Set();
 | 
			
		||||
        this.objectAPI = objectAPI;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    add(object) {
 | 
			
		||||
        const key = this.objectAPI.makeKeyString(object.identifier);
 | 
			
		||||
 | 
			
		||||
        this.dirtyObjects[key] = object;
 | 
			
		||||
        this.dirtyObjects.add(object);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    cancel() {
 | 
			
		||||
@@ -39,8 +37,7 @@ export default class Transaction {
 | 
			
		||||
    commit() {
 | 
			
		||||
        const promiseArray = [];
 | 
			
		||||
        const save = this.objectAPI.save.bind(this.objectAPI);
 | 
			
		||||
 | 
			
		||||
        Object.values(this.dirtyObjects).forEach(object => {
 | 
			
		||||
        this.dirtyObjects.forEach(object => {
 | 
			
		||||
            promiseArray.push(this.createDirtyObjectPromise(object, save));
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
@@ -51,9 +48,7 @@ export default class Transaction {
 | 
			
		||||
        return new Promise((resolve, reject) => {
 | 
			
		||||
            action(object)
 | 
			
		||||
                .then((success) => {
 | 
			
		||||
                    const key = this.objectAPI.makeKeyString(object.identifier);
 | 
			
		||||
 | 
			
		||||
                    delete this.dirtyObjects[key];
 | 
			
		||||
                    this.dirtyObjects.delete(object);
 | 
			
		||||
                    resolve(success);
 | 
			
		||||
                })
 | 
			
		||||
                .catch(reject);
 | 
			
		||||
@@ -62,8 +57,7 @@ export default class Transaction {
 | 
			
		||||
 | 
			
		||||
    getDirtyObject(identifier) {
 | 
			
		||||
        let dirtyObject;
 | 
			
		||||
 | 
			
		||||
        Object.values(this.dirtyObjects).forEach(object => {
 | 
			
		||||
        this.dirtyObjects.forEach(object => {
 | 
			
		||||
            const areIdsEqual = this.objectAPI.areIdsEqual(object.identifier, identifier);
 | 
			
		||||
            if (areIdsEqual) {
 | 
			
		||||
                dirtyObject = object;
 | 
			
		||||
@@ -73,11 +67,14 @@ export default class Transaction {
 | 
			
		||||
        return dirtyObject;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    start() {
 | 
			
		||||
        this.dirtyObjects = new Set();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _clear() {
 | 
			
		||||
        const promiseArray = [];
 | 
			
		||||
        const refresh = this.objectAPI.refresh.bind(this.objectAPI);
 | 
			
		||||
 | 
			
		||||
        Object.values(this.dirtyObjects).forEach(object => {
 | 
			
		||||
        this.dirtyObjects.forEach(object => {
 | 
			
		||||
            promiseArray.push(this.createDirtyObjectPromise(object, refresh));
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -34,24 +34,24 @@ describe("Transaction Class", () => {
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('has no dirty objects', () => {
 | 
			
		||||
        expect(Object.keys(transaction.dirtyObjects).length).toEqual(0);
 | 
			
		||||
        expect(transaction.dirtyObjects.size).toEqual(0);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('add(), adds object to dirtyObjects', () => {
 | 
			
		||||
        const mockDomainObjects = createMockDomainObjects();
 | 
			
		||||
        transaction.add(mockDomainObjects[0]);
 | 
			
		||||
        expect(Object.keys(transaction.dirtyObjects).length).toEqual(1);
 | 
			
		||||
        expect(transaction.dirtyObjects.size).toEqual(1);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('cancel(), clears all dirtyObjects', (done) => {
 | 
			
		||||
        const mockDomainObjects = createMockDomainObjects(3);
 | 
			
		||||
        mockDomainObjects.forEach(transaction.add.bind(transaction));
 | 
			
		||||
 | 
			
		||||
        expect(Object.keys(transaction.dirtyObjects).length).toEqual(3);
 | 
			
		||||
        expect(transaction.dirtyObjects.size).toEqual(3);
 | 
			
		||||
 | 
			
		||||
        transaction.cancel()
 | 
			
		||||
            .then(success => {
 | 
			
		||||
                expect(Object.keys(transaction.dirtyObjects).length).toEqual(0);
 | 
			
		||||
                expect(transaction.dirtyObjects.size).toEqual(0);
 | 
			
		||||
            }).finally(done);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
@@ -59,12 +59,12 @@ describe("Transaction Class", () => {
 | 
			
		||||
        const mockDomainObjects = createMockDomainObjects(3);
 | 
			
		||||
        mockDomainObjects.forEach(transaction.add.bind(transaction));
 | 
			
		||||
 | 
			
		||||
        expect(Object.keys(transaction.dirtyObjects).length).toEqual(3);
 | 
			
		||||
        expect(transaction.dirtyObjects.size).toEqual(3);
 | 
			
		||||
        spyOn(objectAPI, 'save').and.callThrough();
 | 
			
		||||
 | 
			
		||||
        transaction.commit()
 | 
			
		||||
            .then(success => {
 | 
			
		||||
                expect(Object.keys(transaction.dirtyObjects).length).toEqual(0);
 | 
			
		||||
                expect(transaction.dirtyObjects.size).toEqual(0);
 | 
			
		||||
                expect(objectAPI.save.calls.count()).toEqual(3);
 | 
			
		||||
            }).finally(done);
 | 
			
		||||
    });
 | 
			
		||||
@@ -73,7 +73,7 @@ describe("Transaction Class", () => {
 | 
			
		||||
        const mockDomainObjects = createMockDomainObjects();
 | 
			
		||||
        transaction.add(mockDomainObjects[0]);
 | 
			
		||||
 | 
			
		||||
        expect(Object.keys(transaction.dirtyObjects).length).toEqual(1);
 | 
			
		||||
        expect(transaction.dirtyObjects.size).toEqual(1);
 | 
			
		||||
        const dirtyObject = transaction.getDirtyObject(mockDomainObjects[0].identifier);
 | 
			
		||||
 | 
			
		||||
        expect(dirtyObject).toEqual(mockDomainObjects[0]);
 | 
			
		||||
@@ -82,7 +82,7 @@ describe("Transaction Class", () => {
 | 
			
		||||
    it('getDirtyObject(), returns empty dirtyObject for no active transaction', () => {
 | 
			
		||||
        const mockDomainObjects = createMockDomainObjects();
 | 
			
		||||
 | 
			
		||||
        expect(Object.keys(transaction.dirtyObjects).length).toEqual(0);
 | 
			
		||||
        expect(transaction.dirtyObjects.size).toEqual(0);
 | 
			
		||||
        const dirtyObject = transaction.getDirtyObject(mockDomainObjects[0].identifier);
 | 
			
		||||
 | 
			
		||||
        expect(dirtyObject).toEqual(undefined);
 | 
			
		||||
 
 | 
			
		||||
@@ -512,7 +512,7 @@ define([
 | 
			
		||||
    TelemetryAPI.prototype.handleMissingRequestProvider = function (domainObject) {
 | 
			
		||||
        this.noRequestProviderForAllObjects = this.requestProviders.every(requestProvider => {
 | 
			
		||||
            const supportsRequest = requestProvider.supportsRequest.apply(requestProvider, arguments);
 | 
			
		||||
            const hasRequestProvider = Object.prototype.hasOwnProperty.call(requestProvider, 'request') && typeof requestProvider.request === 'function';
 | 
			
		||||
            const hasRequestProvider = Object.hasOwn(requestProvider, 'request');
 | 
			
		||||
 | 
			
		||||
            return supportsRequest && hasRequestProvider;
 | 
			
		||||
        });
 | 
			
		||||
 
 | 
			
		||||
@@ -22,7 +22,11 @@
 | 
			
		||||
 | 
			
		||||
import _ from 'lodash';
 | 
			
		||||
import EventEmitter from 'EventEmitter';
 | 
			
		||||
import { LOADED_ERROR, TIMESYSTEM_KEY_NOTIFICATION, TIMESYSTEM_KEY_WARNING } from './constants';
 | 
			
		||||
 | 
			
		||||
const ERRORS = {
 | 
			
		||||
    TIMESYSTEM_KEY: 'All telemetry metadata must have a telemetry value with a key that matches the key of the active time system.',
 | 
			
		||||
    LOADED: 'Telemetry Collection has already been loaded.'
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** Class representing a Telemetry Collection. */
 | 
			
		||||
 | 
			
		||||
@@ -57,7 +61,7 @@ export class TelemetryCollection extends EventEmitter {
 | 
			
		||||
     */
 | 
			
		||||
    load() {
 | 
			
		||||
        if (this.loaded) {
 | 
			
		||||
            this._error(LOADED_ERROR);
 | 
			
		||||
            this._error(ERRORS.LOADED);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this._setTimeSystem(this.openmct.time.timeSystem());
 | 
			
		||||
@@ -168,7 +172,6 @@ export class TelemetryCollection extends EventEmitter {
 | 
			
		||||
     * @private
 | 
			
		||||
     */
 | 
			
		||||
    _processNewTelemetry(telemetryData) {
 | 
			
		||||
        performance.mark('tlm:process:start');
 | 
			
		||||
        if (telemetryData === undefined) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
@@ -263,10 +266,6 @@ export class TelemetryCollection extends EventEmitter {
 | 
			
		||||
        this.lastBounds = bounds;
 | 
			
		||||
 | 
			
		||||
        if (isTick) {
 | 
			
		||||
            if (this.timeKey === undefined) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // need to check futureBuffer and need to check
 | 
			
		||||
            // if anything has fallen out of bounds
 | 
			
		||||
            let startIndex = 0;
 | 
			
		||||
@@ -306,6 +305,7 @@ export class TelemetryCollection extends EventEmitter {
 | 
			
		||||
            if (added.length > 0) {
 | 
			
		||||
                this.emit('add', added);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        } else {
 | 
			
		||||
            // user bounds change, reset
 | 
			
		||||
            this._reset();
 | 
			
		||||
@@ -325,16 +325,12 @@ export class TelemetryCollection extends EventEmitter {
 | 
			
		||||
        let domains = this.metadata.valuesForHints(['domain']);
 | 
			
		||||
        let domain = domains.find((d) => d.key === timeSystem.key);
 | 
			
		||||
 | 
			
		||||
        if (domain !== undefined) {
 | 
			
		||||
            // timeKey is used to create a dummy datum used for sorting
 | 
			
		||||
            this.timeKey = domain.source;
 | 
			
		||||
        } else {
 | 
			
		||||
            this.timeKey = undefined;
 | 
			
		||||
 | 
			
		||||
            this._warn(TIMESYSTEM_KEY_WARNING);
 | 
			
		||||
            this.openmct.notifications.alert(TIMESYSTEM_KEY_NOTIFICATION);
 | 
			
		||||
        if (domain === undefined) {
 | 
			
		||||
            this._error(ERRORS.TIMESYSTEM_KEY);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // timeKey is used to create a dummy datum used for sorting
 | 
			
		||||
        this.timeKey = domain.source; // this defaults to key if no source is set
 | 
			
		||||
        let metadataValue = this.metadata.value(timeSystem.key) || { format: timeSystem.key };
 | 
			
		||||
        let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
 | 
			
		||||
 | 
			
		||||
@@ -356,7 +352,6 @@ export class TelemetryCollection extends EventEmitter {
 | 
			
		||||
     * @todo handle subscriptions more granually
 | 
			
		||||
     */
 | 
			
		||||
    _reset() {
 | 
			
		||||
        performance.mark('tlm:reset');
 | 
			
		||||
        this.boundedTelemetry = [];
 | 
			
		||||
        this.futureBuffer = [];
 | 
			
		||||
 | 
			
		||||
@@ -405,8 +400,4 @@ export class TelemetryCollection extends EventEmitter {
 | 
			
		||||
    _error(message) {
 | 
			
		||||
        throw new Error(message);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _warn(message) {
 | 
			
		||||
        console.warn(message);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,101 +0,0 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2022, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
    createOpenMct,
 | 
			
		||||
    resetApplicationState
 | 
			
		||||
} from 'utils/testing';
 | 
			
		||||
import { TIMESYSTEM_KEY_WARNING } from './constants';
 | 
			
		||||
 | 
			
		||||
describe('Telemetry Collection', () => {
 | 
			
		||||
    let openmct;
 | 
			
		||||
    let mockMetadataProvider;
 | 
			
		||||
    let mockMetadata = {};
 | 
			
		||||
    let domainObject;
 | 
			
		||||
 | 
			
		||||
    beforeEach(done => {
 | 
			
		||||
        openmct = createOpenMct();
 | 
			
		||||
        openmct.on('start', done);
 | 
			
		||||
 | 
			
		||||
        domainObject = {
 | 
			
		||||
            identifier: {
 | 
			
		||||
                key: 'a',
 | 
			
		||||
                namespace: 'b'
 | 
			
		||||
            },
 | 
			
		||||
            type: 'sample-type'
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        mockMetadataProvider = {
 | 
			
		||||
            key: 'mockMetadataProvider',
 | 
			
		||||
            supportsMetadata() {
 | 
			
		||||
                return true;
 | 
			
		||||
            },
 | 
			
		||||
            getMetadata() {
 | 
			
		||||
                return mockMetadata;
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        openmct.telemetry.addProvider(mockMetadataProvider);
 | 
			
		||||
        openmct.startHeadless();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(() => {
 | 
			
		||||
        return resetApplicationState();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('Warns if telemetry metadata does not match the active timesystem', () => {
 | 
			
		||||
        mockMetadata.values = [
 | 
			
		||||
            {
 | 
			
		||||
                key: 'foo',
 | 
			
		||||
                name: 'Bar',
 | 
			
		||||
                hints: {
 | 
			
		||||
                    domain: 1
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        const telemetryCollection = openmct.telemetry.requestCollection(domainObject);
 | 
			
		||||
        spyOn(telemetryCollection, '_warn');
 | 
			
		||||
        telemetryCollection.load();
 | 
			
		||||
 | 
			
		||||
        expect(telemetryCollection._warn).toHaveBeenCalledOnceWith(TIMESYSTEM_KEY_WARNING);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('Does not warn if telemetry metadata matches the active timesystem', () => {
 | 
			
		||||
        mockMetadata.values = [
 | 
			
		||||
            {
 | 
			
		||||
                key: 'utc',
 | 
			
		||||
                name: 'Timestamp',
 | 
			
		||||
                format: 'utc',
 | 
			
		||||
                hints: {
 | 
			
		||||
                    domain: 1
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        const telemetryCollection = openmct.telemetry.requestCollection(domainObject);
 | 
			
		||||
        spyOn(telemetryCollection, '_warn');
 | 
			
		||||
        telemetryCollection.load();
 | 
			
		||||
 | 
			
		||||
        expect(telemetryCollection._warn).not.toHaveBeenCalled();
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@@ -1,25 +0,0 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2022, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
export const TIMESYSTEM_KEY_WARNING = 'All telemetry metadata must have a telemetry value with a key that matches the key of the active time system.';
 | 
			
		||||
export const TIMESYSTEM_KEY_NOTIFICATION = 'Telemetry metadata does not match the active time system.';
 | 
			
		||||
export const LOADED_ERROR = 'Telemetry Collection has already been loaded.';
 | 
			
		||||
@@ -365,7 +365,3 @@ class TimeContext extends EventEmitter {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default TimeContext;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@typedef {{start: number, end: number}} Bounds
 | 
			
		||||
*/
 | 
			
		||||
 
 | 
			
		||||
@@ -40,13 +40,11 @@ describe("The User API", () => {
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(() => {
 | 
			
		||||
        const activeOverlays = openmct.overlays.activeOverlays;
 | 
			
		||||
        activeOverlays.forEach(overlay => overlay.dismiss());
 | 
			
		||||
 | 
			
		||||
        return resetApplicationState(openmct);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('with regard to user providers', () => {
 | 
			
		||||
 | 
			
		||||
        it('allows you to specify a user provider', () => {
 | 
			
		||||
            openmct.user.on('providerAdded', (provider) => {
 | 
			
		||||
                expect(provider).toBeInstanceOf(ExampleUserProvider);
 | 
			
		||||
 
 | 
			
		||||
@@ -33,7 +33,7 @@ function replaceDotsWithUnderscores(filename) {
 | 
			
		||||
 | 
			
		||||
import {saveAs} from 'saveAs';
 | 
			
		||||
import html2canvas from 'html2canvas';
 | 
			
		||||
import { v4 as uuid } from 'uuid';
 | 
			
		||||
import uuid from 'uuid';
 | 
			
		||||
 | 
			
		||||
class ImageExporter {
 | 
			
		||||
    constructor(openmct) {
 | 
			
		||||
@@ -51,7 +51,7 @@ class ImageExporter {
 | 
			
		||||
        const overlays = this.openmct.overlays;
 | 
			
		||||
        const dialog = overlays.dialog({
 | 
			
		||||
            iconClass: 'info',
 | 
			
		||||
            message: 'Capturing image, please wait...',
 | 
			
		||||
            message: 'Caputuring an image',
 | 
			
		||||
            buttons: [
 | 
			
		||||
                {
 | 
			
		||||
                    label: 'Cancel',
 | 
			
		||||
 
 | 
			
		||||
@@ -15,8 +15,7 @@ export default function (folderName, couchPlugin, searchFilter) {
 | 
			
		||||
                    return Promise.resolve({
 | 
			
		||||
                        identifier,
 | 
			
		||||
                        type: 'folder',
 | 
			
		||||
                        name: folderName || "CouchDB Documents",
 | 
			
		||||
                        location: 'ROOT'
 | 
			
		||||
                        name: folderName || "CouchDB Documents"
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
@@ -85,8 +85,7 @@ describe('the plugin', function () {
 | 
			
		||||
            expect(object).toEqual({
 | 
			
		||||
                identifier,
 | 
			
		||||
                type: 'folder',
 | 
			
		||||
                name: 'CouchDB Documents',
 | 
			
		||||
                location: 'ROOT'
 | 
			
		||||
                name: "CouchDB Documents"
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 
 | 
			
		||||
@@ -52,6 +52,7 @@ export default (agent, document) => {
 | 
			
		||||
    if (agent.isMobile()) {
 | 
			
		||||
        const mediaQuery = window.matchMedia("(orientation: landscape)");
 | 
			
		||||
        function eventHandler(event) {
 | 
			
		||||
            console.log("changed");
 | 
			
		||||
            if (event.matches) {
 | 
			
		||||
                body.classList.remove("portrait");
 | 
			
		||||
                body.classList.add("landscape");
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,4 @@
 | 
			
		||||
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2022, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
@@ -113,12 +114,14 @@ export default {
 | 
			
		||||
        this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
 | 
			
		||||
        this.formats = this.openmct.telemetry.getFormatMap(this.metadata);
 | 
			
		||||
        this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
 | 
			
		||||
        this.bounds = this.openmct.time.bounds();
 | 
			
		||||
 | 
			
		||||
        this.limitEvaluator = this.openmct
 | 
			
		||||
            .telemetry
 | 
			
		||||
            .limitEvaluator(this.domainObject);
 | 
			
		||||
 | 
			
		||||
        this.openmct.time.on('timeSystem', this.updateTimeSystem);
 | 
			
		||||
        this.openmct.time.on('bounds', this.updateBounds);
 | 
			
		||||
 | 
			
		||||
        this.timestampKey = this.openmct.time.timeSystem().key;
 | 
			
		||||
 | 
			
		||||
@@ -132,41 +135,72 @@ export default {
 | 
			
		||||
 | 
			
		||||
        this.valueKey = this.valueMetadata ? this.valueMetadata.key : undefined;
 | 
			
		||||
 | 
			
		||||
        this.telemetryCollection = this.openmct.telemetry.requestCollection(this.domainObject, {
 | 
			
		||||
            size: 1,
 | 
			
		||||
            strategy: 'latest'
 | 
			
		||||
        });
 | 
			
		||||
        this.telemetryCollection.on('add', this.setLatestValues);
 | 
			
		||||
        this.telemetryCollection.on('clear', this.resetValues);
 | 
			
		||||
        this.telemetryCollection.load();
 | 
			
		||||
        this.unsubscribe = this.openmct
 | 
			
		||||
            .telemetry
 | 
			
		||||
            .subscribe(this.domainObject, this.setLatestValues);
 | 
			
		||||
 | 
			
		||||
        this.requestHistory();
 | 
			
		||||
 | 
			
		||||
        if (this.hasUnits) {
 | 
			
		||||
            this.setUnit();
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    destroyed() {
 | 
			
		||||
        this.unsubscribe();
 | 
			
		||||
        this.openmct.time.off('timeSystem', this.updateTimeSystem);
 | 
			
		||||
        this.telemetryCollection.off('add', this.setLatestValues);
 | 
			
		||||
        this.telemetryCollection.off('clear', this.resetValues);
 | 
			
		||||
 | 
			
		||||
        this.telemetryCollection.destroy();
 | 
			
		||||
        this.openmct.time.off('bounds', this.updateBounds);
 | 
			
		||||
    },
 | 
			
		||||
    methods: {
 | 
			
		||||
        updateView() {
 | 
			
		||||
            if (!this.updatingView) {
 | 
			
		||||
                this.updatingView = true;
 | 
			
		||||
                requestAnimationFrame(() => {
 | 
			
		||||
                    this.timestamp = this.getParsedTimestamp(this.latestDatum);
 | 
			
		||||
                    this.datum = this.latestDatum;
 | 
			
		||||
                    let newTimestamp = this.getParsedTimestamp(this.latestDatum);
 | 
			
		||||
 | 
			
		||||
                    if (this.shouldUpdate(newTimestamp)) {
 | 
			
		||||
                        this.timestamp = newTimestamp;
 | 
			
		||||
                        this.datum = this.latestDatum;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    this.updatingView = false;
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        setLatestValues(data) {
 | 
			
		||||
            this.latestDatum = data[data.length - 1];
 | 
			
		||||
        setLatestValues(datum) {
 | 
			
		||||
            this.latestDatum = datum;
 | 
			
		||||
 | 
			
		||||
            this.updateView();
 | 
			
		||||
        },
 | 
			
		||||
        shouldUpdate(newTimestamp) {
 | 
			
		||||
            return this.inBounds(newTimestamp)
 | 
			
		||||
                && (this.timestamp === undefined || newTimestamp > this.timestamp);
 | 
			
		||||
        },
 | 
			
		||||
        requestHistory() {
 | 
			
		||||
            this.openmct
 | 
			
		||||
                .telemetry
 | 
			
		||||
                .request(this.domainObject, {
 | 
			
		||||
                    start: this.bounds.start,
 | 
			
		||||
                    end: this.bounds.end,
 | 
			
		||||
                    size: 1,
 | 
			
		||||
                    strategy: 'latest'
 | 
			
		||||
                })
 | 
			
		||||
                .then((array) => this.setLatestValues(array[array.length - 1]))
 | 
			
		||||
                .catch((error) => {
 | 
			
		||||
                    console.warn('Error fetching data', error);
 | 
			
		||||
                });
 | 
			
		||||
        },
 | 
			
		||||
        updateBounds(bounds, isTick) {
 | 
			
		||||
            this.bounds = bounds;
 | 
			
		||||
            if (!isTick) {
 | 
			
		||||
                this.resetValues();
 | 
			
		||||
                this.requestHistory();
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        inBounds(timestamp) {
 | 
			
		||||
            return timestamp >= this.bounds.start && timestamp <= this.bounds.end;
 | 
			
		||||
        },
 | 
			
		||||
        updateTimeSystem(timeSystem) {
 | 
			
		||||
            this.resetValues();
 | 
			
		||||
            this.timestampKey = timeSystem.key;
 | 
			
		||||
        },
 | 
			
		||||
        updateViewContext() {
 | 
			
		||||
@@ -207,3 +241,4 @@ export default {
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -46,7 +46,6 @@ describe("The LAD Table", () => {
 | 
			
		||||
 | 
			
		||||
    let openmct;
 | 
			
		||||
    let ladPlugin;
 | 
			
		||||
    let historicalProvider;
 | 
			
		||||
    let parent;
 | 
			
		||||
    let child;
 | 
			
		||||
    let telemetryCount = 3;
 | 
			
		||||
@@ -82,13 +81,6 @@ describe("The LAD Table", () => {
 | 
			
		||||
 | 
			
		||||
        spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({}));
 | 
			
		||||
 | 
			
		||||
        historicalProvider = {
 | 
			
		||||
            request: () => {
 | 
			
		||||
                return Promise.resolve([]);
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
        spyOn(openmct.telemetry, 'findRequestProvider').and.returnValue(historicalProvider);
 | 
			
		||||
 | 
			
		||||
        openmct.time.bounds({
 | 
			
		||||
            start: bounds.start,
 | 
			
		||||
            end: bounds.end
 | 
			
		||||
@@ -155,7 +147,7 @@ describe("The LAD Table", () => {
 | 
			
		||||
        // add another telemetry object as composition in lad table to test multi rows
 | 
			
		||||
        mockObj.ladTable.composition.push(anotherTelemetryObj.identifier);
 | 
			
		||||
 | 
			
		||||
        beforeEach(async (done) => {
 | 
			
		||||
        beforeEach(async () => {
 | 
			
		||||
            let telemetryRequestResolve;
 | 
			
		||||
            let telemetryObjectResolve;
 | 
			
		||||
            let anotherTelemetryObjectResolve;
 | 
			
		||||
@@ -174,12 +166,11 @@ describe("The LAD Table", () => {
 | 
			
		||||
                callBack();
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            historicalProvider.request = () => {
 | 
			
		||||
            openmct.telemetry.request.and.callFake(() => {
 | 
			
		||||
                telemetryRequestResolve(mockTelemetry);
 | 
			
		||||
 | 
			
		||||
                return telemetryRequestPromise;
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            });
 | 
			
		||||
            openmct.objects.get.and.callFake((obj) => {
 | 
			
		||||
                if (obj.key === 'telemetry-object') {
 | 
			
		||||
                    telemetryObjectResolve(mockObj.telemetry);
 | 
			
		||||
@@ -204,8 +195,6 @@ describe("The LAD Table", () => {
 | 
			
		||||
 | 
			
		||||
            await Promise.all([telemetryRequestPromise, telemetryObjectPromise, anotherTelemetryObjectPromise]);
 | 
			
		||||
            await Vue.nextTick();
 | 
			
		||||
 | 
			
		||||
            done();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("should show one row per object in the composition", () => {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,34 +1,28 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div
 | 
			
		||||
    ref="plotWrapper"
 | 
			
		||||
    class="has-local-controls"
 | 
			
		||||
    :class="{ 's-unsynced' : isZoomed }"
 | 
			
		||||
<div ref="plotWrapper"
 | 
			
		||||
     class="has-local-controls"
 | 
			
		||||
     :class="{ 's-unsynced' : isZoomed }"
 | 
			
		||||
>
 | 
			
		||||
    <div
 | 
			
		||||
        v-if="isZoomed"
 | 
			
		||||
        class="l-state-indicators"
 | 
			
		||||
    <div v-if="isZoomed"
 | 
			
		||||
         class="l-state-indicators"
 | 
			
		||||
    >
 | 
			
		||||
        <span
 | 
			
		||||
            class="l-state-indicators__alert-no-lad t-object-alert t-alert-unsynced icon-alert-triangle"
 | 
			
		||||
            title="This plot is not currently displaying the latest data. Reset pan/zoom to view latest data."
 | 
			
		||||
        <span class="l-state-indicators__alert-no-lad t-object-alert t-alert-unsynced icon-alert-triangle"
 | 
			
		||||
              title="This plot is not currently displaying the latest data. Reset pan/zoom to view latest data."
 | 
			
		||||
        ></span>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div
 | 
			
		||||
        ref="plot"
 | 
			
		||||
        class="c-bar-chart"
 | 
			
		||||
        @plotly_relayout="zoom"
 | 
			
		||||
    <div ref="plot"
 | 
			
		||||
         class="c-bar-chart"
 | 
			
		||||
         @plotly_relayout="zoom"
 | 
			
		||||
    ></div>
 | 
			
		||||
    <div
 | 
			
		||||
        v-if="false"
 | 
			
		||||
        ref="localControl"
 | 
			
		||||
        class="gl-plot__local-controls h-local-controls h-local-controls--overlay-content c-local-controls--show-on-hover"
 | 
			
		||||
    <div v-if="false"
 | 
			
		||||
         ref="localControl"
 | 
			
		||||
         class="gl-plot__local-controls h-local-controls h-local-controls--overlay-content c-local-controls--show-on-hover"
 | 
			
		||||
    >
 | 
			
		||||
        <button
 | 
			
		||||
            v-if="data.length"
 | 
			
		||||
            class="c-button icon-reset"
 | 
			
		||||
            :disabled="!isZoomed"
 | 
			
		||||
            title="Reset pan/zoom"
 | 
			
		||||
            @click="reset()"
 | 
			
		||||
        <button v-if="data.length"
 | 
			
		||||
                class="c-button icon-reset"
 | 
			
		||||
                :disabled="!isZoomed"
 | 
			
		||||
                title="Reset pan/zoom"
 | 
			
		||||
                @click="reset()"
 | 
			
		||||
        >
 | 
			
		||||
        </button>
 | 
			
		||||
    </div>
 | 
			
		||||
@@ -21,13 +21,12 @@
 | 
			
		||||
-->
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
<BarGraph
 | 
			
		||||
    ref="barGraph"
 | 
			
		||||
    class="c-plot c-bar-chart-view"
 | 
			
		||||
    :data="trace"
 | 
			
		||||
    :plot-axis-title="plotAxisTitle"
 | 
			
		||||
    @subscribe="subscribeToAll"
 | 
			
		||||
    @unsubscribe="removeAllSubscriptions"
 | 
			
		||||
<BarGraph ref="barGraph"
 | 
			
		||||
          class="c-plot c-bar-chart-view"
 | 
			
		||||
          :data="trace"
 | 
			
		||||
          :plot-axis-title="plotAxisTitle"
 | 
			
		||||
          @subscribe="subscribeToAll"
 | 
			
		||||
          @unsubscribe="removeAllSubscriptions"
 | 
			
		||||
/>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
@@ -40,14 +39,6 @@ export default {
 | 
			
		||||
        BarGraph
 | 
			
		||||
    },
 | 
			
		||||
    inject: ['openmct', 'domainObject', 'path'],
 | 
			
		||||
    props: {
 | 
			
		||||
        options: {
 | 
			
		||||
            type: Object,
 | 
			
		||||
            default() {
 | 
			
		||||
                return {};
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    data() {
 | 
			
		||||
        this.telemetryObjects = {};
 | 
			
		||||
        this.telemetryObjectFormats = {};
 | 
			
		||||
@@ -255,7 +246,7 @@ export default {
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            let trace = {
 | 
			
		||||
            const trace = {
 | 
			
		||||
                key,
 | 
			
		||||
                name: telemetryObject.name,
 | 
			
		||||
                x: xValues,
 | 
			
		||||
@@ -263,18 +254,13 @@ export default {
 | 
			
		||||
                text: yValues.map(String),
 | 
			
		||||
                xAxisMetadata: axisMetadata.xAxisMetadata,
 | 
			
		||||
                yAxisMetadata: axisMetadata.yAxisMetadata,
 | 
			
		||||
                type: this.options.type ? this.options.type : 'bar',
 | 
			
		||||
                type: 'bar',
 | 
			
		||||
                marker: {
 | 
			
		||||
                    color: this.domainObject.configuration.barStyles.series[key].color
 | 
			
		||||
                },
 | 
			
		||||
                hoverinfo: 'skip'
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            if (this.options.type) {
 | 
			
		||||
                trace.mode = 'markers';
 | 
			
		||||
                trace.hoverinfo = 'x+y';
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.addTrace(trace, key);
 | 
			
		||||
        },
 | 
			
		||||
        isDataInTimeRange(datum, key) {
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user