Compare commits
	
		
			2 Commits
		
	
	
		
			plot-legen
			...
			sprint-2.0
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					aedf7c6aee | ||
| 
						 | 
					efb5d270d9 | 
@@ -2,7 +2,7 @@ version: 2.1
 | 
			
		||||
executors:
 | 
			
		||||
  pw-focal-development:
 | 
			
		||||
    docker:
 | 
			
		||||
      - image: mcr.microsoft.com/playwright:v1.23.0-focal
 | 
			
		||||
      - image: mcr.microsoft.com/playwright:v1.21.1-focal
 | 
			
		||||
    environment:
 | 
			
		||||
      NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed
 | 
			
		||||
parameters:
 | 
			
		||||
@@ -12,7 +12,7 @@ parameters:
 | 
			
		||||
    type: boolean
 | 
			
		||||
commands:
 | 
			
		||||
  build_and_install:
 | 
			
		||||
    description: "All steps used to build and install. Will use cache if found"
 | 
			
		||||
    description: "All steps used to build and install. Will not work on node10"
 | 
			
		||||
    parameters:
 | 
			
		||||
      node-version:
 | 
			
		||||
        type: string
 | 
			
		||||
@@ -23,7 +23,7 @@ commands:
 | 
			
		||||
      - node/install:
 | 
			
		||||
          install-npm: true
 | 
			
		||||
          node-version: << parameters.node-version >>
 | 
			
		||||
      - run: npm install --prefer-offline --no-audit --progress=false
 | 
			
		||||
      - run: npm install
 | 
			
		||||
  restore_cache_cmd:
 | 
			
		||||
    description: "Custom command for restoring cache with the ability to bust cache. When BUST_CACHE is set to true, jobs will not restore cache"
 | 
			
		||||
    parameters:
 | 
			
		||||
@@ -31,7 +31,7 @@ commands:
 | 
			
		||||
        type: string
 | 
			
		||||
    steps:
 | 
			
		||||
      - when:
 | 
			
		||||
          condition:
 | 
			
		||||
          condition: 
 | 
			
		||||
            equal: [false, << pipeline.parameters.BUST_CACHE >> ]
 | 
			
		||||
          steps:
 | 
			
		||||
            - restore_cache:
 | 
			
		||||
@@ -41,7 +41,7 @@ commands:
 | 
			
		||||
    parameters:
 | 
			
		||||
      node-version:
 | 
			
		||||
        type: string
 | 
			
		||||
    steps:
 | 
			
		||||
    steps:    
 | 
			
		||||
      - save_cache:
 | 
			
		||||
          key: deps-{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}-{{ checksum ".circleci/config.yml" }}
 | 
			
		||||
          paths:
 | 
			
		||||
@@ -58,14 +58,10 @@ commands:
 | 
			
		||||
          ls -latR >> /tmp/artifacts/dir.txt
 | 
			
		||||
      - store_artifacts:
 | 
			
		||||
          path: /tmp/artifacts/
 | 
			
		||||
  generate_e2e_code_cov_report:
 | 
			
		||||
   description: "Generate e2e code coverage artifacts and publish to codecov.io. Needed to that we can ignore the exit code status of the npm run test"
 | 
			
		||||
   parameters:
 | 
			
		||||
      suite:
 | 
			
		||||
        type: string
 | 
			
		||||
   steps:
 | 
			
		||||
    - run: npm run cov:e2e:report 
 | 
			
		||||
    - run: npm run cov:e2e:<<parameters.suite>>:publish       
 | 
			
		||||
  upload_code_covio:
 | 
			
		||||
    description: "Command to upload code coverage reports to codecov.io"
 | 
			
		||||
    steps:
 | 
			
		||||
        - run: curl -Os https://uploader.codecov.io/latest/linux/codecov;chmod +x codecov;./codecov 
 | 
			
		||||
orbs:
 | 
			
		||||
  node: circleci/node@4.9.0
 | 
			
		||||
  browser-tools: circleci/browser-tools@1.3.0
 | 
			
		||||
@@ -105,7 +101,7 @@ jobs:
 | 
			
		||||
            equal: [ "FirefoxESR", <<parameters.browser>> ]
 | 
			
		||||
          steps:
 | 
			
		||||
            - browser-tools/install-firefox:
 | 
			
		||||
                version: "91.7.1esr" #https://archive.mozilla.org/pub/firefox/releases/
 | 
			
		||||
                version: "91.7.1esr" #https://archive.mozilla.org/pub/firefox/releases/          
 | 
			
		||||
      - when:
 | 
			
		||||
          condition:
 | 
			
		||||
            equal: [ "FirefoxHeadless", <<parameters.browser>> ]
 | 
			
		||||
@@ -118,13 +114,12 @@ jobs:
 | 
			
		||||
            - browser-tools/install-chrome:
 | 
			
		||||
                replace-existing: false
 | 
			
		||||
      - run: npm run test -- --browsers=<<parameters.browser>>
 | 
			
		||||
      - run: npm run cov:unit:publish
 | 
			
		||||
      - save_cache_cmd:
 | 
			
		||||
          node-version: <<parameters.node-version>>
 | 
			
		||||
      - store_test_results:
 | 
			
		||||
          path: dist/reports/tests/
 | 
			
		||||
      - store_artifacts:
 | 
			
		||||
          path: coverage
 | 
			
		||||
          path: dist/reports/
 | 
			
		||||
      - generate_and_store_version_and_filesystem_artifacts
 | 
			
		||||
  e2e-test:
 | 
			
		||||
    parameters:
 | 
			
		||||
@@ -133,49 +128,28 @@ jobs:
 | 
			
		||||
      suite:
 | 
			
		||||
        type: string
 | 
			
		||||
    executor: pw-focal-development
 | 
			
		||||
    parallelism: 4
 | 
			
		||||
    steps:
 | 
			
		||||
      - build_and_install:
 | 
			
		||||
          node-version: <<parameters.node-version>>
 | 
			
		||||
      - when: #Only install chrome-beta when running the full suite to save $$$
 | 
			
		||||
          condition:
 | 
			
		||||
            equal: [ "full", <<parameters.suite>> ]
 | 
			
		||||
          steps:
 | 
			
		||||
            - run: npx playwright install chrome-beta
 | 
			
		||||
      - run: SHARD="$((${CIRCLE_NODE_INDEX}+1))"; npm run test:e2e:<<parameters.suite>> -- --shard=${SHARD}/${CIRCLE_NODE_TOTAL}
 | 
			
		||||
      - generate_e2e_code_cov_report:
 | 
			
		||||
         suite: <<parameters.suite>>          
 | 
			
		||||
      - run: npx playwright install
 | 
			
		||||
      - run: npm run test:e2e:<<parameters.suite>>
 | 
			
		||||
      - store_test_results:
 | 
			
		||||
          path: test-results/results.xml
 | 
			
		||||
      - store_artifacts:
 | 
			
		||||
          path: test-results
 | 
			
		||||
      - store_artifacts:
 | 
			
		||||
          path: coverage
 | 
			
		||||
      - store_artifacts:
 | 
			
		||||
          path: html-test-results
 | 
			
		||||
      - generate_and_store_version_and_filesystem_artifacts
 | 
			
		||||
  perf-test:
 | 
			
		||||
    parameters:
 | 
			
		||||
      node-version:
 | 
			
		||||
        type: string
 | 
			
		||||
    executor: pw-focal-development
 | 
			
		||||
    steps:
 | 
			
		||||
      - build_and_install:
 | 
			
		||||
          node-version: <<parameters.node-version>>
 | 
			
		||||
      - run: npm run test:perf
 | 
			
		||||
      - store_test_results:
 | 
			
		||||
          path: test-results/results.xml
 | 
			
		||||
      - store_artifacts:
 | 
			
		||||
          path: test-results
 | 
			
		||||
      - store_artifacts:
 | 
			
		||||
          path: html-test-results
 | 
			
		||||
      - generate_and_store_version_and_filesystem_artifacts
 | 
			
		||||
workflows:
 | 
			
		||||
  overall-circleci-commit-status: #These jobs run on every commit
 | 
			
		||||
    jobs:
 | 
			
		||||
      - lint:
 | 
			
		||||
          name: node14-lint
 | 
			
		||||
          name: node16-lint
 | 
			
		||||
          node-version: lts/gallium
 | 
			
		||||
      - unit-test: 
 | 
			
		||||
          name: node14-chrome
 | 
			
		||||
          node-version: lts/fermium
 | 
			
		||||
          browser: ChromeHeadless
 | 
			
		||||
          post-steps:
 | 
			
		||||
            - upload_code_covio
 | 
			
		||||
      - unit-test:
 | 
			
		||||
          name: node16-chrome
 | 
			
		||||
          node-version: lts/gallium
 | 
			
		||||
@@ -188,8 +162,6 @@ workflows:
 | 
			
		||||
          name: e2e-ci
 | 
			
		||||
          node-version: lts/gallium
 | 
			
		||||
          suite: ci
 | 
			
		||||
      - perf-test:
 | 
			
		||||
          node-version: lts/gallium
 | 
			
		||||
  the-nightly: #These jobs do not run on PRs, but against master at night
 | 
			
		||||
    jobs:
 | 
			
		||||
      - unit-test:
 | 
			
		||||
 
 | 
			
		||||
@@ -29,7 +29,6 @@ module.exports = {
 | 
			
		||||
        "you-dont-need-lodash-underscore/omit": "off",
 | 
			
		||||
        "you-dont-need-lodash-underscore/throttle": "off",
 | 
			
		||||
        "you-dont-need-lodash-underscore/flatten": "off",
 | 
			
		||||
        "you-dont-need-lodash-underscore/get": "off",
 | 
			
		||||
        "no-bitwise": "error",
 | 
			
		||||
        "curly": "error",
 | 
			
		||||
        "eqeqeq": "error",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										22
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										22
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -15,6 +15,8 @@
 | 
			
		||||
*.idea
 | 
			
		||||
*.iml
 | 
			
		||||
 | 
			
		||||
# External dependencies
 | 
			
		||||
 | 
			
		||||
# Build output
 | 
			
		||||
target
 | 
			
		||||
dist
 | 
			
		||||
@@ -22,24 +24,30 @@ dist
 | 
			
		||||
# Mac OS X Finder
 | 
			
		||||
.DS_Store
 | 
			
		||||
 | 
			
		||||
# Closed source libraries
 | 
			
		||||
closed-lib
 | 
			
		||||
 | 
			
		||||
# Node, Bower dependencies
 | 
			
		||||
node_modules
 | 
			
		||||
bower_components
 | 
			
		||||
 | 
			
		||||
# Protractor logs
 | 
			
		||||
protractor/logs
 | 
			
		||||
 | 
			
		||||
# npm-debug log
 | 
			
		||||
npm-debug.log
 | 
			
		||||
 | 
			
		||||
# karma reports
 | 
			
		||||
report.*.json
 | 
			
		||||
 | 
			
		||||
# Lighthouse reports
 | 
			
		||||
.lighthouseci
 | 
			
		||||
 | 
			
		||||
# e2e test artifacts
 | 
			
		||||
test-results
 | 
			
		||||
html-test-results
 | 
			
		||||
allure-results
 | 
			
		||||
 | 
			
		||||
# codecov artifacts
 | 
			
		||||
.nyc_output
 | 
			
		||||
coverage
 | 
			
		||||
codecov
 | 
			
		||||
 | 
			
		||||
# :(
 | 
			
		||||
package-lock.json
 | 
			
		||||
 | 
			
		||||
#codecov artifacts
 | 
			
		||||
codecov
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								app.js
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								app.js
									
									
									
									
									
								
							@@ -49,7 +49,7 @@ class WatchRunPlugin {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const webpack = require('webpack');
 | 
			
		||||
const webpackConfig = process.env.CI ? require('./webpack.coverage.js') : require('./webpack.dev.js');
 | 
			
		||||
const webpackConfig = require('./webpack.dev.js');
 | 
			
		||||
webpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin());
 | 
			
		||||
webpackConfig.plugins.push(new WatchRunPlugin());
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										17
									
								
								codecov.yml
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								codecov.yml
									
									
									
									
									
								
							@@ -13,16 +13,17 @@ coverage:
 | 
			
		||||
  round: down
 | 
			
		||||
  range: "66...100"
 | 
			
		||||
 | 
			
		||||
flags:
 | 
			
		||||
  unit:
 | 
			
		||||
    carryforward: true 
 | 
			
		||||
  e2e-ci:
 | 
			
		||||
    carryforward: true
 | 
			
		||||
  e2e-full:
 | 
			
		||||
    carryforward: true    
 | 
			
		||||
ignore:
 | 
			
		||||
 | 
			
		||||
parsers:
 | 
			
		||||
  gcov:
 | 
			
		||||
    branch_detection:
 | 
			
		||||
      conditional: true
 | 
			
		||||
      loop: true
 | 
			
		||||
      method: false
 | 
			
		||||
      macro: false
 | 
			
		||||
 | 
			
		||||
comment:
 | 
			
		||||
  layout: "reach,diff,flags,files,footer"
 | 
			
		||||
  behavior: default
 | 
			
		||||
  require_changes: false
 | 
			
		||||
  show_carryforward_flags: true
 | 
			
		||||
@@ -1,12 +1,4 @@
 | 
			
		||||
/* eslint-disable no-undef */
 | 
			
		||||
module.exports = {
 | 
			
		||||
    "extends": ["plugin:playwright/playwright-test"],
 | 
			
		||||
    "overrides": [
 | 
			
		||||
        {
 | 
			
		||||
            "files": ["tests/visual/*.spec.js"],
 | 
			
		||||
            "rules": {
 | 
			
		||||
                "playwright/no-wait-for-timeout": "off"
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    ]
 | 
			
		||||
    "extends": ["plugin:playwright/playwright-test"]
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -1,69 +0,0 @@
 | 
			
		||||
/* This file extends the base functionality of the playwright test framework to enable
 | 
			
		||||
 * code coverage instrumentation, console log error detection and working with a 3rd
 | 
			
		||||
 * party Chrome-as-a-service extension called Browserless.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
const base = require('@playwright/test');
 | 
			
		||||
const { expect } = require('@playwright/test');
 | 
			
		||||
const fs = require('fs');
 | 
			
		||||
const path = require('path');
 | 
			
		||||
const { v4: uuid } = require('uuid');
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Takes a `ConsoleMessage` and returns a formatted string
 | 
			
		||||
 * @param {import('@playwright/test').ConsoleMessage} msg
 | 
			
		||||
 * @returns {String} formatted string with message type, text, url, and line and column numbers
 | 
			
		||||
 */
 | 
			
		||||
function consoleMessageToString(msg) {
 | 
			
		||||
    const { url, lineNumber, columnNumber } = msg.location();
 | 
			
		||||
 | 
			
		||||
    return `[${msg.type()}] ${msg.text()}
 | 
			
		||||
    at (${url} ${lineNumber}:${columnNumber})`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
//The following is based on https://github.com/mxschmitt/playwright-test-coverage
 | 
			
		||||
// eslint-disable-next-line no-undef
 | 
			
		||||
const istanbulCLIOutput = path.join(process.cwd(), '.nyc_output');
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line no-undef
 | 
			
		||||
exports.test = base.test.extend({
 | 
			
		||||
    //The following is based on https://github.com/mxschmitt/playwright-test-coverage
 | 
			
		||||
    context: async ({ context }, use) => {
 | 
			
		||||
        await context.addInitScript(() =>
 | 
			
		||||
            window.addEventListener('beforeunload', () =>
 | 
			
		||||
                (window).collectIstanbulCoverage(JSON.stringify((window).__coverage__))
 | 
			
		||||
            )
 | 
			
		||||
        );
 | 
			
		||||
        await fs.promises.mkdir(istanbulCLIOutput, { recursive: true });
 | 
			
		||||
        await context.exposeFunction('collectIstanbulCoverage', (coverageJSON) => {
 | 
			
		||||
            if (coverageJSON) {
 | 
			
		||||
                fs.writeFileSync(path.join(istanbulCLIOutput, `playwright_coverage_${uuid()}.json`), coverageJSON);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        await use(context);
 | 
			
		||||
        for (const page of context.pages()) {
 | 
			
		||||
            await page.evaluate(() => (window).collectIstanbulCoverage(JSON.stringify((window).__coverage__)));
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    page: async ({ baseURL, page }, use) => {
 | 
			
		||||
        const messages = [];
 | 
			
		||||
        page.on('console', (msg) => messages.push(msg));
 | 
			
		||||
        await use(page);
 | 
			
		||||
        messages.forEach(
 | 
			
		||||
            msg => expect.soft(msg.type(), `Console error detected: ${consoleMessageToString(msg)}`).not.toEqual('error')
 | 
			
		||||
        );
 | 
			
		||||
    },
 | 
			
		||||
    browser: async ({ playwright, browser }, use, workerInfo) => {
 | 
			
		||||
        // Use browserless if configured
 | 
			
		||||
        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,30 +2,27 @@
 | 
			
		||||
// 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: 3, //Retries 3 times for a total of 4. When running sharded and with maxFailures = 5, this should ensure that flake is managed without failing the full suite
 | 
			
		||||
    retries: 1,
 | 
			
		||||
    testDir: 'tests',
 | 
			
		||||
    testIgnore: '**/*.perf.spec.js', //Ignore performance tests and define in playwright-perfromance.config.js
 | 
			
		||||
    timeout: 60 * 1000,
 | 
			
		||||
    webServer: {
 | 
			
		||||
        command: 'npm run start',
 | 
			
		||||
        url: 'http://localhost:8080/#',
 | 
			
		||||
        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: [
 | 
			
		||||
        {
 | 
			
		||||
@@ -36,7 +33,6 @@ const config = {
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            name: 'MMOC',
 | 
			
		||||
            testMatch: '**/*.e2e.spec.js', // only run e2e tests
 | 
			
		||||
            grepInvert: /@snapshot/,
 | 
			
		||||
            use: {
 | 
			
		||||
                browserName: 'chromium',
 | 
			
		||||
@@ -45,32 +41,19 @@ const config = {
 | 
			
		||||
                    height: 1440
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            name: 'firefox',
 | 
			
		||||
            testMatch: '**/*.e2e.spec.js', // only run e2e tests
 | 
			
		||||
            grepInvert: /@snapshot/,
 | 
			
		||||
            use: {
 | 
			
		||||
                browserName: 'firefox'
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            name: 'chrome-beta', //Only Chrome Beta is available on ubuntu -- not chrome canary
 | 
			
		||||
            testMatch: '**/*.e2e.spec.js', // only run e2e tests
 | 
			
		||||
            grepInvert: /@snapshot/,
 | 
			
		||||
            use: {
 | 
			
		||||
                browserName: 'chromium',
 | 
			
		||||
                channel: 'chrome-beta'
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        /*{
 | 
			
		||||
            name: 'ipad',
 | 
			
		||||
            use: {
 | 
			
		||||
                browserName: 'webkit',
 | 
			
		||||
                ...devices['iPad (gen 7) landscape'] // Complete List https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json
 | 
			
		||||
            }
 | 
			
		||||
        }*/
 | 
			
		||||
    ],
 | 
			
		||||
    reporter: [
 | 
			
		||||
        ['list'],
 | 
			
		||||
        ['html', {
 | 
			
		||||
            open: 'never',
 | 
			
		||||
            outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840
 | 
			
		||||
        }],
 | 
			
		||||
        ['junit', { outputFile: 'test-results/results.xml' }],
 | 
			
		||||
        ['allure-playwright'],
 | 
			
		||||
        ['github']
 | 
			
		||||
    ]
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -2,18 +2,16 @@
 | 
			
		||||
// 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',
 | 
			
		||||
        url: 'http://localhost:8080/#',
 | 
			
		||||
        port: 8080,
 | 
			
		||||
        timeout: 120 * 1000,
 | 
			
		||||
        reuseExistingServer: !process.env.CI
 | 
			
		||||
    },
 | 
			
		||||
@@ -23,9 +21,9 @@ 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: [
 | 
			
		||||
        {
 | 
			
		||||
@@ -36,7 +34,6 @@ const config = {
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            name: 'MMOC',
 | 
			
		||||
            testMatch: '**/*.e2e.spec.js', // only run e2e tests
 | 
			
		||||
            grepInvert: /@snapshot/,
 | 
			
		||||
            use: {
 | 
			
		||||
                browserName: 'chromium',
 | 
			
		||||
@@ -45,59 +42,18 @@ const config = {
 | 
			
		||||
                    height: 1440
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            name: 'safari',
 | 
			
		||||
            testMatch: '**/*.e2e.spec.js', // only run e2e tests
 | 
			
		||||
            grep: /@ipad/, // only run ipad tests due to this bug https://github.com/microsoft/playwright/issues/8340
 | 
			
		||||
            grepInvert: /@snapshot/,
 | 
			
		||||
            use: {
 | 
			
		||||
                browserName: 'webkit'
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            name: 'firefox',
 | 
			
		||||
            testMatch: '**/*.e2e.spec.js', // only run e2e tests
 | 
			
		||||
            grepInvert: /@snapshot/,
 | 
			
		||||
            use: {
 | 
			
		||||
                browserName: 'firefox'
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            name: 'canary',
 | 
			
		||||
            testMatch: '**/*.e2e.spec.js', // only run e2e tests
 | 
			
		||||
            grepInvert: /@snapshot/,
 | 
			
		||||
            use: {
 | 
			
		||||
                browserName: 'chromium',
 | 
			
		||||
                channel: 'chrome-canary' //Note this is not available in ubuntu/CircleCI
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            name: 'chrome-beta',
 | 
			
		||||
            testMatch: '**/*.e2e.spec.js', // only run e2e tests
 | 
			
		||||
            grepInvert: /@snapshot/,
 | 
			
		||||
            use: {
 | 
			
		||||
                browserName: 'chromium',
 | 
			
		||||
                channel: 'chrome-beta'
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
        }
 | 
			
		||||
        /*{
 | 
			
		||||
            name: 'ipad',
 | 
			
		||||
            testMatch: '**/*.e2e.spec.js', // only run e2e tests
 | 
			
		||||
            grep: /@ipad/,
 | 
			
		||||
            grepInvert: /@snapshot/,
 | 
			
		||||
            use: {
 | 
			
		||||
                browserName: 'webkit',
 | 
			
		||||
                ...devices['iPad (gen 7) landscape'] // Complete List https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        }*/
 | 
			
		||||
    ],
 | 
			
		||||
    reporter: [
 | 
			
		||||
        ['list'],
 | 
			
		||||
        ['html', {
 | 
			
		||||
            open: 'on-failure',
 | 
			
		||||
            outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840
 | 
			
		||||
        }]
 | 
			
		||||
        ['allure-playwright']
 | 
			
		||||
    ]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,41 +0,0 @@
 | 
			
		||||
/* eslint-disable no-undef */
 | 
			
		||||
// playwright.config.js
 | 
			
		||||
// @ts-check
 | 
			
		||||
 | 
			
		||||
/** @type {import('@playwright/test').PlaywrightTestConfig} */
 | 
			
		||||
const config = {
 | 
			
		||||
    retries: 1, //Only for debugging purposes because trace is enabled only on first retry
 | 
			
		||||
    testDir: 'tests/performance/',
 | 
			
		||||
    timeout: 60 * 1000,
 | 
			
		||||
    workers: 1, //Only run in serial with 1 worker
 | 
			
		||||
    webServer: {
 | 
			
		||||
        command: 'npm run start',
 | 
			
		||||
        url: 'http://localhost: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: 'on-first-retry',
 | 
			
		||||
        video: 'off'
 | 
			
		||||
    },
 | 
			
		||||
    projects: [
 | 
			
		||||
        {
 | 
			
		||||
            name: 'chrome',
 | 
			
		||||
            use: {
 | 
			
		||||
                browserName: 'chromium'
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    ],
 | 
			
		||||
    reporter: [
 | 
			
		||||
        ['list'],
 | 
			
		||||
        ['junit', { outputFile: 'test-results/results.xml' }],
 | 
			
		||||
        ['json', { outputFile: 'test-results/results.json' }]
 | 
			
		||||
    ]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
module.exports = config;
 | 
			
		||||
@@ -4,20 +4,20 @@
 | 
			
		||||
 | 
			
		||||
/** @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',
 | 
			
		||||
        url: 'http://localhost:8080/#',
 | 
			
		||||
        port: 8080,
 | 
			
		||||
        timeout: 200 * 1000,
 | 
			
		||||
        reuseExistingServer: !process.env.CI
 | 
			
		||||
    },
 | 
			
		||||
    use: {
 | 
			
		||||
        browserName: "chromium",
 | 
			
		||||
        baseURL: 'http://localhost:8080/',
 | 
			
		||||
        headless: true, // this needs to remain headless to avoid visual changes due to GPU
 | 
			
		||||
        headless: true,
 | 
			
		||||
        ignoreHTTPSErrors: true,
 | 
			
		||||
        screenshot: 'on',
 | 
			
		||||
        trace: 'off',
 | 
			
		||||
@@ -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":{"b3cee102-86dd-4c0a-8eec-4d5d276f8691":{"identifier":{"key":"b3cee102-86dd-4c0a-8eec-4d5d276f8691","namespace":""},"name":"Performance Display Layout","type":"layout","composition":[{"key":"9666e7b4-be0c-47a5-94b8-99accad7155e","namespace":""}],"configuration":{"items":[{"width":32,"height":18,"x":12,"y":9,"identifier":{"key":"9666e7b4-be0c-47a5-94b8-99accad7155e","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"23ca351d-a67d-46aa-a762-290eb742d2f1"}],"layoutGrid":[10,10]},"modified":1654299875432,"location":"mine","persisted":1654299878751},"9666e7b4-be0c-47a5-94b8-99accad7155e":{"identifier":{"key":"9666e7b4-be0c-47a5-94b8-99accad7155e","namespace":""},"name":"Performance Example Imagery","type":"example.imagery","configuration":{"imageLocation":"","imageLoadDelayInMilliSeconds":20000,"imageSamples":[],"layers":[{"source":"dist/imagery/example-imagery-layer-16x9.png","name":"16:9","visible":false},{"source":"dist/imagery/example-imagery-layer-safe.png","name":"Safe","visible":false},{"source":"dist/imagery/example-imagery-layer-scale.png","name":"Scale","visible":false}]},"telemetry":{"values":[{"name":"Name","key":"name"},{"name":"Time","key":"utc","format":"utc","hints":{"domain":2}},{"name":"Local Time","key":"local","format":"local-format","hints":{"domain":1}},{"name":"Image","key":"url","format":"image","hints":{"image":1},"layers":[{"source":"dist/imagery/example-imagery-layer-16x9.png","name":"16:9"},{"source":"dist/imagery/example-imagery-layer-safe.png","name":"Safe"},{"source":"dist/imagery/example-imagery-layer-scale.png","name":"Scale"}]},{"name":"Image Download Name","key":"imageDownloadName","format":"imageDownloadName","hints":{"imageDownloadName":1}}]},"modified":1654299840077,"location":"b3cee102-86dd-4c0a-8eec-4d5d276f8691","persisted":1654299840078}},"rootId":"b3cee102-86dd-4c0a-8eec-4d5d276f8691"}
 | 
			
		||||
@@ -1 +0,0 @@
 | 
			
		||||
{"openmct":{"6d2fa9fd-f2aa-461a-a1e1-164ac44bec9d":{"identifier":{"key":"6d2fa9fd-f2aa-461a-a1e1-164ac44bec9d","namespace":""},"name":"Performance Notebook","type":"notebook","configuration":{"defaultSort":"oldest","entries":{"3e31c412-33ba-4757-8ade-e9821f6ba321":{"8c8f6035-631c-45af-8c24-786c60295335":[{"id":"entry-1652815305457","createdOn":1652815305457,"createdBy":"","text":"Existing Entry 1","embeds":[]},{"id":"entry-1652815313465","createdOn":1652815313465,"createdBy":"","text":"Existing Entry 2","embeds":[]},{"id":"entry-1652815399955","createdOn":1652815399955,"createdBy":"","text":"Existing Entry 3","embeds":[]}]}},"imageMigrationVer":"v1","pageTitle":"Page","sections":[{"id":"3e31c412-33ba-4757-8ade-e9821f6ba321","isDefault":false,"isSelected":false,"name":"Section1","pages":[{"id":"8c8f6035-631c-45af-8c24-786c60295335","isDefault":false,"isSelected":false,"name":"Page1","pageTitle":"Page"},{"id":"36555942-c9aa-439c-bbdb-0aaf50db50f5","isDefault":false,"isSelected":false,"name":"Page2","pageTitle":"Page"}],"sectionTitle":"Section"},{"id":"dab0bd1d-2c5a-405c-987f-107123d6189a","isDefault":false,"isSelected":true,"name":"Section2","pages":[{"id":"f625a86a-cb99-4898-8082-80543c8de534","isDefault":false,"isSelected":false,"name":"Page1","pageTitle":"Page"},{"id":"e77ef810-f785-42a7-942e-07e999b79c59","isDefault":false,"isSelected":true,"name":"Page2","pageTitle":"Page"}],"sectionTitle":"Section"}],"sectionTitle":"Section","type":"General","showTime":"0"},"modified":1652815915219,"location":"mine","persisted":1652815915222}},"rootId":"6d2fa9fd-f2aa-461a-a1e1-164ac44bec9d"}
 | 
			
		||||
@@ -1,22 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "cookies": [],
 | 
			
		||||
  "origins": [
 | 
			
		||||
    {
 | 
			
		||||
      "origin": "http://localhost:8080",
 | 
			
		||||
      "localStorage": [
 | 
			
		||||
        {
 | 
			
		||||
          "name": "tcHistory",
 | 
			
		||||
          "value": "{\"utc\":[{\"start\":1654548551471,\"end\":1654550351471}]}"
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "name": "mct",
 | 
			
		||||
          "value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"527856c0-cced-4b64-bb19-f943432326d0\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1654550352296,\"modified\":1654550352296},\"527856c0-cced-4b64-bb19-f943432326d0\":{\"identifier\":{\"key\":\"527856c0-cced-4b64-bb19-f943432326d0\",\"namespace\":\"\"},\"name\":\"Unnamed Overlay Plot\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"ce88ce37-8bb9-45e1-a85b-bb7e3c8453b9\",\"namespace\":\"\"}],\"configuration\":{\"series\":[{\"identifier\":{\"key\":\"ce88ce37-8bb9-45e1-a85b-bb7e3c8453b9\",\"namespace\":\"\"}}],\"yAxis\":{},\"xAxis\":{}},\"modified\":1654550353356,\"location\":\"mine\",\"persisted\":1654550353357},\"ce88ce37-8bb9-45e1-a85b-bb7e3c8453b9\":{\"name\":\"Unnamed Sine Wave Generator\",\"type\":\"generator\",\"identifier\":{\"key\":\"ce88ce37-8bb9-45e1-a85b-bb7e3c8453b9\",\"namespace\":\"\"},\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":\"5000\"},\"modified\":1654550353350,\"location\":\"527856c0-cced-4b64-bb19-f943432326d0\",\"persisted\":1654550353350}}"
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "name": "mct-tree-expanded",
 | 
			
		||||
          "value": "[\"/browse/mine\"]"
 | 
			
		||||
        }
 | 
			
		||||
      ]
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
@@ -1,22 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "cookies": [],
 | 
			
		||||
  "origins": [
 | 
			
		||||
    {
 | 
			
		||||
      "origin": "http://localhost:8080",
 | 
			
		||||
      "localStorage": [
 | 
			
		||||
        {
 | 
			
		||||
          "name": "tcHistory",
 | 
			
		||||
          "value": "{\"utc\":[{\"start\":1654537164464,\"end\":1654538964464},{\"start\":1652301954635,\"end\":1652303754635}]}"
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "name": "mct",
 | 
			
		||||
          "value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1654538965703,\"modified\":1654538965703},\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"73f2d9ae-d1f3-4561-b7fc-ecd5df557249\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1652303755999,\"location\":\"mine\",\"persisted\":1652303756002},\"2d02a680-eb7e-4645-bba2-dd298f76efb8\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"4291d80c-303c-4d8d-85e1-10f012b864fb\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1654538965702,\"location\":\"mine\",\"persisted\":1654538965702}}"
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "name": "mct-tree-expanded",
 | 
			
		||||
          "value": "[]"
 | 
			
		||||
        }
 | 
			
		||||
      ]
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
@@ -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
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@@ -24,8 +24,7 @@
 | 
			
		||||
This test suite is dedicated to tests which verify branding related components.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
const { test } = require('../fixtures.js');
 | 
			
		||||
const { expect } = require('@playwright/test');
 | 
			
		||||
const { test, expect } = require('@playwright/test');
 | 
			
		||||
 | 
			
		||||
test.describe('Branding tests', () => {
 | 
			
		||||
    test('About Modal launches with basic branding properties', async ({ page }) => {
 | 
			
		||||
@@ -58,7 +57,6 @@ test.describe('Branding tests', () => {
 | 
			
		||||
            page.waitForEvent('popup'),
 | 
			
		||||
            page.locator('text=click here for third party licensing information').click()
 | 
			
		||||
        ]);
 | 
			
		||||
        await page2.waitForLoadState('networkidle'); //Avoids timing issues with juggler/firefox
 | 
			
		||||
        expect(page2.waitForURL('**/licenses**')).toBeTruthy();
 | 
			
		||||
        expect(page2.waitForURL('**\/licenses**')).toBeTruthy();
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -24,8 +24,7 @@
 | 
			
		||||
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');
 | 
			
		||||
const { test, expect } = require('@playwright/test');
 | 
			
		||||
 | 
			
		||||
test.describe('Example Event Generator Operations', () => {
 | 
			
		||||
    test('Can create example event generator with a name', async ({ page }) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -24,13 +24,10 @@
 | 
			
		||||
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, browserName }) => {
 | 
			
		||||
        test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox');
 | 
			
		||||
 | 
			
		||||
    test('Create new Sine Wave Generator Object and validate create Form Logic', async ({ page }) => {
 | 
			
		||||
        //Go to baseURL
 | 
			
		||||
        await page.goto('/', { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
@@ -42,45 +39,44 @@ test.describe('Sine Wave Generator', () => {
 | 
			
		||||
 | 
			
		||||
        // Verify that the each required field has required indicator
 | 
			
		||||
        // Title
 | 
			
		||||
        await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(/req/);
 | 
			
		||||
        await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(['c-form-row__state-indicator  req']);
 | 
			
		||||
 | 
			
		||||
        // Verify that the Notes row does not have a required indicator
 | 
			
		||||
        await expect(page.locator('.c-form__section div:nth-child(3) .form-row .c-form-row__state-indicator')).not.toContain('.req');
 | 
			
		||||
        await page.locator('textarea[type="text"]').fill('Optional Note Text');
 | 
			
		||||
 | 
			
		||||
        // Period
 | 
			
		||||
        await expect(page.locator('div:nth-child(4) .c-form-row__state-indicator')).toHaveClass(/req/);
 | 
			
		||||
        await expect(page.locator('.c-form__section div:nth-child(4) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator  req']);
 | 
			
		||||
 | 
			
		||||
        // Amplitude
 | 
			
		||||
        await expect(page.locator('div:nth-child(5) .c-form-row__state-indicator')).toHaveClass(/req/);
 | 
			
		||||
        await expect(page.locator('.c-form__section div:nth-child(5) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator  req']);
 | 
			
		||||
 | 
			
		||||
        // Offset
 | 
			
		||||
        await expect(page.locator('div:nth-child(6) .c-form-row__state-indicator')).toHaveClass(/req/);
 | 
			
		||||
        await expect(page.locator('.c-form__section div:nth-child(6) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator  req']);
 | 
			
		||||
 | 
			
		||||
        // Data Rate
 | 
			
		||||
        await expect(page.locator('div:nth-child(7) .c-form-row__state-indicator')).toHaveClass(/req/);
 | 
			
		||||
        await expect(page.locator('.c-form__section div:nth-child(7) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator  req']);
 | 
			
		||||
 | 
			
		||||
        // Phase
 | 
			
		||||
        await expect(page.locator('div:nth-child(8) .c-form-row__state-indicator')).toHaveClass(/req/);
 | 
			
		||||
        await expect(page.locator('.c-form__section div:nth-child(8) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator  req']);
 | 
			
		||||
 | 
			
		||||
        // Randomness
 | 
			
		||||
        await expect(page.locator('div:nth-child(9) .c-form-row__state-indicator')).toHaveClass(/req/);
 | 
			
		||||
        await expect(page.locator('.c-form__section div:nth-child(9) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator  req']);
 | 
			
		||||
 | 
			
		||||
        // Verify that by removing value from required text field shows invalid indicator
 | 
			
		||||
        await page.locator('text=Properties Title Notes Period Amplitude Offset Data Rate (hz) Phase (radians) Ra >> input[type="text"]').fill('');
 | 
			
		||||
        await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(/invalid/);
 | 
			
		||||
        await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(['c-form-row__state-indicator  req invalid']);
 | 
			
		||||
 | 
			
		||||
        // Verify that by adding value to empty required text field changes invalid to valid indicator
 | 
			
		||||
        await page.locator('text=Properties Title Notes Period Amplitude Offset Data Rate (hz) Phase (radians) Ra >> input[type="text"]').fill('New Sine Wave Generator');
 | 
			
		||||
        await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(/valid/);
 | 
			
		||||
        await page.locator('text=Properties Title Notes Period Amplitude Offset Data Rate (hz) Phase (radians) Ra >> input[type="text"]').fill('non empty');
 | 
			
		||||
        await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(['c-form-row__state-indicator  req valid']);
 | 
			
		||||
 | 
			
		||||
        // Verify that by removing value from required number field shows invalid indicator
 | 
			
		||||
        await page.locator('.field.control.l-input-sm input').first().fill('');
 | 
			
		||||
        await expect(page.locator('div:nth-child(4) .c-form-row__state-indicator')).toHaveClass(/invalid/);
 | 
			
		||||
        await expect(page.locator('.c-form__section div:nth-child(4) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator  req invalid']);
 | 
			
		||||
 | 
			
		||||
        // Verify that by adding value to empty required number field changes invalid to valid indicator
 | 
			
		||||
        await page.locator('.field.control.l-input-sm input').first().fill('3');
 | 
			
		||||
        await expect(page.locator('div:nth-child(4) .c-form-row__state-indicator')).toHaveClass(/valid/);
 | 
			
		||||
        await expect(page.locator('.c-form__section div:nth-child(4) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator  req valid']);
 | 
			
		||||
 | 
			
		||||
        // Verify that can change value of number field by up/down arrows keys
 | 
			
		||||
        // Click .field.control.l-input-sm input >> nth=0
 | 
			
		||||
@@ -93,6 +89,57 @@ test.describe('Sine Wave Generator', () => {
 | 
			
		||||
        const value = await page.locator('.field.control.l-input-sm input').first().inputValue();
 | 
			
		||||
        await expect(value).toBe('6');
 | 
			
		||||
 | 
			
		||||
        // Click .c-form-row__state-indicator.grows
 | 
			
		||||
        await page.locator('.c-form-row__state-indicator.grows').click();
 | 
			
		||||
 | 
			
		||||
        // Click text=Properties Title Notes Period Amplitude Offset Data Rate (hz) Phase (radians) Ra >> input[type="text"]
 | 
			
		||||
        await page.locator('text=Properties Title Notes Period Amplitude Offset Data Rate (hz) Phase (radians) Ra >> input[type="text"]').click();
 | 
			
		||||
 | 
			
		||||
        // Click .c-form-row__state-indicator >> nth=0
 | 
			
		||||
        await page.locator('.c-form-row__state-indicator').first().click();
 | 
			
		||||
 | 
			
		||||
        // Fill text=Properties Title Notes Period Amplitude Offset Data Rate (hz) Phase (radians) Ra >> input[type="text"]
 | 
			
		||||
        await page.locator('text=Properties Title Notes Period Amplitude Offset Data Rate (hz) Phase (radians) Ra >> input[type="text"]').fill('New Sine Wave Generator');
 | 
			
		||||
 | 
			
		||||
        // Double click div:nth-child(4) .form-row .c-form-row__controls
 | 
			
		||||
        await page.locator('div:nth-child(4) .form-row .c-form-row__controls').dblclick();
 | 
			
		||||
 | 
			
		||||
        // Click .field.control.l-input-sm input >> nth=0
 | 
			
		||||
        await page.locator('.field.control.l-input-sm input').first().click();
 | 
			
		||||
 | 
			
		||||
        // Click div:nth-child(4) .form-row .c-form-row__state-indicator
 | 
			
		||||
        await page.locator('div:nth-child(4) .form-row .c-form-row__state-indicator').click();
 | 
			
		||||
 | 
			
		||||
        // Click .field.control.l-input-sm input >> nth=0
 | 
			
		||||
        await page.locator('.field.control.l-input-sm input').first().click();
 | 
			
		||||
 | 
			
		||||
        // Click .field.control.l-input-sm input >> nth=0
 | 
			
		||||
        await page.locator('.field.control.l-input-sm input').first().click();
 | 
			
		||||
 | 
			
		||||
        // Click div:nth-child(5) .form-row .c-form-row__controls .form-control .field input
 | 
			
		||||
        await page.locator('div:nth-child(5) .form-row .c-form-row__controls .form-control .field input').click();
 | 
			
		||||
 | 
			
		||||
        // Click div:nth-child(5) .form-row .c-form-row__controls .form-control .field input
 | 
			
		||||
        await page.locator('div:nth-child(5) .form-row .c-form-row__controls .form-control .field input').click();
 | 
			
		||||
 | 
			
		||||
        // Click div:nth-child(5) .form-row .c-form-row__controls .form-control .field input
 | 
			
		||||
        await page.locator('div:nth-child(5) .form-row .c-form-row__controls .form-control .field input').click();
 | 
			
		||||
 | 
			
		||||
        // Click div:nth-child(6) .form-row .c-form-row__controls .form-control .field input
 | 
			
		||||
        await page.locator('div:nth-child(6) .form-row .c-form-row__controls .form-control .field input').click();
 | 
			
		||||
 | 
			
		||||
        // Double click div:nth-child(7) .form-row .c-form-row__controls .form-control .field input
 | 
			
		||||
        await page.locator('div:nth-child(7) .form-row .c-form-row__controls .form-control .field input').dblclick();
 | 
			
		||||
 | 
			
		||||
        // Click div:nth-child(7) .form-row .c-form-row__state-indicator
 | 
			
		||||
        await page.locator('div:nth-child(7) .form-row .c-form-row__state-indicator').click();
 | 
			
		||||
 | 
			
		||||
        // Click div:nth-child(7) .form-row .c-form-row__controls .form-control .field input
 | 
			
		||||
        await page.locator('div:nth-child(7) .form-row .c-form-row__controls .form-control .field input').click();
 | 
			
		||||
 | 
			
		||||
        // Fill div:nth-child(7) .form-row .c-form-row__controls .form-control .field input
 | 
			
		||||
        await page.locator('div:nth-child(7) .form-row .c-form-row__controls .form-control .field input').fill('3');
 | 
			
		||||
 | 
			
		||||
        //Click text=OK
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            page.waitForNavigation(),
 | 
			
		||||
@@ -103,7 +150,7 @@ test.describe('Sine Wave Generator', () => {
 | 
			
		||||
        // Verify object properties
 | 
			
		||||
        await expect(page.locator('.l-browse-bar__object-name')).toContainText('New Sine Wave Generator');
 | 
			
		||||
 | 
			
		||||
        // Verify canvas rendered and can be interacted with
 | 
			
		||||
        // Verify canvas rendered
 | 
			
		||||
        await page.locator('canvas').nth(1).click({
 | 
			
		||||
            position: {
 | 
			
		||||
                x: 341,
 | 
			
		||||
 
 | 
			
		||||
@@ -24,8 +24,7 @@
 | 
			
		||||
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');
 | 
			
		||||
const { test, 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 }) => {
 | 
			
		||||
@@ -39,7 +38,6 @@ test.describe('Move item tests', () => {
 | 
			
		||||
 | 
			
		||||
        await page.locator('text=Properties Title Notes >> input[type="text"]').click();
 | 
			
		||||
        await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder1);
 | 
			
		||||
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            page.waitForNavigation(),
 | 
			
		||||
            page.locator('text=OK').click(),
 | 
			
		||||
@@ -55,7 +53,6 @@ test.describe('Move item tests', () => {
 | 
			
		||||
        await page.locator('li.icon-folder').click();
 | 
			
		||||
        await page.locator('text=Properties Title Notes >> input[type="text"]').click();
 | 
			
		||||
        await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder2);
 | 
			
		||||
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            page.waitForNavigation(),
 | 
			
		||||
            page.locator('text=OK').click(),
 | 
			
		||||
@@ -74,8 +71,10 @@ test.describe('Move item tests', () => {
 | 
			
		||||
        });
 | 
			
		||||
        await page.locator('li.icon-move').click();
 | 
			
		||||
        await page.locator('form[name="mctForm"] >> text=My Items').click();
 | 
			
		||||
 | 
			
		||||
        await page.locator('text=OK').click();
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            page.waitForNavigation(),
 | 
			
		||||
            page.locator('text=OK').click()
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        // Expect that Folder 2 is in My Items, the root folder
 | 
			
		||||
        expect(page.locator(`text=My Items >> nth=0:has(text=${folder2})`)).toBeTruthy();
 | 
			
		||||
@@ -90,8 +89,10 @@ test.describe('Move item tests', () => {
 | 
			
		||||
        await page.locator('li:has-text("Telemetry Table")').click();
 | 
			
		||||
        await page.locator('text=Properties Title Notes >> input[type="text"]').click();
 | 
			
		||||
        await page.locator('text=Properties Title Notes >> input[type="text"]').fill(telemetryTable);
 | 
			
		||||
 | 
			
		||||
        await page.locator('text=OK').click();
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            page.waitForNavigation(),
 | 
			
		||||
            page.locator('text=OK').click()
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        // Finish editing and save Telemetry Table
 | 
			
		||||
        await page.locator('.c-button--menu.c-button--major.icon-save').click();
 | 
			
		||||
@@ -112,7 +113,10 @@ test.describe('Move item tests', () => {
 | 
			
		||||
 | 
			
		||||
        // Continue test regardless of assertion and create it in My Items
 | 
			
		||||
        await page.locator('form[name="mctForm"] >> text=My Items').click();
 | 
			
		||||
        await page.locator('text=OK').click();
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            page.waitForNavigation(),
 | 
			
		||||
            page.locator('text=OK').click()
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        // Open My Items
 | 
			
		||||
        await page.locator('text=Open MCT My Items >> span').nth(3).click();
 | 
			
		||||
 
 | 
			
		||||
@@ -1,177 +0,0 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2022, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
This test suite is dedicated to performance tests to ensure that testability of performance
 | 
			
		||||
is not broken upstream on Open MCT. Any assumptions made downstream will be tested here
 | 
			
		||||
 | 
			
		||||
TODO:
 | 
			
		||||
 - Update resolution of performance config
 | 
			
		||||
 - Add Performance Observer on init to push all performance marks
 | 
			
		||||
 - Move client CDP connection to before or to a fixture
 | 
			
		||||
 -
 | 
			
		||||
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
const { test, expect } = require('@playwright/test');
 | 
			
		||||
 | 
			
		||||
const filePath = 'e2e/test-data/PerformanceDisplayLayout.json';
 | 
			
		||||
 | 
			
		||||
test.describe('Performance tests', () => {
 | 
			
		||||
    test.beforeEach(async ({ page, browser }, testInfo) => {
 | 
			
		||||
        // Go to baseURL
 | 
			
		||||
        await page.goto('/', { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
        // Click a:has-text("My Items")
 | 
			
		||||
        await page.locator('a:has-text("My Items")').click({
 | 
			
		||||
            button: 'right'
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Click text=Import from JSON
 | 
			
		||||
        await page.locator('text=Import from JSON').click();
 | 
			
		||||
 | 
			
		||||
        // Upload Performance Display Layout.json
 | 
			
		||||
        await page.setInputFiles('#fileElem', filePath);
 | 
			
		||||
 | 
			
		||||
        // Click text=OK
 | 
			
		||||
        await page.locator('text=OK').click();
 | 
			
		||||
 | 
			
		||||
        await expect(page.locator('a:has-text("Performance Display Layout Display Layout")')).toBeVisible();
 | 
			
		||||
 | 
			
		||||
        //Create a Chrome Performance Timeline trace to store as a test artifact
 | 
			
		||||
        console.log("\n==== Devtools: startTracing ====\n");
 | 
			
		||||
        await browser.startTracing(page, {
 | 
			
		||||
            path: `${testInfo.outputPath()}-trace.json`,
 | 
			
		||||
            screenshots: true
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
    test.afterEach(async ({ page, browser}) => {
 | 
			
		||||
        console.log("\n==== Devtools: stopTracing ====\n");
 | 
			
		||||
        await browser.stopTracing();
 | 
			
		||||
 | 
			
		||||
        /* Measurement Section
 | 
			
		||||
        / The following section includes a block of performance measurements.
 | 
			
		||||
        */
 | 
			
		||||
        //Get time difference between viewlarge actionability and evaluate time
 | 
			
		||||
        await page.evaluate(() => (window.performance.measure("machine-time-difference", "viewlarge.start", "viewLarge.start.test")));
 | 
			
		||||
 | 
			
		||||
        //Get StartTime
 | 
			
		||||
        const startTime = await page.evaluate(() => window.performance.timing.navigationStart);
 | 
			
		||||
        console.log('window.performance.timing.navigationStart', startTime);
 | 
			
		||||
 | 
			
		||||
        //Get All Performance Marks
 | 
			
		||||
        const getAllMarksJson = await page.evaluate(() => JSON.stringify(window.performance.getEntriesByType("mark")));
 | 
			
		||||
        const getAllMarks = JSON.parse(getAllMarksJson);
 | 
			
		||||
        console.log('window.performance.getEntriesByType("mark")', getAllMarks);
 | 
			
		||||
 | 
			
		||||
        //Get All Performance Measures
 | 
			
		||||
        const getAllMeasuresJson = await page.evaluate(() => JSON.stringify(window.performance.getEntriesByType("measure")));
 | 
			
		||||
        const getAllMeasures = JSON.parse(getAllMeasuresJson);
 | 
			
		||||
        console.log('window.performance.getEntriesByType("measure")', getAllMeasures);
 | 
			
		||||
 | 
			
		||||
    });
 | 
			
		||||
    /* The following test will navigate to a previously created Performance Display Layout and measure the
 | 
			
		||||
    /  following metrics:
 | 
			
		||||
    /  - ElementResourceTiming
 | 
			
		||||
    /  - Interaction Timing
 | 
			
		||||
    */
 | 
			
		||||
    test('Embedded View Large for Imagery is performant in Fixed Time', async ({ page, browser }) => {
 | 
			
		||||
        const client = await page.context().newCDPSession(page);
 | 
			
		||||
        // Tell the DevTools session to record performance metrics
 | 
			
		||||
        // https://chromedevtools.github.io/devtools-protocol/tot/Performance/#method-getMetrics
 | 
			
		||||
        await client.send('Performance.enable');
 | 
			
		||||
        // Go to baseURL
 | 
			
		||||
        await page.goto('/');
 | 
			
		||||
 | 
			
		||||
        // Search Available after Launch
 | 
			
		||||
        await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
 | 
			
		||||
        await page.evaluate(() => window.performance.mark("search-available"));
 | 
			
		||||
        // Fill Search input
 | 
			
		||||
        await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Performance Display Layout');
 | 
			
		||||
        await page.evaluate(() => window.performance.mark("search-entered"));
 | 
			
		||||
        //Search Result Appears and is clicked
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            page.waitForNavigation(),
 | 
			
		||||
            page.locator('a:has-text("Performance Display Layout")').first().click(),
 | 
			
		||||
            page.evaluate(() => window.performance.mark("click-search-result"))
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        //Time to Example Imagery Frame loads within Display Layout
 | 
			
		||||
        await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible'});
 | 
			
		||||
        //Time to Example Imagery object loads
 | 
			
		||||
        await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible'});
 | 
			
		||||
 | 
			
		||||
        //Get background-image url from background-image css prop
 | 
			
		||||
        const backgroundImage = await page.locator('.c-imagery__main-image__background-image');
 | 
			
		||||
        let backgroundImageUrl = await backgroundImage.evaluate((el) => {
 | 
			
		||||
            return window.getComputedStyle(el).getPropertyValue('background-image').match(/url\(([^)]+)\)/)[1];
 | 
			
		||||
        });
 | 
			
		||||
        backgroundImageUrl = backgroundImageUrl.slice(1, -1); //forgive me, padre
 | 
			
		||||
        console.log('backgroundImageurl ' + backgroundImageUrl);
 | 
			
		||||
 | 
			
		||||
        //Get ResourceTiming of background-image jpg
 | 
			
		||||
        const resourceTimingJson = await page.evaluate((bgImageUrl) =>
 | 
			
		||||
            JSON.stringify(window.performance.getEntriesByName(bgImageUrl).pop()),
 | 
			
		||||
        backgroundImageUrl
 | 
			
		||||
        );
 | 
			
		||||
        console.log('resourceTimingJson ' + resourceTimingJson);
 | 
			
		||||
 | 
			
		||||
        //Open Large view
 | 
			
		||||
        await page.locator('button:has-text("Large View")').click(); //This action includes the performance.mark named 'viewLarge.start'
 | 
			
		||||
        await page.evaluate(() => window.performance.mark("viewLarge.start.test")); //This is a mark only to compare evaluate timing
 | 
			
		||||
 | 
			
		||||
        //Time to Imagery Rendered in Large Frame
 | 
			
		||||
        await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible'});
 | 
			
		||||
        await page.evaluate(() => window.performance.mark("background-image-frame"));
 | 
			
		||||
 | 
			
		||||
        //Time to Example Imagery object loads
 | 
			
		||||
        await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible'});
 | 
			
		||||
        await page.evaluate(() => window.performance.mark("background-image-visible"));
 | 
			
		||||
 | 
			
		||||
        // Get Current number of images in thumbstrip
 | 
			
		||||
        await page.waitForSelector('.c-imagery__thumb');
 | 
			
		||||
        const thumbCount = await page.locator('.c-imagery__thumb').count();
 | 
			
		||||
        console.log('number of thumbs rendered ' + thumbCount);
 | 
			
		||||
        await page.locator('.c-imagery__thumb').last().click();
 | 
			
		||||
 | 
			
		||||
        //Get ResourceTiming of all jpg resources
 | 
			
		||||
        const resourceTimingJson2 = await page.evaluate(() =>
 | 
			
		||||
            JSON.stringify(window.performance.getEntriesByType('resource'))
 | 
			
		||||
        );
 | 
			
		||||
        const resourceTiming = JSON.parse(resourceTimingJson2);
 | 
			
		||||
        const jpgResourceTiming = resourceTiming.find((element) =>
 | 
			
		||||
            element.name.includes('.jpg')
 | 
			
		||||
        );
 | 
			
		||||
        console.log('jpgResourceTiming ' + JSON.stringify(jpgResourceTiming));
 | 
			
		||||
 | 
			
		||||
        // Click Close Icon
 | 
			
		||||
        await page.locator('[aria-label="Close"]').click();
 | 
			
		||||
        await page.evaluate(() => window.performance.mark("view-large-close-button"));
 | 
			
		||||
 | 
			
		||||
        //await client.send('HeapProfiler.enable');
 | 
			
		||||
        await client.send('HeapProfiler.collectGarbage');
 | 
			
		||||
 | 
			
		||||
        let performanceMetrics = await client.send('Performance.getMetrics');
 | 
			
		||||
        console.log(performanceMetrics.metrics);
 | 
			
		||||
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@@ -1,119 +0,0 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2022, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
This test suite is an initial example for memory leak testing using performance. This configuration and execution must
 | 
			
		||||
be kept separate from the traditional performance measurements to avoid any "observer" effects associated with tracing
 | 
			
		||||
or profiling playwright and/or the browser.
 | 
			
		||||
 | 
			
		||||
Based on a pattern identified in https://github.com/trentmwillis/devtools-protocol-demos/blob/master/testing-demos/memory-leak-by-heap.js
 | 
			
		||||
and https://github.com/paulirish/automated-chrome-profiling/issues/3
 | 
			
		||||
 | 
			
		||||
Best path forward: https://github.com/cowchimp/headless-devtools/blob/master/src/Memory/example.js
 | 
			
		||||
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
const { test, expect } = require('@playwright/test');
 | 
			
		||||
 | 
			
		||||
const filePath = 'e2e/test-data/PerformanceDisplayLayout.json';
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line playwright/no-skipped-test
 | 
			
		||||
test.describe.skip('Memory Performance tests', () => {
 | 
			
		||||
    test.beforeEach(async ({ page, browser }, testInfo) => {
 | 
			
		||||
        // Go to baseURL
 | 
			
		||||
        await page.goto('/', { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
        // Click a:has-text("My Items")
 | 
			
		||||
        await page.locator('a:has-text("My Items")').click({
 | 
			
		||||
            button: 'right'
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Click text=Import from JSON
 | 
			
		||||
        await page.locator('text=Import from JSON').click();
 | 
			
		||||
 | 
			
		||||
        // Upload Performance Display Layout.json
 | 
			
		||||
        await page.setInputFiles('#fileElem', filePath);
 | 
			
		||||
 | 
			
		||||
        // Click text=OK
 | 
			
		||||
        await page.locator('text=OK').click();
 | 
			
		||||
 | 
			
		||||
        await expect(page.locator('a:has-text("Performance Display Layout Display Layout")')).toBeVisible();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('Embedded View Large for Imagery is performant in Fixed Time', async ({ page, browser }) => {
 | 
			
		||||
 | 
			
		||||
        await page.goto('/', {waitUntil: 'networkidle'});
 | 
			
		||||
 | 
			
		||||
        // To to Search Available after Launch
 | 
			
		||||
        await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
 | 
			
		||||
        // Fill Search input
 | 
			
		||||
        await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Performance Display Layout');
 | 
			
		||||
        //Search Result Appears and is clicked
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            page.waitForNavigation(),
 | 
			
		||||
            page.locator('a:has-text("Performance Display Layout")').first().click()
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        //Time to Example Imagery Frame loads within Display Layout
 | 
			
		||||
        await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible'});
 | 
			
		||||
        //Time to Example Imagery object loads
 | 
			
		||||
        await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible'});
 | 
			
		||||
 | 
			
		||||
        const client = await page.context().newCDPSession(page);
 | 
			
		||||
        await client.send('HeapProfiler.enable');
 | 
			
		||||
        await client.send('HeapProfiler.startSampling');
 | 
			
		||||
        // await client.send('HeapProfiler.collectGarbage');
 | 
			
		||||
        await client.send('Performance.enable');
 | 
			
		||||
 | 
			
		||||
        let performanceMetricsBefore = await client.send('Performance.getMetrics');
 | 
			
		||||
        console.log(performanceMetricsBefore.metrics);
 | 
			
		||||
 | 
			
		||||
        //await client.send('Performance.disable');
 | 
			
		||||
 | 
			
		||||
        //Open Large view
 | 
			
		||||
        await page.locator('button:has-text("Large View")').click();
 | 
			
		||||
        await client.send('HeapProfiler.takeHeapSnapshot');
 | 
			
		||||
 | 
			
		||||
        //Time to Imagery Rendered in Large Frame
 | 
			
		||||
        await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible'});
 | 
			
		||||
 | 
			
		||||
        //Time to Example Imagery object loads
 | 
			
		||||
        await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible'});
 | 
			
		||||
 | 
			
		||||
        // Click Close Icon
 | 
			
		||||
        await page.locator('.c-click-icon').click();
 | 
			
		||||
 | 
			
		||||
        //Time to Example Imagery Frame loads within Display Layout
 | 
			
		||||
        await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible'});
 | 
			
		||||
        //Time to Example Imagery object loads
 | 
			
		||||
        await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible'});
 | 
			
		||||
 | 
			
		||||
        await client.send('HeapProfiler.collectGarbage');
 | 
			
		||||
        //await client.send('Performance.enable');
 | 
			
		||||
 | 
			
		||||
        let performanceMetricsAfter = await client.send('Performance.getMetrics');
 | 
			
		||||
        console.log(performanceMetricsAfter.metrics);
 | 
			
		||||
 | 
			
		||||
        //await client.send('Performance.disable');
 | 
			
		||||
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@@ -1,158 +0,0 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2022, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
This test suite is dedicated to performance tests to ensure that testability of performance
 | 
			
		||||
is not broken upstream on Open MCT. Any assumptions made downstream will be tested here.
 | 
			
		||||
 | 
			
		||||
TODO:
 | 
			
		||||
 - Update resolution of performance config
 | 
			
		||||
 - Add Performance Observer on init to push all performance marks
 | 
			
		||||
 - Move client CDP connection to before or to a fixture
 | 
			
		||||
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
const { test, expect } = require('@playwright/test');
 | 
			
		||||
 | 
			
		||||
const notebookFilePath = 'e2e/test-data/PerformanceNotebook.json';
 | 
			
		||||
 | 
			
		||||
test.describe('Performance tests', () => {
 | 
			
		||||
    test.beforeEach(async ({ page, browser }, testInfo) => {
 | 
			
		||||
        // Go to baseURL
 | 
			
		||||
        await page.goto('/', { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
        // Click a:has-text("My Items")
 | 
			
		||||
        await page.locator('a:has-text("My Items")').click({
 | 
			
		||||
            button: 'right'
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Click text=Import from JSON
 | 
			
		||||
        await page.locator('text=Import from JSON').click();
 | 
			
		||||
 | 
			
		||||
        // Upload Performance Display Layout.json
 | 
			
		||||
        await page.setInputFiles('#fileElem', notebookFilePath);
 | 
			
		||||
 | 
			
		||||
        // TODO Fix this
 | 
			
		||||
        await page.locator('text=OK >> nth=1').click();
 | 
			
		||||
 | 
			
		||||
        await expect(page.locator('a:has-text("Performance Notebook")')).toBeVisible();
 | 
			
		||||
 | 
			
		||||
        //Create a Chrome Performance Timeline trace to store as a test artifact
 | 
			
		||||
        console.log("\n==== Devtools: startTracing ====\n");
 | 
			
		||||
        await browser.startTracing(page, {
 | 
			
		||||
            path: `${testInfo.outputPath()}-trace.json`,
 | 
			
		||||
            screenshots: true
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
    test.afterEach(async ({ page, browser}) => {
 | 
			
		||||
        console.log("\n==== Devtools: stopTracing ====\n");
 | 
			
		||||
        await browser.stopTracing();
 | 
			
		||||
 | 
			
		||||
        /* Measurement Section
 | 
			
		||||
        / The following section includes a block of performance measurements.
 | 
			
		||||
        */
 | 
			
		||||
        const startTime = await page.evaluate(() => window.performance.timing.navigationStart);
 | 
			
		||||
        console.log('window.performance.timing.navigationStart', startTime);
 | 
			
		||||
 | 
			
		||||
        //Get All Performance Marks
 | 
			
		||||
        const getAllMarksJson = await page.evaluate(() => JSON.stringify(window.performance.getEntriesByType("mark")));
 | 
			
		||||
        const getAllMarks = JSON.parse(getAllMarksJson);
 | 
			
		||||
        console.log('window.performance.getEntriesByType("mark")', getAllMarks);
 | 
			
		||||
 | 
			
		||||
        //Get All Performance Measures
 | 
			
		||||
        const getAllMeasuresJson = await page.evaluate(() => JSON.stringify(window.performance.getEntriesByType("measure")));
 | 
			
		||||
        const getAllMeasures = JSON.parse(getAllMeasuresJson);
 | 
			
		||||
        console.log('window.performance.getEntriesByType("measure")', getAllMeasures);
 | 
			
		||||
 | 
			
		||||
    });
 | 
			
		||||
    /* The following test will navigate to a previously created Performance Display Layout and measure the
 | 
			
		||||
    /  following metrics:
 | 
			
		||||
    /  - ElementResourceTiming
 | 
			
		||||
    /  - Interaction Timing
 | 
			
		||||
    */
 | 
			
		||||
    test('Notebook Search, Add Entry, Update Entry are performant', async ({ page, browser }) => {
 | 
			
		||||
        const client = await page.context().newCDPSession(page);
 | 
			
		||||
        // Tell the DevTools session to record performance metrics
 | 
			
		||||
        // https://chromedevtools.github.io/devtools-protocol/tot/Performance/#method-getMetrics
 | 
			
		||||
        await client.send('Performance.enable');
 | 
			
		||||
        // Go to baseURL
 | 
			
		||||
        await page.goto('/');
 | 
			
		||||
 | 
			
		||||
        // To to Search Available after Launch
 | 
			
		||||
        await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
 | 
			
		||||
        await page.evaluate(() => window.performance.mark("search-available"));
 | 
			
		||||
        // Fill Search input
 | 
			
		||||
        await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Performance Notebook');
 | 
			
		||||
        await page.evaluate(() => window.performance.mark("search-entered"));
 | 
			
		||||
        //Search Result Appears and is clicked
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            page.waitForNavigation(),
 | 
			
		||||
            page.locator('a:has-text("Performance Notebook")').first().click(),
 | 
			
		||||
            page.evaluate(() => window.performance.mark("click-search-result"))
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        await page.waitForSelector('.c-tree__item c-tree-and-search__loading loading', {state: 'hidden'});
 | 
			
		||||
        await page.evaluate(() => window.performance.mark("search-spinner-gone"));
 | 
			
		||||
 | 
			
		||||
        await page.waitForSelector('.l-browse-bar__object-name', { state: 'visible'});
 | 
			
		||||
        await page.evaluate(() => window.performance.mark("object-title-appears"));
 | 
			
		||||
 | 
			
		||||
        await page.waitForSelector('.c-notebook__entry >> nth=0', { state: 'visible'});
 | 
			
		||||
        await page.evaluate(() => window.performance.mark("notebook-entry-appears"));
 | 
			
		||||
 | 
			
		||||
        // Click Add new Notebook Entry
 | 
			
		||||
        await page.locator('.c-notebook__drag-area').click();
 | 
			
		||||
        await page.evaluate(() => window.performance.mark("new-notebook-entry-created"));
 | 
			
		||||
 | 
			
		||||
        // Enter Notebook Entry text
 | 
			
		||||
        await page.locator('div.c-ne__text').last().fill('New Entry');
 | 
			
		||||
        await page.keyboard.press('Enter');
 | 
			
		||||
        await page.evaluate(() => window.performance.mark("new-notebook-entry-filled"));
 | 
			
		||||
 | 
			
		||||
        //Individual Notebook Entry Search
 | 
			
		||||
        await page.evaluate(() => window.performance.mark("notebook-search-start"));
 | 
			
		||||
        await page.locator('.c-notebook__search >> input').fill('Existing Entry');
 | 
			
		||||
        await page.evaluate(() => window.performance.mark("notebook-search-filled"));
 | 
			
		||||
        await page.waitForSelector('text=Search Results (3)', { state: 'visible'});
 | 
			
		||||
        await page.evaluate(() => window.performance.mark("notebook-search-processed"));
 | 
			
		||||
        await page.waitForSelector('.c-notebook__entry >> nth=2', { state: 'visible'});
 | 
			
		||||
        await page.evaluate(() => window.performance.mark("notebook-search-processed"));
 | 
			
		||||
 | 
			
		||||
        //Clear Search
 | 
			
		||||
        await page.locator('.c-search.c-notebook__search .c-search__clear-input').click();
 | 
			
		||||
        await page.evaluate(() => window.performance.mark("notebook-search-processed"));
 | 
			
		||||
 | 
			
		||||
        // Hover on Last
 | 
			
		||||
        await page.evaluate(() => window.performance.mark("new-notebook-entry-delete"));
 | 
			
		||||
        await page.locator('div.c-ne__time-and-content').last().hover();
 | 
			
		||||
        await page.locator('button[title="Delete this entry"]').last().click();
 | 
			
		||||
        await page.locator('button:has-text("Ok")').click();
 | 
			
		||||
        await page.waitForSelector('.c-notebook__entry >> nth=3', { state: 'detached'});
 | 
			
		||||
        await page.evaluate(() => window.performance.mark("new-notebook-entry-deleted"));
 | 
			
		||||
 | 
			
		||||
        //await client.send('HeapProfiler.enable');
 | 
			
		||||
        await client.send('HeapProfiler.collectGarbage');
 | 
			
		||||
 | 
			
		||||
        let performanceMetrics = await client.send('Performance.getMetrics');
 | 
			
		||||
        console.log(performanceMetrics.metrics);
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@@ -24,11 +24,12 @@
 | 
			
		||||
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');
 | 
			
		||||
const path = require('path');
 | 
			
		||||
 | 
			
		||||
test.describe('Persistence operations @addInit', () => {
 | 
			
		||||
// https://github.com/nasa/openmct/issues/4323#issuecomment-1067282651
 | 
			
		||||
 | 
			
		||||
test.describe('Persistence operations', () => {
 | 
			
		||||
    // add non persistable root item
 | 
			
		||||
    test.beforeEach(async ({ page }) => {
 | 
			
		||||
        // eslint-disable-next-line no-undef
 | 
			
		||||
@@ -36,10 +37,6 @@ test.describe('Persistence operations @addInit', () => {
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('Persistability should be respected in the create form location field', async ({ page }) => {
 | 
			
		||||
        test.info().annotations.push({
 | 
			
		||||
            type: 'issue',
 | 
			
		||||
            description: 'https://github.com/nasa/openmct/issues/4323'
 | 
			
		||||
        });
 | 
			
		||||
        // Go to baseURL
 | 
			
		||||
        await page.goto('/', { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -24,10 +24,7 @@
 | 
			
		||||
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');
 | 
			
		||||
const { test, 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 }) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -24,10 +24,7 @@
 | 
			
		||||
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');
 | 
			
		||||
const { test, expect } = require('@playwright/test');
 | 
			
		||||
 | 
			
		||||
test.describe('ExportAsJSON', () => {
 | 
			
		||||
    test.fixme('Verify that domain object can be importAsJSON from Tree', async ({ page }) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -24,8 +24,7 @@
 | 
			
		||||
This test suite is dedicated to tests which verify the basic operations surrounding Clock.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
const { test } = require('../../../fixtures.js');
 | 
			
		||||
const { expect } = require('@playwright/test');
 | 
			
		||||
const { test, expect } = require('@playwright/test');
 | 
			
		||||
 | 
			
		||||
test.describe('Clock Generator', () => {
 | 
			
		||||
 | 
			
		||||
@@ -46,22 +45,22 @@ test.describe('Clock Generator', () => {
 | 
			
		||||
        // Click .icon-arrow-down
 | 
			
		||||
        await page.locator('.icon-arrow-down').click();
 | 
			
		||||
        //verify if the autocomplete dropdown is visible
 | 
			
		||||
        await expect(page.locator(".c-input--autocomplete__options")).toBeVisible();
 | 
			
		||||
        await expect(page.locator(".optionPreSelected")).toBeVisible();
 | 
			
		||||
        // Click .icon-arrow-down
 | 
			
		||||
        await page.locator('.icon-arrow-down').click();
 | 
			
		||||
 | 
			
		||||
        // Verify clicking on the autocomplete arrow collapses the dropdown
 | 
			
		||||
        await expect(page.locator(".c-input--autocomplete__options")).not.toBeVisible();
 | 
			
		||||
        await expect(page.locator(".optionPreSelected")).not.toBeVisible();
 | 
			
		||||
 | 
			
		||||
        // Click timezone input to open dropdown
 | 
			
		||||
        await page.locator('.c-input--autocomplete__input').click();
 | 
			
		||||
        await page.locator('.autocompleteInput').click();
 | 
			
		||||
        //verify if the autocomplete dropdown is visible
 | 
			
		||||
        await expect(page.locator(".c-input--autocomplete__options")).toBeVisible();
 | 
			
		||||
        await expect(page.locator(".optionPreSelected")).toBeVisible();
 | 
			
		||||
 | 
			
		||||
        // Verify clicking outside the autocomplete dropdown collapses it
 | 
			
		||||
        await page.locator('text=Timezone').click();
 | 
			
		||||
        // Verify clicking on the autocomplete arrow collapses the dropdown
 | 
			
		||||
        await expect(page.locator(".c-input--autocomplete__options")).not.toBeVisible();
 | 
			
		||||
        await expect(page.locator(".optionPreSelected")).not.toBeVisible();
 | 
			
		||||
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -26,53 +26,50 @@ suite is sharing state between tests which is considered an anti-pattern. Implim
 | 
			
		||||
demonstrate some playwright for test developers. This pattern should not be re-used in other CRUD suites.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
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 text=OK
 | 
			
		||||
    await Promise.all([
 | 
			
		||||
        page.waitForNavigation(),
 | 
			
		||||
        page.click('text=OK')
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
 | 
			
		||||
    //Save localStorage for future test execution
 | 
			
		||||
    await context.storageState({ path: './e2e/tests/recycled_storage.json' });
 | 
			
		||||
 | 
			
		||||
    //Set object identifier from url
 | 
			
		||||
    conditionSetUrl = await page.url();
 | 
			
		||||
    console.log('conditionSetUrl ' + conditionSetUrl);
 | 
			
		||||
 | 
			
		||||
    getConditionSetIdentifierFromUrl = await conditionSetUrl.split('/').pop().split('?')[0];
 | 
			
		||||
    console.log('getConditionSetIdentifierFromUrl ' + getConditionSetIdentifierFromUrl);
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
 | 
			
		||||
    test.beforeAll(async ({ browser}) => {
 | 
			
		||||
        const context = await browser.newContext();
 | 
			
		||||
        const page = await context.newPage();
 | 
			
		||||
        //Go to baseURL
 | 
			
		||||
        await page.goto('/', { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
        //Click the Create button
 | 
			
		||||
        await page.click('button:has-text("Create")');
 | 
			
		||||
 | 
			
		||||
        // Click text=Condition Set
 | 
			
		||||
        await page.locator('li:has-text("Condition Set")').click();
 | 
			
		||||
 | 
			
		||||
        // Click text=OK
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            page.waitForNavigation(),
 | 
			
		||||
            page.click('text=OK')
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        //Save localStorage for future test execution
 | 
			
		||||
        await context.storageState({ path: './e2e/test-data/recycled_local_storage.json' });
 | 
			
		||||
 | 
			
		||||
        //Set object identifier from url
 | 
			
		||||
        conditionSetUrl = await page.url();
 | 
			
		||||
        console.log('conditionSetUrl ' + conditionSetUrl);
 | 
			
		||||
 | 
			
		||||
        getConditionSetIdentifierFromUrl = await conditionSetUrl.split('/').pop().split('?')[0];
 | 
			
		||||
        console.debug('getConditionSetIdentifierFromUrl ' + getConditionSetIdentifierFromUrl);
 | 
			
		||||
        await page.close();
 | 
			
		||||
    });
 | 
			
		||||
    test.afterAll(async ({ browser }) => {
 | 
			
		||||
        await browser.close();
 | 
			
		||||
    });
 | 
			
		||||
    //Load localStorage for subsequent tests
 | 
			
		||||
    test.use({ storageState: './e2e/test-data/recycled_local_storage.json' });
 | 
			
		||||
    test.use({ storageState: './e2e/tests/recycled_storage.json' });
 | 
			
		||||
 | 
			
		||||
    //Begin suite of tests again localStorage
 | 
			
		||||
    test('Condition set object properties persist in main view and inspector @localStorage', async ({ page }) => {
 | 
			
		||||
    test('Condition set object properties persist in main view and inspector', async ({ page }) => {
 | 
			
		||||
        //Navigate to baseURL with injected localStorage
 | 
			
		||||
        await page.goto(conditionSetUrl, { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
        //Assertions on loaded Condition Set in main view. This is a stateful transition step after page.goto()
 | 
			
		||||
        //Assertions on loaded Condition Set in main view
 | 
			
		||||
        await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
 | 
			
		||||
 | 
			
		||||
        //Assertions on loaded Condition Set in Inspector
 | 
			
		||||
@@ -93,7 +90,7 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
 | 
			
		||||
    test('condition set object can be modified on @localStorage', async ({ page }) => {
 | 
			
		||||
        await page.goto(conditionSetUrl, { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
        //Assertions on loaded Condition Set in main view. This is a stateful transition step after page.goto()
 | 
			
		||||
        //Assertions on loaded Condition Set in main view
 | 
			
		||||
        await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
 | 
			
		||||
 | 
			
		||||
        //Update the Condition Set properties
 | 
			
		||||
@@ -123,7 +120,7 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
 | 
			
		||||
        // 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('[aria-label="OpenMCT Search"] input[type="search"]').fill('Renamed');
 | 
			
		||||
        await page.locator('input[type="search"]').fill('Renamed');
 | 
			
		||||
        await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
 | 
			
		||||
 | 
			
		||||
        //Reload Page
 | 
			
		||||
@@ -147,33 +144,35 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
 | 
			
		||||
        // 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('[aria-label="OpenMCT Search"] input[type="search"]').fill('Renamed');
 | 
			
		||||
        await page.locator('input[type="search"]').fill('Renamed');
 | 
			
		||||
        await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
 | 
			
		||||
    });
 | 
			
		||||
    test('condition set object can be deleted by Search Tree Actions menu on @localStorage', async ({ page }) => {
 | 
			
		||||
        //Navigate to baseURL
 | 
			
		||||
        await page.goto('/', { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
        //Assertions on loaded Condition Set in main view. This is a stateful transition step after page.goto()
 | 
			
		||||
        await expect(page.locator('a:has-text("Unnamed Condition Set Condition Set") >> nth=0')).toBeVisible();
 | 
			
		||||
 | 
			
		||||
        const numberOfConditionSetsToStart = await page.locator('a:has-text("Unnamed Condition Set Condition Set")').count();
 | 
			
		||||
        //Expect Unnamed Condition Set to be visible in Main View
 | 
			
		||||
        await expect(page.locator('a:has-text("Unnamed Condition Set Condition Set")')).toBeVisible();
 | 
			
		||||
 | 
			
		||||
        // Search for Unnamed Condition Set
 | 
			
		||||
        await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Unnamed Condition Set');
 | 
			
		||||
        // Click Search Result
 | 
			
		||||
        await page.locator('[aria-label="OpenMCT Search"] >> text=Unnamed Condition Set').first().click();
 | 
			
		||||
        // Click hamburger button
 | 
			
		||||
        await page.locator('[title="More options"]').click();
 | 
			
		||||
 | 
			
		||||
        // Click text=Remove
 | 
			
		||||
        await page.locator('input[type="search"]').fill('Unnamed Condition Set');
 | 
			
		||||
        // Right Click to Open Actions Menu
 | 
			
		||||
        await page.locator('a:has-text("Unnamed Condition Set")').click({
 | 
			
		||||
            button: 'right'
 | 
			
		||||
        });
 | 
			
		||||
        // Click Remove Action
 | 
			
		||||
        await page.locator('text=Remove').click();
 | 
			
		||||
 | 
			
		||||
        await page.locator('text=OK').click();
 | 
			
		||||
 | 
			
		||||
        //Expect Unnamed Condition Set to be removed in Main View
 | 
			
		||||
        const numberOfConditionSetsAtEnd = await page.locator('a:has-text("Unnamed Condition Set Condition Set")').count();
 | 
			
		||||
        await expect(page.locator('a:has-text("Unnamed Condition Set Condition Set")')).not.toBeVisible();
 | 
			
		||||
 | 
			
		||||
        expect(numberOfConditionSetsAtEnd).toEqual(numberOfConditionSetsToStart - 1);
 | 
			
		||||
        await page.locator('.c-search__clear-input').click();
 | 
			
		||||
        // Search for Unnamed Condition Set
 | 
			
		||||
        await page.locator('input[type="search"]').fill('Unnamed Condition Set');
 | 
			
		||||
        // Expect Unnamed Condition Set to be removed
 | 
			
		||||
        await expect(page.locator('a:has-text("Unnamed Condition Set")')).not.toBeVisible();
 | 
			
		||||
 | 
			
		||||
        //Feature?
 | 
			
		||||
        //Domain Object is still available by direct URL after delete
 | 
			
		||||
 
 | 
			
		||||
@@ -26,15 +26,12 @@ but only assume that example imagery is present.
 | 
			
		||||
*/
 | 
			
		||||
/* globals process */
 | 
			
		||||
 | 
			
		||||
const { test } = require('../../../fixtures.js');
 | 
			
		||||
const { expect } = require('@playwright/test');
 | 
			
		||||
const { test, expect } = require('@playwright/test');
 | 
			
		||||
 | 
			
		||||
const backgroundImageSelector = '.c-imagery__main-image__background-image';
 | 
			
		||||
 | 
			
		||||
//The following block of tests verifies the basic functionality of example imagery and serves as a template for Imagery objects embedded in other objects.
 | 
			
		||||
test.describe('Example Imagery Object', () => {
 | 
			
		||||
test.describe('Example Imagery', () => {
 | 
			
		||||
 | 
			
		||||
    test.beforeEach(async ({ page }) => {
 | 
			
		||||
        page.on('console', msg => console.log(msg.text()));
 | 
			
		||||
        //Go to baseURL
 | 
			
		||||
        await page.goto('/', { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
@@ -51,30 +48,28 @@ test.describe('Example Imagery Object', () => {
 | 
			
		||||
            //Wait for Save Banner to appear
 | 
			
		||||
            page.waitForSelector('.c-message-banner__message')
 | 
			
		||||
        ]);
 | 
			
		||||
        // Close Banner
 | 
			
		||||
        await page.locator('.c-message-banner__close-button').click();
 | 
			
		||||
 | 
			
		||||
        //Wait until Save Banner is gone
 | 
			
		||||
        await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
 | 
			
		||||
        await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');
 | 
			
		||||
        await page.locator(backgroundImageSelector).hover({trial: true});
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const backgroundImageSelector = '.c-imagery__main-image__background-image';
 | 
			
		||||
    test('Can use Mouse Wheel to zoom in and out of latest image', async ({ page }) => {
 | 
			
		||||
        const bgImageLocator = await page.locator(backgroundImageSelector);
 | 
			
		||||
        const deltaYStep = 100; //equivalent to 1x zoom
 | 
			
		||||
        await page.locator(backgroundImageSelector).hover({trial: true});
 | 
			
		||||
        await bgImageLocator.hover();
 | 
			
		||||
        const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox();
 | 
			
		||||
        // zoom in
 | 
			
		||||
        await page.locator(backgroundImageSelector).hover({trial: true});
 | 
			
		||||
        await bgImageLocator.hover();
 | 
			
		||||
        await page.mouse.wheel(0, deltaYStep * 2);
 | 
			
		||||
        // wait for zoom animation to finish
 | 
			
		||||
        await page.locator(backgroundImageSelector).hover({trial: true});
 | 
			
		||||
        await bgImageLocator.hover();
 | 
			
		||||
        const imageMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox();
 | 
			
		||||
        // zoom out
 | 
			
		||||
        await page.locator(backgroundImageSelector).hover({trial: true});
 | 
			
		||||
        await bgImageLocator.hover();
 | 
			
		||||
        await page.mouse.wheel(0, -deltaYStep);
 | 
			
		||||
        // wait for zoom animation to finish
 | 
			
		||||
        await page.locator(backgroundImageSelector).hover({trial: true});
 | 
			
		||||
        await bgImageLocator.hover();
 | 
			
		||||
        const imageMouseZoomedOut = await page.locator(backgroundImageSelector).boundingBox();
 | 
			
		||||
 | 
			
		||||
        expect(imageMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height);
 | 
			
		||||
@@ -88,12 +83,13 @@ test.describe('Example Imagery Object', () => {
 | 
			
		||||
        const deltaYStep = 100; //equivalent to 1x zoom
 | 
			
		||||
        const panHotkey = process.platform === 'linux' ? ['Control', 'Alt'] : ['Alt'];
 | 
			
		||||
 | 
			
		||||
        await page.locator(backgroundImageSelector).hover({trial: true});
 | 
			
		||||
        const bgImageLocator = await page.locator(backgroundImageSelector);
 | 
			
		||||
        await bgImageLocator.hover();
 | 
			
		||||
 | 
			
		||||
        // zoom in
 | 
			
		||||
        await page.mouse.wheel(0, deltaYStep * 2);
 | 
			
		||||
        await page.locator(backgroundImageSelector).hover({trial: true});
 | 
			
		||||
        const zoomedBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
 | 
			
		||||
        await bgImageLocator.hover();
 | 
			
		||||
        const zoomedBoundingBox = await bgImageLocator.boundingBox();
 | 
			
		||||
        const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;
 | 
			
		||||
        const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
 | 
			
		||||
        // move to the right
 | 
			
		||||
@@ -116,7 +112,7 @@ test.describe('Example Imagery Object', () => {
 | 
			
		||||
        await page.mouse.move(imageCenterX - 200, imageCenterY, 10);
 | 
			
		||||
        await page.mouse.up();
 | 
			
		||||
        await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
 | 
			
		||||
        const afterRightPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
 | 
			
		||||
        const afterRightPanBoundingBox = await bgImageLocator.boundingBox();
 | 
			
		||||
        expect(zoomedBoundingBox.x).toBeGreaterThan(afterRightPanBoundingBox.x);
 | 
			
		||||
 | 
			
		||||
        // pan left
 | 
			
		||||
@@ -125,7 +121,7 @@ test.describe('Example Imagery Object', () => {
 | 
			
		||||
        await page.mouse.move(imageCenterX, imageCenterY, 10);
 | 
			
		||||
        await page.mouse.up();
 | 
			
		||||
        await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
 | 
			
		||||
        const afterLeftPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
 | 
			
		||||
        const afterLeftPanBoundingBox = await bgImageLocator.boundingBox();
 | 
			
		||||
        expect(afterRightPanBoundingBox.x).toBeLessThan(afterLeftPanBoundingBox.x);
 | 
			
		||||
 | 
			
		||||
        // pan up
 | 
			
		||||
@@ -135,7 +131,7 @@ test.describe('Example Imagery Object', () => {
 | 
			
		||||
        await page.mouse.move(imageCenterX, imageCenterY + 200, 10);
 | 
			
		||||
        await page.mouse.up();
 | 
			
		||||
        await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
 | 
			
		||||
        const afterUpPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
 | 
			
		||||
        const afterUpPanBoundingBox = await bgImageLocator.boundingBox();
 | 
			
		||||
        expect(afterUpPanBoundingBox.y).toBeGreaterThan(afterLeftPanBoundingBox.y);
 | 
			
		||||
 | 
			
		||||
        // pan down
 | 
			
		||||
@@ -144,58 +140,60 @@ test.describe('Example Imagery Object', () => {
 | 
			
		||||
        await page.mouse.move(imageCenterX, imageCenterY - 200, 10);
 | 
			
		||||
        await page.mouse.up();
 | 
			
		||||
        await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
 | 
			
		||||
        const afterDownPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
 | 
			
		||||
        const afterDownPanBoundingBox = await bgImageLocator.boundingBox();
 | 
			
		||||
        expect(afterDownPanBoundingBox.y).toBeLessThan(afterUpPanBoundingBox.y);
 | 
			
		||||
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('Can use + - buttons to zoom on the image', async ({ page }) => {
 | 
			
		||||
        await page.locator(backgroundImageSelector).hover({trial: true});
 | 
			
		||||
        const zoomInBtn = page.locator('.t-btn-zoom-in').nth(0);
 | 
			
		||||
        const zoomOutBtn = page.locator('.t-btn-zoom-out').nth(0);
 | 
			
		||||
        const initialBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
 | 
			
		||||
        const bgImageLocator = await page.locator(backgroundImageSelector);
 | 
			
		||||
        await bgImageLocator.hover();
 | 
			
		||||
        const zoomInBtn = await page.locator('.t-btn-zoom-in');
 | 
			
		||||
        const zoomOutBtn = await page.locator('.t-btn-zoom-out');
 | 
			
		||||
        const initialBoundingBox = await bgImageLocator.boundingBox();
 | 
			
		||||
 | 
			
		||||
        await zoomInBtn.click();
 | 
			
		||||
        await zoomInBtn.click();
 | 
			
		||||
        // wait for zoom animation to finish
 | 
			
		||||
        await page.locator(backgroundImageSelector).hover({trial: true});
 | 
			
		||||
        const zoomedInBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
 | 
			
		||||
        await bgImageLocator.hover();
 | 
			
		||||
        const zoomedInBoundingBox = await bgImageLocator.boundingBox();
 | 
			
		||||
        expect(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
 | 
			
		||||
        expect(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);
 | 
			
		||||
 | 
			
		||||
        await zoomOutBtn.click();
 | 
			
		||||
        // wait for zoom animation to finish
 | 
			
		||||
        await page.locator(backgroundImageSelector).hover({trial: true});
 | 
			
		||||
        const zoomedOutBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
 | 
			
		||||
        await bgImageLocator.hover();
 | 
			
		||||
        const zoomedOutBoundingBox = await bgImageLocator.boundingBox();
 | 
			
		||||
        expect(zoomedOutBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height);
 | 
			
		||||
        expect(zoomedOutBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width);
 | 
			
		||||
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('Can use the reset button to reset the image', async ({ page }) => {
 | 
			
		||||
        const bgImageLocator = await page.locator(backgroundImageSelector);
 | 
			
		||||
        // wait for zoom animation to finish
 | 
			
		||||
        await page.locator(backgroundImageSelector).hover({trial: true});
 | 
			
		||||
        await bgImageLocator.hover();
 | 
			
		||||
 | 
			
		||||
        const zoomInBtn = page.locator('.t-btn-zoom-in').nth(0);
 | 
			
		||||
        const zoomResetBtn = page.locator('.t-btn-zoom-reset').nth(0);
 | 
			
		||||
        const initialBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
 | 
			
		||||
        const zoomInBtn = await page.locator('.t-btn-zoom-in');
 | 
			
		||||
        const zoomResetBtn = await page.locator('.t-btn-zoom-reset');
 | 
			
		||||
        const initialBoundingBox = await bgImageLocator.boundingBox();
 | 
			
		||||
 | 
			
		||||
        await zoomInBtn.click();
 | 
			
		||||
        // wait for zoom animation to finish
 | 
			
		||||
        await page.locator(backgroundImageSelector).hover({trial: true});
 | 
			
		||||
        await bgImageLocator.hover();
 | 
			
		||||
        await zoomInBtn.click();
 | 
			
		||||
        // wait for zoom animation to finish
 | 
			
		||||
        await page.locator(backgroundImageSelector).hover({trial: true});
 | 
			
		||||
        await bgImageLocator.hover();
 | 
			
		||||
 | 
			
		||||
        const zoomedInBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
 | 
			
		||||
        const zoomedInBoundingBox = await bgImageLocator.boundingBox();
 | 
			
		||||
        expect.soft(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
 | 
			
		||||
        expect.soft(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);
 | 
			
		||||
 | 
			
		||||
        await zoomResetBtn.click();
 | 
			
		||||
        // wait for zoom animation to finish
 | 
			
		||||
        await page.locator(backgroundImageSelector).hover({trial: true});
 | 
			
		||||
        await bgImageLocator.hover();
 | 
			
		||||
 | 
			
		||||
        const resetBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
 | 
			
		||||
        const resetBoundingBox = await bgImageLocator.boundingBox();
 | 
			
		||||
        expect.soft(resetBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height);
 | 
			
		||||
        expect.soft(resetBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width);
 | 
			
		||||
 | 
			
		||||
@@ -203,359 +201,30 @@ test.describe('Example Imagery Object', () => {
 | 
			
		||||
        expect(resetBoundingBox.width).toEqual(initialBoundingBox.width);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('Using the zoom features does not pause telemetry', async ({ page }) => {
 | 
			
		||||
        const pausePlayButton = page.locator('.c-button.pause-play');
 | 
			
		||||
        // wait for zoom animation to finish
 | 
			
		||||
        await page.locator(backgroundImageSelector).hover({trial: true});
 | 
			
		||||
 | 
			
		||||
        // open the time conductor drop down
 | 
			
		||||
        await page.locator('button:has-text("Fixed Timespan")').click();
 | 
			
		||||
        // Click local clock
 | 
			
		||||
        await page.locator('[data-testid="conductor-modeOption-realtime"]').click();
 | 
			
		||||
 | 
			
		||||
        await expect.soft(pausePlayButton).not.toHaveClass(/is-paused/);
 | 
			
		||||
        const zoomInBtn = page.locator('.t-btn-zoom-in').nth(0);
 | 
			
		||||
        await zoomInBtn.click();
 | 
			
		||||
        // wait for zoom animation to finish
 | 
			
		||||
        await page.locator(backgroundImageSelector).hover({trial: true});
 | 
			
		||||
 | 
			
		||||
        return expect(pausePlayButton).not.toHaveClass(/is-paused/);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    //test.fixme('Can use Mouse Wheel to zoom in and out of previous image');
 | 
			
		||||
    //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');
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 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');
 | 
			
		||||
test('Example Imagery in Display layout', async ({ page, browserName }) => {
 | 
			
		||||
    test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox');
 | 
			
		||||
    test.info().annotations.push({
 | 
			
		||||
        type: 'issue',
 | 
			
		||||
        description: 'https://github.com/nasa/openmct/issues/5265'
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // 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');
 | 
			
		||||
    await page.locator(backgroundImageSelector).hover({trial: true});
 | 
			
		||||
 | 
			
		||||
    // 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 page.locator(backgroundImageSelector).hover({trial: true});
 | 
			
		||||
    const deltaYStep = 100; // equivalent to 1x zoom
 | 
			
		||||
    await page.mouse.wheel(0, deltaYStep * 2);
 | 
			
		||||
    const zoomedBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
 | 
			
		||||
    const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;
 | 
			
		||||
    const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
 | 
			
		||||
 | 
			
		||||
    // Wait for zoom animation to finish
 | 
			
		||||
    await page.locator(backgroundImageSelector).hover({trial: true});
 | 
			
		||||
    const imageMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox();
 | 
			
		||||
    expect(imageMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height);
 | 
			
		||||
    expect(imageMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width);
 | 
			
		||||
 | 
			
		||||
    // 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 page.locator(backgroundImageSelector).hover({trial: true});
 | 
			
		||||
    await page.mouse.wheel(0, deltaYStep * 2);
 | 
			
		||||
 | 
			
		||||
    // Wait for zoom animation to finish
 | 
			
		||||
    await page.locator(backgroundImageSelector).hover({trial: true});
 | 
			
		||||
    const imageNextMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox();
 | 
			
		||||
    expect(imageNextMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height);
 | 
			
		||||
    expect(imageNextMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width);
 | 
			
		||||
 | 
			
		||||
    // Click previous image button
 | 
			
		||||
    await previousImageButton.click();
 | 
			
		||||
 | 
			
		||||
    // Verify previous image
 | 
			
		||||
    await expect(selectedImage).toBeVisible();
 | 
			
		||||
 | 
			
		||||
    const imageCount = await page.locator('.c-imagery__thumb').count();
 | 
			
		||||
    await expect.poll(async () => {
 | 
			
		||||
        const newImageCount = await page.locator('.c-imagery__thumb').count();
 | 
			
		||||
 | 
			
		||||
        return newImageCount;
 | 
			
		||||
    }, {
 | 
			
		||||
        message: "verify that old images are discarded",
 | 
			
		||||
        timeout: 6 * 1000
 | 
			
		||||
    }).toBe(imageCount);
 | 
			
		||||
 | 
			
		||||
    // Verify selected image is still displayed
 | 
			
		||||
    await expect(selectedImage).toBeVisible();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
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 Display 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 Flexible layout', () => {
 | 
			
		||||
    test('Example Imagery in Flexible layout', async ({ page, browserName }) => {
 | 
			
		||||
        test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox');
 | 
			
		||||
        test.info().annotations.push({
 | 
			
		||||
            type: 'issue',
 | 
			
		||||
            description: 'https://github.com/nasa/openmct/issues/5326'
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Go to baseURL
 | 
			
		||||
        await page.goto('/', { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
        // Click the Create button
 | 
			
		||||
        await page.click('button:has-text("Create")');
 | 
			
		||||
 | 
			
		||||
        // Click text=Example Imagery
 | 
			
		||||
        await page.click('text=Example Imagery');
 | 
			
		||||
 | 
			
		||||
        // Clear and set Image load delay (milliseconds)
 | 
			
		||||
        await page.click('input[type="number"]', {clickCount: 3});
 | 
			
		||||
        await page.type('input[type="number"]', "20");
 | 
			
		||||
 | 
			
		||||
        // Click text=OK
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            page.waitForNavigation({waitUntil: 'networkidle'}),
 | 
			
		||||
            page.click('text=OK'),
 | 
			
		||||
            //Wait for Save Banner to appear
 | 
			
		||||
            page.waitForSelector('.c-message-banner__message')
 | 
			
		||||
        ]);
 | 
			
		||||
        // Wait until Save Banner is gone
 | 
			
		||||
        await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
 | 
			
		||||
        await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');
 | 
			
		||||
        await page.locator(backgroundImageSelector).hover({trial: true});
 | 
			
		||||
 | 
			
		||||
        // Click the Create button
 | 
			
		||||
        await page.click('button:has-text("Create")');
 | 
			
		||||
 | 
			
		||||
        // Click text=Flexible Layout
 | 
			
		||||
        await page.click('text=Flexible Layout');
 | 
			
		||||
 | 
			
		||||
        // Assert Flexable layout
 | 
			
		||||
        await expect(page.locator('.js-form-title')).toHaveText('Create a New Flexible Layout');
 | 
			
		||||
 | 
			
		||||
        await page.locator('form[name="mctForm"] >> text=My Items').click();
 | 
			
		||||
 | 
			
		||||
        // Click My Items
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            page.locator('text=OK').click(),
 | 
			
		||||
            page.waitForNavigation({waitUntil: 'networkidle'})
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        // Click My Items
 | 
			
		||||
        await page.locator('.c-disclosure-triangle').click();
 | 
			
		||||
 | 
			
		||||
        // Right click example imagery
 | 
			
		||||
        await page.click(('text=Unnamed Example Imagery'), { button: 'right' });
 | 
			
		||||
 | 
			
		||||
        // Click move
 | 
			
		||||
        await page.locator('.icon-move').click();
 | 
			
		||||
 | 
			
		||||
        // Click triangle to open sub menu
 | 
			
		||||
        await page.locator('.c-form__section .c-disclosure-triangle').click();
 | 
			
		||||
 | 
			
		||||
        // Click Flexable Layout
 | 
			
		||||
        await page.click('.c-overlay__outer >> text=Unnamed Flexible Layout');
 | 
			
		||||
 | 
			
		||||
        // Click text=OK
 | 
			
		||||
        await page.locator('text=OK').click();
 | 
			
		||||
 | 
			
		||||
        // Save template
 | 
			
		||||
        await saveTemplate(page);
 | 
			
		||||
 | 
			
		||||
        // Zoom in
 | 
			
		||||
        await mouseZoomIn(page);
 | 
			
		||||
 | 
			
		||||
        // Center the mouse pointer
 | 
			
		||||
        const zoomedBoundingBox = await await page.locator(backgroundImageSelector).boundingBox();
 | 
			
		||||
        const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;
 | 
			
		||||
        const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
 | 
			
		||||
        await page.mouse.move(imageCenterX, imageCenterY);
 | 
			
		||||
 | 
			
		||||
        // Pan zoom
 | 
			
		||||
        await panZoomAndAssertImageProperties(page);
 | 
			
		||||
 | 
			
		||||
        // Click previous image button
 | 
			
		||||
        const previousImageButton = page.locator('.c-nav--prev');
 | 
			
		||||
        await previousImageButton.click();
 | 
			
		||||
 | 
			
		||||
        // Verify previous image
 | 
			
		||||
        const selectedImage = page.locator('.selected');
 | 
			
		||||
        await expect(selectedImage).toBeVisible();
 | 
			
		||||
 | 
			
		||||
        // Click time conductor mode button
 | 
			
		||||
        await page.locator('.c-mode-button').click();
 | 
			
		||||
 | 
			
		||||
        // Select local clock mode
 | 
			
		||||
        await page.locator('[data-testid=conductor-modeOption-realtime]').click();
 | 
			
		||||
 | 
			
		||||
        // Zoom in on next image
 | 
			
		||||
        await mouseZoomIn(page);
 | 
			
		||||
 | 
			
		||||
        // Click previous image button
 | 
			
		||||
        await previousImageButton.click();
 | 
			
		||||
 | 
			
		||||
        // Verify previous image
 | 
			
		||||
        await expect(selectedImage).toBeVisible();
 | 
			
		||||
 | 
			
		||||
        const imageCount = await page.locator('.c-imagery__thumb').count();
 | 
			
		||||
        await expect.poll(async () => {
 | 
			
		||||
            const newImageCount = await page.locator('.c-imagery__thumb').count();
 | 
			
		||||
 | 
			
		||||
            return newImageCount;
 | 
			
		||||
        }, {
 | 
			
		||||
            message: "verify that old images are discarded",
 | 
			
		||||
            timeout: 6 * 1000
 | 
			
		||||
        }).toBe(imageCount);
 | 
			
		||||
 | 
			
		||||
        // Verify selected image is still displayed
 | 
			
		||||
        await expect(selectedImage).toBeVisible();
 | 
			
		||||
 | 
			
		||||
        // Unpause imagery
 | 
			
		||||
        await page.locator('.pause-play').click();
 | 
			
		||||
 | 
			
		||||
        //Get background-image url from background-image css prop
 | 
			
		||||
        await assertBackgroundImageUrlFromBackgroundCss(page);
 | 
			
		||||
 | 
			
		||||
        // Open the image filter menu
 | 
			
		||||
        await page.locator('[role=toolbar] button[title="Brightness and contrast"]').click();
 | 
			
		||||
 | 
			
		||||
        // Drag the brightness and contrast sliders around and assert filter values
 | 
			
		||||
        await dragBrightnessSliderAndAssertFilterValues(page);
 | 
			
		||||
        await dragContrastSliderAndAssertFilterValues(page);
 | 
			
		||||
    });
 | 
			
		||||
    test.fixme('Can use Mouse Wheel to zoom in and out of previous image');
 | 
			
		||||
    test.fixme('Can use alt+drag to move around image once zoomed in');
 | 
			
		||||
    test.fixme('Can zoom into the latest image and the real-time/fixed-time imagery will pause');
 | 
			
		||||
    test.fixme('Clicking on the left arrow should pause the imagery and go to previous image');
 | 
			
		||||
    test.fixme('If the imagery view is in pause mode, it should not be updated when new images come in');
 | 
			
		||||
    test.fixme('If the imagery view is not in pause mode, it should be updated when new images come in');
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test.describe('Example Imagery in Tabs view', () => {
 | 
			
		||||
@@ -567,185 +236,3 @@ test.describe('Example Imagery in Tabs view', () => {
 | 
			
		||||
    test.fixme('If the imagery view is in pause mode, it should not be updated when new images come in');
 | 
			
		||||
    test.fixme('If the imagery view is not in pause mode, it should be updated when new images come in');
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {import('@playwright/test').Page} page
 | 
			
		||||
 */
 | 
			
		||||
async function saveTemplate(page) {
 | 
			
		||||
    await page.locator('.c-button--menu.c-button--major.icon-save').click();
 | 
			
		||||
    await page.locator('text=Save and Finish Editing').click();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Drag the brightness slider to max, min, and midpoint and assert the filter values
 | 
			
		||||
 * @param {import('@playwright/test').Page} page
 | 
			
		||||
 */
 | 
			
		||||
async function dragBrightnessSliderAndAssertFilterValues(page) {
 | 
			
		||||
    const brightnessSlider = 'div.c-image-controls__slider-wrapper.icon-brightness > input';
 | 
			
		||||
    const brightnessBoundingBox = await page.locator(brightnessSlider).boundingBox();
 | 
			
		||||
    const brightnessMidX = brightnessBoundingBox.x + brightnessBoundingBox.width / 2;
 | 
			
		||||
    const brightnessMidY = brightnessBoundingBox.y + brightnessBoundingBox.height / 2;
 | 
			
		||||
 | 
			
		||||
    await page.locator(brightnessSlider).hover({trial: true});
 | 
			
		||||
    await page.mouse.down();
 | 
			
		||||
    await page.mouse.move(brightnessBoundingBox.x + brightnessBoundingBox.width, brightnessMidY);
 | 
			
		||||
    await assertBackgroundImageBrightness(page, '500');
 | 
			
		||||
    await page.mouse.move(brightnessBoundingBox.x, brightnessMidY);
 | 
			
		||||
    await assertBackgroundImageBrightness(page, '0');
 | 
			
		||||
    await page.mouse.move(brightnessMidX, brightnessMidY);
 | 
			
		||||
    await assertBackgroundImageBrightness(page, '250');
 | 
			
		||||
    await page.mouse.up();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Drag the contrast slider to max, min, and midpoint and assert the filter values
 | 
			
		||||
 * @param {import('@playwright/test').Page} page
 | 
			
		||||
 */
 | 
			
		||||
async function dragContrastSliderAndAssertFilterValues(page) {
 | 
			
		||||
    const contrastSlider = 'div.c-image-controls__slider-wrapper.icon-contrast > input';
 | 
			
		||||
    const contrastBoundingBox = await page.locator(contrastSlider).boundingBox();
 | 
			
		||||
    const contrastMidX = contrastBoundingBox.x + contrastBoundingBox.width / 2;
 | 
			
		||||
    const contrastMidY = contrastBoundingBox.y + contrastBoundingBox.height / 2;
 | 
			
		||||
 | 
			
		||||
    await page.locator(contrastSlider).hover({trial: true});
 | 
			
		||||
    await page.mouse.down();
 | 
			
		||||
    await page.mouse.move(contrastBoundingBox.x + contrastBoundingBox.width, contrastMidY);
 | 
			
		||||
    await assertBackgroundImageContrast(page, '500');
 | 
			
		||||
    await page.mouse.move(contrastBoundingBox.x, contrastMidY);
 | 
			
		||||
    await assertBackgroundImageContrast(page, '0');
 | 
			
		||||
    await page.mouse.move(contrastMidX, contrastMidY);
 | 
			
		||||
    await assertBackgroundImageContrast(page, '250');
 | 
			
		||||
    await page.mouse.up();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Gets the filter:brightness value of the current background-image and
 | 
			
		||||
 * asserts against an expected value
 | 
			
		||||
 * @param {import('@playwright/test').Page} page
 | 
			
		||||
 * @param {String} expected The expected brightness value
 | 
			
		||||
 */
 | 
			
		||||
async function assertBackgroundImageBrightness(page, expected) {
 | 
			
		||||
    const backgroundImage = page.locator('.c-imagery__main-image__background-image');
 | 
			
		||||
 | 
			
		||||
    // Get the brightness filter value (i.e: filter: brightness(500%) => "500")
 | 
			
		||||
    const actual = await backgroundImage.evaluate((el) => {
 | 
			
		||||
        return el.style.filter.match(/brightness\((\d{1,3})%\)/)[1];
 | 
			
		||||
    });
 | 
			
		||||
    expect(actual).toBe(expected);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Gets the filter:contrast value of the current background-image and
 | 
			
		||||
 * asserts against an expected value
 | 
			
		||||
 * @param {import('@playwright/test').Page} page
 | 
			
		||||
 * @param {String} expected The expected contrast value
 | 
			
		||||
 */
 | 
			
		||||
async function assertBackgroundImageContrast(page, expected) {
 | 
			
		||||
    const backgroundImage = page.locator('.c-imagery__main-image__background-image');
 | 
			
		||||
 | 
			
		||||
    // Get the contrast filter value (i.e: filter: contrast(500%) => "500")
 | 
			
		||||
    const actual = await backgroundImage.evaluate((el) => {
 | 
			
		||||
        return el.style.filter.match(/contrast\((\d{1,3})%\)/)[1];
 | 
			
		||||
    });
 | 
			
		||||
    expect(actual).toBe(expected);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {import('@playwright/test').Page} page
 | 
			
		||||
 */
 | 
			
		||||
async function assertBackgroundImageUrlFromBackgroundCss(page) {
 | 
			
		||||
    const backgroundImage = page.locator('.c-imagery__main-image__background-image');
 | 
			
		||||
    let backgroundImageUrl = await backgroundImage.evaluate((el) => {
 | 
			
		||||
        return window.getComputedStyle(el).getPropertyValue('background-image').match(/url\(([^)]+)\)/)[1];
 | 
			
		||||
    });
 | 
			
		||||
    let backgroundImageUrl1 = backgroundImageUrl.slice(1, -1); //forgive me, padre
 | 
			
		||||
    console.log('backgroundImageUrl1 ' + backgroundImageUrl1);
 | 
			
		||||
 | 
			
		||||
    let backgroundImageUrl2;
 | 
			
		||||
    await expect.poll(async () => {
 | 
			
		||||
        // Verify next image has updated
 | 
			
		||||
        let backgroundImageUrlNext = await backgroundImage.evaluate((el) => {
 | 
			
		||||
            return window.getComputedStyle(el).getPropertyValue('background-image').match(/url\(([^)]+)\)/)[1];
 | 
			
		||||
        });
 | 
			
		||||
        backgroundImageUrl2 = backgroundImageUrlNext.slice(1, -1); //forgive me, padre
 | 
			
		||||
 | 
			
		||||
        return backgroundImageUrl2;
 | 
			
		||||
    }, {
 | 
			
		||||
        message: "verify next image has updated",
 | 
			
		||||
        timeout: 6 * 1000
 | 
			
		||||
    }).not.toBe(backgroundImageUrl1);
 | 
			
		||||
    console.log('backgroundImageUrl2 ' + backgroundImageUrl2);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {import('@playwright/test').Page} page
 | 
			
		||||
 */
 | 
			
		||||
async function panZoomAndAssertImageProperties(page) {
 | 
			
		||||
    const panHotkey = process.platform === 'linux' ? ['Control', 'Alt'] : ['Alt'];
 | 
			
		||||
    const expectedAltText = process.platform === 'linux' ? 'Ctrl+Alt drag to pan' : 'Alt drag to pan';
 | 
			
		||||
    const imageryHintsText = await page.locator('.c-imagery__hints').innerText();
 | 
			
		||||
    expect(expectedAltText).toEqual(imageryHintsText);
 | 
			
		||||
    const zoomedBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
 | 
			
		||||
    const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;
 | 
			
		||||
    const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
 | 
			
		||||
 | 
			
		||||
    // Pan right
 | 
			
		||||
    await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
 | 
			
		||||
    await page.mouse.down();
 | 
			
		||||
    await page.mouse.move(imageCenterX - 200, imageCenterY, 10);
 | 
			
		||||
    await page.mouse.up();
 | 
			
		||||
    await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
 | 
			
		||||
    const afterRightPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
 | 
			
		||||
    expect(zoomedBoundingBox.x).toBeGreaterThan(afterRightPanBoundingBox.x);
 | 
			
		||||
 | 
			
		||||
    // Pan left
 | 
			
		||||
    await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
 | 
			
		||||
    await page.mouse.down();
 | 
			
		||||
    await page.mouse.move(imageCenterX, imageCenterY, 10);
 | 
			
		||||
    await page.mouse.up();
 | 
			
		||||
    await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
 | 
			
		||||
    const afterLeftPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
 | 
			
		||||
    expect(afterRightPanBoundingBox.x).toBeLessThan(afterLeftPanBoundingBox.x);
 | 
			
		||||
 | 
			
		||||
    // Pan up
 | 
			
		||||
    await page.mouse.move(imageCenterX, imageCenterY);
 | 
			
		||||
    await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
 | 
			
		||||
    await page.mouse.down();
 | 
			
		||||
    await page.mouse.move(imageCenterX, imageCenterY + 200, 10);
 | 
			
		||||
    await page.mouse.up();
 | 
			
		||||
    await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
 | 
			
		||||
    const afterUpPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
 | 
			
		||||
    expect(afterUpPanBoundingBox.y).toBeGreaterThanOrEqual(afterLeftPanBoundingBox.y);
 | 
			
		||||
 | 
			
		||||
    // Pan down
 | 
			
		||||
    await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
 | 
			
		||||
    await page.mouse.down();
 | 
			
		||||
    await page.mouse.move(imageCenterX, imageCenterY - 200, 10);
 | 
			
		||||
    await page.mouse.up();
 | 
			
		||||
    await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
 | 
			
		||||
    const afterDownPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
 | 
			
		||||
    expect(afterDownPanBoundingBox.y).toBeLessThanOrEqual(afterUpPanBoundingBox.y);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {import('@playwright/test').Page} page
 | 
			
		||||
*/
 | 
			
		||||
async function mouseZoomIn(page) {
 | 
			
		||||
    // Zoom in
 | 
			
		||||
    const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox();
 | 
			
		||||
    await page.locator(backgroundImageSelector).hover({trial: true});
 | 
			
		||||
    const deltaYStep = 100; // equivalent to 1x zoom
 | 
			
		||||
    await page.mouse.wheel(0, deltaYStep * 2);
 | 
			
		||||
    const zoomedBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
 | 
			
		||||
    const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;
 | 
			
		||||
    const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
 | 
			
		||||
 | 
			
		||||
    // center the mouse pointer
 | 
			
		||||
    await page.mouse.move(imageCenterX, imageCenterY);
 | 
			
		||||
 | 
			
		||||
    // Wait for zoom animation to finish
 | 
			
		||||
    await page.locator(backgroundImageSelector).hover({trial: true});
 | 
			
		||||
    const imageMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox();
 | 
			
		||||
    expect(imageMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height);
 | 
			
		||||
    expect(imageMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,30 +0,0 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2022, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
// this will be called from the test suite with
 | 
			
		||||
// await page.addInitScript({ path: path.join(__dirname, 'addInitRestrictedNotebook.js') });
 | 
			
		||||
// it will install the RestrictedNotebook since it is not installed by default
 | 
			
		||||
 | 
			
		||||
document.addEventListener('DOMContentLoaded', () => {
 | 
			
		||||
    const openmct = window.openmct;
 | 
			
		||||
    openmct.install(openmct.plugins.RestrictedNotebook('CUSTOM_NAME'));
 | 
			
		||||
});
 | 
			
		||||
@@ -1,198 +0,0 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2022, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
This test suite is dedicated to tests which verify the basic operations surrounding Notebooks.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
const { test } = require('../../../fixtures');
 | 
			
		||||
 | 
			
		||||
test.describe('Notebook CRUD Operations', () => {
 | 
			
		||||
    test.fixme('Can create a Notebook Object', async ({ page }) => {
 | 
			
		||||
        //Create domain object
 | 
			
		||||
        //Newly created notebook should have one Section and one page, 'Unnamed Section'/'Unnamed Page'
 | 
			
		||||
    });
 | 
			
		||||
    test.fixme('Can update a Notebook Object', async ({ page }) => {});
 | 
			
		||||
    test.fixme('Can view a perviously created Notebook Object', async ({ page }) => {});
 | 
			
		||||
    test.fixme('Can Delete a Notebook Object', async ({ page }) => {
 | 
			
		||||
        // Other than non-persistible objects
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test.describe('Default Notebook', () => {
 | 
			
		||||
    // General Default Notebook statements
 | 
			
		||||
    // ## Useful commands:
 | 
			
		||||
    // 1.  - To check default notebook:
 | 
			
		||||
    //     `JSON.parse(localStorage.getItem('notebook-storage'));`
 | 
			
		||||
    // 1.  - Clear default notebook:
 | 
			
		||||
    //     `localStorage.setItem('notebook-storage', null);`
 | 
			
		||||
    test.fixme('A newly created Notebook is automatically set as the default notebook if no other notebooks exist', async ({ page }) => {
 | 
			
		||||
        //Create new notebook
 | 
			
		||||
        //Verify Default Notebook Characteristics
 | 
			
		||||
    });
 | 
			
		||||
    test.fixme('A newly created Notebook is automatically set as the default notebook if at least one other notebook exists', async ({ page }) => {
 | 
			
		||||
        //Create new notebook A
 | 
			
		||||
        //Create second notebook B
 | 
			
		||||
        //Verify Non-Default Notebook A Characteristics
 | 
			
		||||
        //Verify Default Notebook B Characteristics
 | 
			
		||||
    });
 | 
			
		||||
    test.fixme('If a default notebook is deleted, the second most recent notebook becomes the default', async ({ page }) => {
 | 
			
		||||
        //Create new notebook A
 | 
			
		||||
        //Create second notebook B
 | 
			
		||||
        //Delete Notebook B
 | 
			
		||||
        //Verify Default Notebook A Characteristics
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test.describe('Notebook section tests', () => {
 | 
			
		||||
    //The following test cases are associated with Notebook Sections
 | 
			
		||||
    test.fixme('New sections are automatically named Unnamed Section with Unnamed Page', async ({ page }) => {
 | 
			
		||||
        //Create new notebook A
 | 
			
		||||
        //Add section
 | 
			
		||||
        //Verify new section and new page details
 | 
			
		||||
    });
 | 
			
		||||
    test.fixme('Section selection operations and associated behavior', async ({ page }) => {
 | 
			
		||||
        //Create new notebook A
 | 
			
		||||
        //Add Sections until 6 total with no default section/page
 | 
			
		||||
        //Select 3rd section
 | 
			
		||||
        //Delete 4th section
 | 
			
		||||
        //3rd section is still selected
 | 
			
		||||
        //Delete 3rd section
 | 
			
		||||
        //1st section is selected
 | 
			
		||||
        //Set 3rd section as default
 | 
			
		||||
        //Delete 2nd section
 | 
			
		||||
        //3rd section is still default
 | 
			
		||||
        //Delete 3rd section
 | 
			
		||||
        //1st is selected and there is no default notebook
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test.describe('Notebook page tests', () => {
 | 
			
		||||
    //The following test cases are associated with Notebook Pages
 | 
			
		||||
    test.fixme('Page selection operations and associated behavior', async ({ page }) => {
 | 
			
		||||
        //Create new notebook A
 | 
			
		||||
        //Delete existing Page
 | 
			
		||||
        //New 'Unnamed Page' automatically created
 | 
			
		||||
        //Create 6 total Pages without a default page
 | 
			
		||||
        //Select 3rd
 | 
			
		||||
        //Delete 3rd
 | 
			
		||||
        //First is now selected
 | 
			
		||||
        //Set 3rd as default
 | 
			
		||||
        //Select 2nd page
 | 
			
		||||
        //Delete 2nd page
 | 
			
		||||
        //3rd (default) is now selected
 | 
			
		||||
        //Set 3rd as default page
 | 
			
		||||
        //Select 3rd (default) page
 | 
			
		||||
        //Delete 3rd page
 | 
			
		||||
        //First is now selected and there is no default notebook
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test.describe('Notebook search tests', () => {
 | 
			
		||||
    test.fixme('Can search for a single result', async ({ page }) => {});
 | 
			
		||||
    test.fixme('Can search for many results', async ({ page }) => {});
 | 
			
		||||
    test.fixme('Can search for new and recently modified entries', async ({ page }) => {});
 | 
			
		||||
    test.fixme('Can search for section text', async ({ page }) => {});
 | 
			
		||||
    test.fixme('Can search for page text', async ({ page }) => {});
 | 
			
		||||
    test.fixme('Can search for entry text', async ({ page }) => {});
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test.describe('Notebook entry tests', () => {
 | 
			
		||||
    test.fixme('When a new entry is created, it should be focused', async ({ page }) => {});
 | 
			
		||||
    test.fixme('When a telemetry object is dropped into a notebook, a new entry is created and it should be focused', async ({ page }) => {
 | 
			
		||||
        // Drag and drop any telmetry object on 'drop object'
 | 
			
		||||
        // new entry gets created with telemtry object
 | 
			
		||||
    });
 | 
			
		||||
    test.fixme('When a telemetry object is dropped into a notebooks existing entry, it should be focused', async ({ page }) => {
 | 
			
		||||
        // Drag and drop any telemetry object onto existing entry
 | 
			
		||||
        // Entry updated with object and snapshot
 | 
			
		||||
    });
 | 
			
		||||
    test.fixme('new entries persist through navigation events without save', async ({ page }) => {});
 | 
			
		||||
    test.fixme('previous and new entries can be deleted', async ({ page }) => {});
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test.describe('Snapshot Menu tests', () => {
 | 
			
		||||
    test.fixme('When no default notebook is selected, Snapshot Menu dropdown should only have a single option', async ({ page }) => {
 | 
			
		||||
        // There should be no default notebook
 | 
			
		||||
        // Clear default notebook if exists using `localStorage.setItem('notebook-storage', null);`
 | 
			
		||||
        // refresh page
 | 
			
		||||
        // Click on 'Notebook Snaphot Menu'
 | 
			
		||||
        // 'save to Notebook Snapshots' should be only option there
 | 
			
		||||
    });
 | 
			
		||||
    test.fixme('When default notebook is updated selected, Snapshot Menu dropdown should list it as the newest option', async ({ page }) => {
 | 
			
		||||
        // Create 2a notebooks
 | 
			
		||||
        // Set Notebook A as Default
 | 
			
		||||
        // Open Snapshot Menu and note that Notebook A is listed
 | 
			
		||||
        // Close Snapshot Menu
 | 
			
		||||
        // Set Default Notebook to Notebook B
 | 
			
		||||
        // Open Snapshot Notebook and note that Notebook B is listed
 | 
			
		||||
        // Select Default Notebook Option and verify that Snapshot is added to Notebook B
 | 
			
		||||
    });
 | 
			
		||||
    test.fixme('Can add Snapshots via Snapshot Menu and details are correct', async ({ page }) => {
 | 
			
		||||
        //Note this should be a visual test, too
 | 
			
		||||
        // Create Telemetry object
 | 
			
		||||
        // Create A notebook with many pages and sections.
 | 
			
		||||
        // Set page and section defaults to be between first and last of many. i.e. 3 of 5
 | 
			
		||||
        // Navigate to Telemetry object
 | 
			
		||||
        // Select Default Notebook Option and verify that Snapshot is added to Notebook A
 | 
			
		||||
        // Verify Snapshot Details appear correctly
 | 
			
		||||
    });
 | 
			
		||||
    test.fixme('Snapshots adjust time conductor', async ({ page }) => {
 | 
			
		||||
        // Create Telemetry object
 | 
			
		||||
        // Set Telemetry object's timeconductor to Fixed time with Start and Endtimes are recorded
 | 
			
		||||
        // Embed Telemetry object into notebook
 | 
			
		||||
        // Set Time Conductor to Local clock
 | 
			
		||||
        // Click into embedded telemetry object and verify object appears with same fixed time from record
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test.describe('Snapshot Container tests', () => {
 | 
			
		||||
    test.fixme('5 Snapshots can be added to a container', async ({ page }) => {});
 | 
			
		||||
    test.fixme('5 Snapshots can be added to a container and Deleted with Delete All action', async ({ page }) => {});
 | 
			
		||||
    test.fixme('A snapshot can be Deleted from Container', async ({ page }) => {});
 | 
			
		||||
    test.fixme('A snapshot can be Previewed from Container', async ({ page }) => {});
 | 
			
		||||
    test.fixme('A snapshot Container can be open and closed', async ({ page }) => {});
 | 
			
		||||
    test.fixme('Can add object to Snapshot container and pull into notebook and create a new entry', async ({ page }) => {
 | 
			
		||||
        //Create Notebook
 | 
			
		||||
        //Create Telemetry Object
 | 
			
		||||
        //From Telemetry Object, use 'save to Notebook Snapshots'
 | 
			
		||||
        //Snapshots indicator should blink, click on it to view snapshots
 | 
			
		||||
        //Navigate to Notebook
 | 
			
		||||
        //Drag and Drop onto droppable area for new entry
 | 
			
		||||
        //New Entry created with given snapshot added
 | 
			
		||||
        //Snapshot removed from container?
 | 
			
		||||
    });
 | 
			
		||||
    test.fixme('Can add object to Snapshot container and pull into notebook and existing entry', async ({ page }) => {
 | 
			
		||||
        //Create Notebook
 | 
			
		||||
        //Create Telemetry Object
 | 
			
		||||
        //From Telemetry Object, use 'save to Notebook Snapshots'
 | 
			
		||||
        //Snapshots indicator should blink, click on it to view snapshots
 | 
			
		||||
        //Navigate to Notebook
 | 
			
		||||
        //Drag and Drop into exiting entry
 | 
			
		||||
        //Existing Entry updated with given snapshot
 | 
			
		||||
        //Snapshot removed from container?
 | 
			
		||||
    });
 | 
			
		||||
    test.fixme('Verify Embedded options for PNG, JPG, and Annotate work correctly', async ({ page }) => {
 | 
			
		||||
        //Add snapshot to container
 | 
			
		||||
        //Verify PNG, JPG, and Annotate buttons work correctly
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@@ -1,262 +0,0 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2022, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
const { test } = require('../../../fixtures');
 | 
			
		||||
const { expect } = require('@playwright/test');
 | 
			
		||||
const path = require('path');
 | 
			
		||||
 | 
			
		||||
const TEST_TEXT = 'Testing text for entries.';
 | 
			
		||||
const TEST_TEXT_NAME = 'Test Page';
 | 
			
		||||
const CUSTOM_NAME = 'CUSTOM_NAME';
 | 
			
		||||
const NOTEBOOK_DROP_AREA = '.c-notebook__drag-area';
 | 
			
		||||
 | 
			
		||||
test.describe('Restricted Notebook', () => {
 | 
			
		||||
    test.beforeEach(async ({ page }) => {
 | 
			
		||||
        await startAndAddRestrictedNotebookObject(page);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('Can be renamed @addInit', async ({ page }) => {
 | 
			
		||||
        await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText(`Unnamed ${CUSTOM_NAME}`);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('Can be deleted if there are no locked pages @addInit', async ({ page }) => {
 | 
			
		||||
        await openContextMenuRestrictedNotebook(page);
 | 
			
		||||
 | 
			
		||||
        const menuOptions = page.locator('.c-menu ul');
 | 
			
		||||
        await expect.soft(menuOptions).toContainText('Remove');
 | 
			
		||||
 | 
			
		||||
        const restrictedNotebookTreeObject = page.locator(`a:has-text("Unnamed ${CUSTOM_NAME}")`);
 | 
			
		||||
 | 
			
		||||
        // notbook tree object exists
 | 
			
		||||
        expect.soft(await restrictedNotebookTreeObject.count()).toEqual(1);
 | 
			
		||||
 | 
			
		||||
        // Click Remove Text
 | 
			
		||||
        await page.locator('text=Remove').click();
 | 
			
		||||
 | 
			
		||||
        //Wait until Save Banner is gone
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            page.waitForNavigation(),
 | 
			
		||||
            page.locator('text=OK').click(),
 | 
			
		||||
            page.waitForSelector('.c-message-banner__message')
 | 
			
		||||
        ]);
 | 
			
		||||
        await page.locator('.c-message-banner__close-button').click();
 | 
			
		||||
 | 
			
		||||
        // has been deleted
 | 
			
		||||
        expect.soft(await restrictedNotebookTreeObject.count()).toEqual(0);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('Can be locked if at least one page has one entry @addInit', async ({ page }) => {
 | 
			
		||||
 | 
			
		||||
        await enterTextEntry(page);
 | 
			
		||||
 | 
			
		||||
        const commitButton = page.locator('button:has-text("Commit Entries")');
 | 
			
		||||
        expect.soft(await commitButton.count()).toEqual(1);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test.describe('Restricted Notebook with at least one entry and with the page locked @addInit', () => {
 | 
			
		||||
 | 
			
		||||
    test.beforeEach(async ({ page }) => {
 | 
			
		||||
        await startAndAddRestrictedNotebookObject(page);
 | 
			
		||||
        await enterTextEntry(page);
 | 
			
		||||
        await lockPage(page);
 | 
			
		||||
 | 
			
		||||
        // open sidebar
 | 
			
		||||
        await page.locator('button.c-notebook__toggle-nav-button').click();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('Locked page should now be in a locked state @addInit', async ({ page }) => {
 | 
			
		||||
        // main lock message on page
 | 
			
		||||
        const lockMessage = page.locator('text=This page has been committed and cannot be modified or removed');
 | 
			
		||||
        expect.soft(await lockMessage.count()).toEqual(1);
 | 
			
		||||
 | 
			
		||||
        // lock icon on page in sidebar
 | 
			
		||||
        const pageLockIcon = page.locator('ul.c-notebook__pages li div.icon-lock');
 | 
			
		||||
        expect.soft(await pageLockIcon.count()).toEqual(1);
 | 
			
		||||
 | 
			
		||||
        // no way to remove a restricted notebook with a locked page
 | 
			
		||||
        await openContextMenuRestrictedNotebook(page);
 | 
			
		||||
 | 
			
		||||
        const menuOptions = page.locator('.c-menu ul');
 | 
			
		||||
 | 
			
		||||
        await expect.soft(menuOptions).not.toContainText('Remove');
 | 
			
		||||
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('Can still: add page, rename, add entry, delete unlocked pages @addInit', async ({ page }) => {
 | 
			
		||||
        // Click text=Page Add >> button
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            page.waitForNavigation(),
 | 
			
		||||
            page.locator('text=Page Add >> button').click()
 | 
			
		||||
        ]);
 | 
			
		||||
        // Click text=Unnamed Page >> nth=1
 | 
			
		||||
        await page.locator('text=Unnamed Page').nth(1).click();
 | 
			
		||||
        // Press a with modifiers
 | 
			
		||||
        await page.locator('text=Unnamed Page').nth(1).fill(TEST_TEXT_NAME);
 | 
			
		||||
 | 
			
		||||
        // expect to be able to rename unlocked pages
 | 
			
		||||
        const newPageElement = page.locator(`text=${TEST_TEXT_NAME}`);
 | 
			
		||||
        const newPageCount = await newPageElement.count();
 | 
			
		||||
        await newPageElement.press('Enter'); // exit contenteditable state
 | 
			
		||||
        expect.soft(newPageCount).toEqual(1);
 | 
			
		||||
 | 
			
		||||
        // enter test text
 | 
			
		||||
        await enterTextEntry(page);
 | 
			
		||||
 | 
			
		||||
        // expect new page to be lockable
 | 
			
		||||
        const commitButton = page.locator('BUTTON:HAS-TEXT("COMMIT ENTRIES")');
 | 
			
		||||
        expect.soft(await commitButton.count()).toEqual(1);
 | 
			
		||||
 | 
			
		||||
        // Click text=Unnamed PageTest Page >> button
 | 
			
		||||
        await page.locator('text=Unnamed PageTest Page >> button').click();
 | 
			
		||||
        // Click text=Delete Page
 | 
			
		||||
        await page.locator('text=Delete Page').click();
 | 
			
		||||
        // Click text=Ok
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            page.waitForNavigation(),
 | 
			
		||||
            page.locator('text=Ok').click()
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        // deleted page, should no longer exist
 | 
			
		||||
        const deletedPageElement = page.locator(`text=${TEST_TEXT_NAME}`);
 | 
			
		||||
        expect.soft(await deletedPageElement.count()).toEqual(0);
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test.describe('Restricted Notebook with a page locked and with an embed @addInit', () => {
 | 
			
		||||
 | 
			
		||||
    test.beforeEach(async ({ page }) => {
 | 
			
		||||
        await startAndAddRestrictedNotebookObject(page);
 | 
			
		||||
        await dragAndDropEmbed(page);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('Allows embeds to be deleted if page unlocked @addInit', async ({ page }) => {
 | 
			
		||||
        // Click .c-ne__embed__name .c-popup-menu-button
 | 
			
		||||
        await page.locator('.c-ne__embed__name .c-popup-menu-button').click(); // embed popup menu
 | 
			
		||||
 | 
			
		||||
        const embedMenu = page.locator('body >> .c-menu');
 | 
			
		||||
        await expect.soft(embedMenu).toContainText('Remove This Embed');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('Disallows embeds to be deleted if page locked @addInit', async ({ page }) => {
 | 
			
		||||
        await lockPage(page);
 | 
			
		||||
        // Click .c-ne__embed__name .c-popup-menu-button
 | 
			
		||||
        await page.locator('.c-ne__embed__name .c-popup-menu-button').click(); // embed popup menu
 | 
			
		||||
 | 
			
		||||
        const embedMenu = page.locator('body >> .c-menu');
 | 
			
		||||
        await expect.soft(embedMenu).not.toContainText('Remove This Embed');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {import('@playwright/test').Page} page
 | 
			
		||||
 */
 | 
			
		||||
async function startAndAddRestrictedNotebookObject(page) {
 | 
			
		||||
    // eslint-disable-next-line no-undef
 | 
			
		||||
    await page.addInitScript({ path: path.join(__dirname, 'addInitRestrictedNotebook.js') });
 | 
			
		||||
    //Go to baseURL
 | 
			
		||||
    await page.goto('/', { waitUntil: 'networkidle' });
 | 
			
		||||
    //Click the Create button
 | 
			
		||||
    await page.click('button:has-text("Create")');
 | 
			
		||||
    // Click text=CUSTOME_NAME
 | 
			
		||||
    await page.click(`text=${CUSTOM_NAME}`); // secondarily tests renamability also
 | 
			
		||||
    // Click text=OK
 | 
			
		||||
    await Promise.all([
 | 
			
		||||
        page.waitForNavigation({waitUntil: 'networkidle'}),
 | 
			
		||||
        page.click('text=OK')
 | 
			
		||||
    ]);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {import('@playwright/test').Page} page
 | 
			
		||||
 */
 | 
			
		||||
async function enterTextEntry(page) {
 | 
			
		||||
    // Click .c-notebook__drag-area
 | 
			
		||||
    await page.locator(NOTEBOOK_DROP_AREA).click();
 | 
			
		||||
 | 
			
		||||
    // enter text
 | 
			
		||||
    await page.locator('div.c-ne__text').click();
 | 
			
		||||
    await page.locator('div.c-ne__text').fill(TEST_TEXT);
 | 
			
		||||
    await page.locator('div.c-ne__text').press('Enter');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {import('@playwright/test').Page} page
 | 
			
		||||
 */
 | 
			
		||||
async function dragAndDropEmbed(page) {
 | 
			
		||||
    // Click button:has-text("Create")
 | 
			
		||||
    await page.locator('button:has-text("Create")').click();
 | 
			
		||||
    // Click li:has-text("Sine Wave Generator")
 | 
			
		||||
    await page.locator('li:has-text("Sine Wave Generator")').click();
 | 
			
		||||
    // Click form[name="mctForm"] >> text=My Items
 | 
			
		||||
    await page.locator('form[name="mctForm"] >> text=My Items').click();
 | 
			
		||||
    // Click text=OK
 | 
			
		||||
    await page.locator('text=OK').click();
 | 
			
		||||
    // Click text=Open MCT My Items >> span >> nth=3
 | 
			
		||||
    await page.locator('text=Open MCT My Items >> span').nth(3).click();
 | 
			
		||||
    // Click text=Unnamed CUSTOM_NAME
 | 
			
		||||
    await Promise.all([
 | 
			
		||||
        page.waitForNavigation(),
 | 
			
		||||
        page.locator('text=Unnamed CUSTOM_NAME').click()
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    await page.dragAndDrop('text=UNNAMED SINE WAVE GENERATOR', NOTEBOOK_DROP_AREA);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {import('@playwright/test').Page} page
 | 
			
		||||
 */
 | 
			
		||||
async function lockPage(page) {
 | 
			
		||||
    const commitButton = page.locator('button:has-text("Commit Entries")');
 | 
			
		||||
    await commitButton.click();
 | 
			
		||||
 | 
			
		||||
    //Wait until Lock Banner is visible
 | 
			
		||||
    await Promise.all([
 | 
			
		||||
        page.locator('text=Lock Page').click(),
 | 
			
		||||
        page.waitForSelector('.c-message-banner__message')
 | 
			
		||||
    ]);
 | 
			
		||||
    // Close Lock Banner
 | 
			
		||||
    await page.locator('.c-message-banner__close-button').click();
 | 
			
		||||
 | 
			
		||||
    //artifically wait to avoid mutation delay TODO: https://github.com/nasa/openmct/issues/5409
 | 
			
		||||
    // eslint-disable-next-line playwright/no-wait-for-timeout
 | 
			
		||||
    await page.waitForTimeout(1 * 1000);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {import('@playwright/test').Page} page
 | 
			
		||||
 */
 | 
			
		||||
async function openContextMenuRestrictedNotebook(page) {
 | 
			
		||||
    // Click text=Open MCT My Items (This expands the My Items folder to show it's chilren in the tree)
 | 
			
		||||
    await page.locator('text=Open MCT My Items >> span').nth(3).click();
 | 
			
		||||
 | 
			
		||||
    //artifically wait to avoid mutation delay TODO: https://github.com/nasa/openmct/issues/5409
 | 
			
		||||
    // eslint-disable-next-line playwright/no-wait-for-timeout
 | 
			
		||||
    await page.waitForTimeout(1 * 1000);
 | 
			
		||||
 | 
			
		||||
    // Click a:has-text("Unnamed CUSTOM_NAME")
 | 
			
		||||
    await page.locator(`a:has-text("Unnamed ${CUSTOM_NAME}")`).click({
 | 
			
		||||
        button: 'right'
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
@@ -1,205 +0,0 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2022, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
This test suite is dedicated to tests which verify form functionality.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
const { expect } = require('@playwright/test');
 | 
			
		||||
const { test } = require('../../../fixtures');
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
  * Creates a notebook object and adds an entry.
 | 
			
		||||
  * @param {import('@playwright/test').Page} - page to load
 | 
			
		||||
  * @param {number} [iterations = 1] - the number of entries to create
 | 
			
		||||
  */
 | 
			
		||||
async function createNotebookAndEntry(page, iterations = 1) {
 | 
			
		||||
    //Go to baseURL
 | 
			
		||||
    await page.goto('/', { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
    // Click button:has-text("Create")
 | 
			
		||||
    await page.locator('button:has-text("Create")').click();
 | 
			
		||||
    await page.locator('[title="Create and save timestamped notes with embedded object snapshots."]').click();
 | 
			
		||||
    // Click button:has-text("OK")
 | 
			
		||||
    await Promise.all([
 | 
			
		||||
        page.waitForNavigation(),
 | 
			
		||||
        page.locator('[name="mctForm"] >> text=My Items').click(),
 | 
			
		||||
        page.locator('button:has-text("OK")').click()
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    for (let iteration = 0; iteration < iterations; iteration++) {
 | 
			
		||||
        // Click text=To start a new entry, click here or drag and drop any object
 | 
			
		||||
        await page.locator('text=To start a new entry, click here or drag and drop any object').click();
 | 
			
		||||
        const entryLocator = `[aria-label="Notebook Entry Input"] >> nth = ${iteration}`;
 | 
			
		||||
        await page.locator(entryLocator).click();
 | 
			
		||||
        await page.locator(entryLocator).fill(`Entry ${iteration}`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
  * Creates a notebook object, adds an entry, and adds a tag.
 | 
			
		||||
  * @param {import('@playwright/test').Page} page
 | 
			
		||||
  * @param {number} [iterations = 1] - the number of entries (and tags) to create
 | 
			
		||||
  */
 | 
			
		||||
async function createNotebookEntryAndTags(page, iterations = 1) {
 | 
			
		||||
    await createNotebookAndEntry(page, iterations);
 | 
			
		||||
 | 
			
		||||
    for (let iteration = 0; iteration < iterations; iteration++) {
 | 
			
		||||
        // Click text=To start a new entry, click here or drag and drop any object
 | 
			
		||||
        await page.locator(`button:has-text("Add Tag") >> nth = ${iteration}`).click();
 | 
			
		||||
 | 
			
		||||
        // Click [placeholder="Type to select tag"]
 | 
			
		||||
        await page.locator('[placeholder="Type to select tag"]').click();
 | 
			
		||||
        // Click text=Driving
 | 
			
		||||
        await page.locator('[aria-label="Autocomplete Options"] >> text=Driving').click();
 | 
			
		||||
 | 
			
		||||
        // Click button:has-text("Add Tag")
 | 
			
		||||
        await page.locator(`button:has-text("Add Tag") >> nth = ${iteration}`).click();
 | 
			
		||||
        // Click [placeholder="Type to select tag"]
 | 
			
		||||
        await page.locator('[placeholder="Type to select tag"]').click();
 | 
			
		||||
        // Click text=Science
 | 
			
		||||
        await page.locator('[aria-label="Autocomplete Options"] >> text=Science').click();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
test.describe('Tagging in Notebooks', () => {
 | 
			
		||||
    test('Can load tags', async ({ page }) => {
 | 
			
		||||
        await createNotebookAndEntry(page);
 | 
			
		||||
        // Click text=To start a new entry, click here or drag and drop any object
 | 
			
		||||
        await page.locator('button:has-text("Add Tag")').click();
 | 
			
		||||
 | 
			
		||||
        // Click [placeholder="Type to select tag"]
 | 
			
		||||
        await page.locator('[placeholder="Type to select tag"]').click();
 | 
			
		||||
 | 
			
		||||
        await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Science");
 | 
			
		||||
        await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Drilling");
 | 
			
		||||
        await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Driving");
 | 
			
		||||
    });
 | 
			
		||||
    test('Can add tags', async ({ page }) => {
 | 
			
		||||
        await createNotebookEntryAndTags(page);
 | 
			
		||||
 | 
			
		||||
        await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Science");
 | 
			
		||||
        await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Driving");
 | 
			
		||||
 | 
			
		||||
        // Click button:has-text("Add Tag")
 | 
			
		||||
        await page.locator('button:has-text("Add Tag")').click();
 | 
			
		||||
        // Click [placeholder="Type to select tag"]
 | 
			
		||||
        await page.locator('[placeholder="Type to select tag"]').click();
 | 
			
		||||
 | 
			
		||||
        await expect(page.locator('[aria-label="Autocomplete Options"]')).not.toContainText("Science");
 | 
			
		||||
        await expect(page.locator('[aria-label="Autocomplete Options"]')).not.toContainText("Driving");
 | 
			
		||||
        await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Drilling");
 | 
			
		||||
    });
 | 
			
		||||
    test('Can search for tags', async ({ page }) => {
 | 
			
		||||
        await createNotebookEntryAndTags(page);
 | 
			
		||||
        // Click [aria-label="OpenMCT Search"] input[type="search"]
 | 
			
		||||
        await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
 | 
			
		||||
        // Fill [aria-label="OpenMCT Search"] input[type="search"]
 | 
			
		||||
        await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc');
 | 
			
		||||
        await expect(page.locator('[aria-label="Search Result"]')).toContainText("Science");
 | 
			
		||||
        await expect(page.locator('[aria-label="Search Result"]')).toContainText("Driving");
 | 
			
		||||
 | 
			
		||||
        // Click [aria-label="OpenMCT Search"] input[type="search"]
 | 
			
		||||
        await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
 | 
			
		||||
        // Fill [aria-label="OpenMCT Search"] input[type="search"]
 | 
			
		||||
        await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Sc');
 | 
			
		||||
        await expect(page.locator('[aria-label="Search Result"]')).toContainText("Science");
 | 
			
		||||
        await expect(page.locator('[aria-label="Search Result"]')).toContainText("Driving");
 | 
			
		||||
 | 
			
		||||
        // Click [aria-label="OpenMCT Search"] input[type="search"]
 | 
			
		||||
        await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
 | 
			
		||||
        // Fill [aria-label="OpenMCT Search"] input[type="search"]
 | 
			
		||||
        await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Xq');
 | 
			
		||||
        await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible();
 | 
			
		||||
        await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('Can delete tags', async ({ page }) => {
 | 
			
		||||
        await createNotebookEntryAndTags(page);
 | 
			
		||||
        await page.locator('[aria-label="Notebook Entries"]').click();
 | 
			
		||||
        // Delete Driving
 | 
			
		||||
        await page.locator('text=Science Driving Add Tag >> button').nth(1).click();
 | 
			
		||||
 | 
			
		||||
        await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Science");
 | 
			
		||||
        await expect(page.locator('[aria-label="Notebook Entry"]')).not.toContainText("Driving");
 | 
			
		||||
 | 
			
		||||
        // Fill [aria-label="OpenMCT Search"] input[type="search"]
 | 
			
		||||
        await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc');
 | 
			
		||||
        await expect(page.locator('[aria-label="Search Result"]')).not.toContainText("Driving");
 | 
			
		||||
    });
 | 
			
		||||
    test('Tags persist across reload', async ({ page }) => {
 | 
			
		||||
        //Go to baseURL
 | 
			
		||||
        await page.goto('/', { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
        // Create a clock object we can navigate to
 | 
			
		||||
        await page.click('button:has-text("Create")');
 | 
			
		||||
 | 
			
		||||
        // Click Clock
 | 
			
		||||
        await page.click('text=Clock');
 | 
			
		||||
        // Click button:has-text("OK")
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            page.waitForNavigation(),
 | 
			
		||||
            page.locator('[name="mctForm"] >> text=My Items').click(),
 | 
			
		||||
            page.locator('button:has-text("OK")').click()
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        await page.click('.c-disclosure-triangle');
 | 
			
		||||
 | 
			
		||||
        const ITERATIONS = 4;
 | 
			
		||||
        await createNotebookEntryAndTags(page, ITERATIONS);
 | 
			
		||||
 | 
			
		||||
        for (let iteration = 0; iteration < ITERATIONS; iteration++) {
 | 
			
		||||
            const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`;
 | 
			
		||||
            await expect(page.locator(entryLocator)).toContainText("Science");
 | 
			
		||||
            await expect(page.locator(entryLocator)).toContainText("Driving");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Click Unnamed Clock
 | 
			
		||||
        await page.click('text="Unnamed Clock"');
 | 
			
		||||
 | 
			
		||||
        // Click Unnamed Notebook
 | 
			
		||||
        await page.click('text="Unnamed Notebook"');
 | 
			
		||||
 | 
			
		||||
        for (let iteration = 0; iteration < ITERATIONS; iteration++) {
 | 
			
		||||
            const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`;
 | 
			
		||||
            await expect(page.locator(entryLocator)).toContainText("Science");
 | 
			
		||||
            await expect(page.locator(entryLocator)).toContainText("Driving");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        //Reload Page
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            page.reload(),
 | 
			
		||||
            page.waitForLoadState('networkidle')
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        // Click Unnamed Notebook
 | 
			
		||||
        await page.click('text="Unnamed Notebook"');
 | 
			
		||||
 | 
			
		||||
        for (let iteration = 0; iteration < ITERATIONS; iteration++) {
 | 
			
		||||
            const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`;
 | 
			
		||||
            await expect(page.locator(entryLocator)).toContainText("Science");
 | 
			
		||||
            await expect(page.locator(entryLocator)).toContainText("Driving");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@@ -24,8 +24,20 @@
 | 
			
		||||
Testsuite for plot autoscale.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
const { test } = require('../../../fixtures.js');
 | 
			
		||||
const { expect } = require('@playwright/test');
 | 
			
		||||
const { test: _test, 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: {
 | 
			
		||||
@@ -35,10 +47,7 @@ test.use({
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test.describe('ExportAsJSON', () => {
 | 
			
		||||
    test('User can set autoscale with a valid range @snapshot', async ({ page }) => {
 | 
			
		||||
        //This is necessary due to the size of the test suite.
 | 
			
		||||
        test.slow();
 | 
			
		||||
 | 
			
		||||
    test.slow('User can set autoscale with a valid range @snapshot', async ({ page }) => {
 | 
			
		||||
        await page.goto('/', { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
        await setTimeRange(page);
 | 
			
		||||
@@ -49,16 +58,16 @@ test.describe('ExportAsJSON', () => {
 | 
			
		||||
 | 
			
		||||
        await turnOffAutoscale(page);
 | 
			
		||||
 | 
			
		||||
        // Make sure that after turning off autoscale, the user selected range values start at the same values the plot had prior.
 | 
			
		||||
        await testYTicks(page, ['-1.00', '-0.50', '0.00', '0.50', '1.00']);
 | 
			
		||||
 | 
			
		||||
        const canvas = page.locator('canvas').nth(1);
 | 
			
		||||
 | 
			
		||||
        await canvas.hover({trial: true});
 | 
			
		||||
        // Make sure that after turning off autoscale, the user selected range values start at the same values the plot had prior.
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            testYTicks(page, ['-1.00', '-0.50', '0.00', '0.50', '1.00']),
 | 
			
		||||
            new Promise(r => setTimeout(r, 100))
 | 
			
		||||
                .then(() => canvas.screenshot())
 | 
			
		||||
                .then(shot => expect(shot).toMatchSnapshot('autoscale-canvas-prepan.png', { maxDiffPixels: 40 }))
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        expect(await canvas.screenshot()).toMatchSnapshot('autoscale-canvas-prepan');
 | 
			
		||||
 | 
			
		||||
        //Alt Drag Start
 | 
			
		||||
        await page.keyboard.down('Alt');
 | 
			
		||||
 | 
			
		||||
        await canvas.dragTo(canvas, {
 | 
			
		||||
@@ -72,15 +81,15 @@ test.describe('ExportAsJSON', () => {
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        //Alt Drag End
 | 
			
		||||
        await page.keyboard.up('Alt');
 | 
			
		||||
 | 
			
		||||
        // Ensure the drag worked.
 | 
			
		||||
        await testYTicks(page, ['0.00', '0.50', '1.00', '1.50', '2.00']);
 | 
			
		||||
 | 
			
		||||
        await canvas.hover({trial: true});
 | 
			
		||||
 | 
			
		||||
        expect(await canvas.screenshot()).toMatchSnapshot('autoscale-canvas-panned');
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            testYTicks(page, ['0.00', '0.50', '1.00', '1.50', '2.00']),
 | 
			
		||||
            new Promise(r => setTimeout(r, 100))
 | 
			
		||||
                .then(() => canvas.screenshot())
 | 
			
		||||
                .then(shot => expect(shot).toMatchSnapshot('autoscale-canvas-panned.png', { maxDiffPixels: 40 }))
 | 
			
		||||
        ]);
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 16 KiB  | 
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 15 KiB  | 
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 15 KiB  | 
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 18 KiB  | 
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 18 KiB  | 
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 18 KiB  | 
@@ -25,14 +25,10 @@ Tests to verify log plot functionality. Note this test suite if very much under
 | 
			
		||||
necessarily be used for reference when writing new tests in this area.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
const { test } = require('../../../fixtures.js');
 | 
			
		||||
const { expect } = require('@playwright/test');
 | 
			
		||||
const { test, expect } = require('@playwright/test');
 | 
			
		||||
 | 
			
		||||
test.describe('Log plot tests', () => {
 | 
			
		||||
    test('Log Plot ticks are functionally correct in regular and log mode and after refresh', async ({ page }) => {
 | 
			
		||||
        //Test.slow decorator is currently broken. Needs to be fixed in https://github.com/nasa/openmct/issues/5374
 | 
			
		||||
        test.slow();
 | 
			
		||||
 | 
			
		||||
    test.slow('Log Plot ticks are functionally correct in regular and log mode and after refresh', async ({ page }) => {
 | 
			
		||||
        await makeOverlayPlot(page);
 | 
			
		||||
        await testRegularTicks(page);
 | 
			
		||||
        await enableEditMode(page);
 | 
			
		||||
@@ -44,11 +40,20 @@ test.describe('Log plot tests', () => {
 | 
			
		||||
        await testLogTicks(page);
 | 
			
		||||
        await saveOverlayPlot(page);
 | 
			
		||||
        await testLogTicks(page);
 | 
			
		||||
        //await testLogPlotPixels(page);
 | 
			
		||||
 | 
			
		||||
        // refresh page and wait for charts and ticks to load
 | 
			
		||||
        await page.waitForTimeout(1 * 1000);
 | 
			
		||||
        await page.reload({ waitUntil: 'networkidle'});
 | 
			
		||||
        await page.waitForSelector('.gl-plot-chart-area');
 | 
			
		||||
        await page.waitForSelector('.gl-plot-y-tick-label');
 | 
			
		||||
 | 
			
		||||
        // test log ticks hold up after refresh
 | 
			
		||||
        await testLogTicks(page);
 | 
			
		||||
        //await testLogPlotPixels(page);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Leaving test as 'TODO' for now.
 | 
			
		||||
    // NOTE: Not eligible for community contributions.
 | 
			
		||||
    test.fixme('Verify that log mode option is reflected in import/export JSON', async ({ page }) => {
 | 
			
		||||
    test.skip('Verify that log mode option is reflected in import/export JSON', async ({ page }) => {
 | 
			
		||||
        await makeOverlayPlot(page);
 | 
			
		||||
        await enableEditMode(page);
 | 
			
		||||
        await enableLogMode(page);
 | 
			
		||||
@@ -107,14 +112,14 @@ async function makeOverlayPlot(page) {
 | 
			
		||||
 | 
			
		||||
    // set amplitude to 6, offset 4, period 2
 | 
			
		||||
 | 
			
		||||
    await page.locator('div:nth-child(5) .c-form-row__controls .form-control .field input').click();
 | 
			
		||||
    await page.locator('div:nth-child(5) .c-form-row__controls .form-control .field input').fill('6');
 | 
			
		||||
    await page.locator('div:nth-child(5) .form-row .c-form-row__controls .form-control .field input').click();
 | 
			
		||||
    await page.locator('div:nth-child(5) .form-row .c-form-row__controls .form-control .field input').fill('6');
 | 
			
		||||
 | 
			
		||||
    await page.locator('div:nth-child(6) .c-form-row__controls .form-control .field input').click();
 | 
			
		||||
    await page.locator('div:nth-child(6) .c-form-row__controls .form-control .field input').fill('4');
 | 
			
		||||
    await page.locator('div:nth-child(6) .form-row .c-form-row__controls .form-control .field input').click();
 | 
			
		||||
    await page.locator('div:nth-child(6) .form-row .c-form-row__controls .form-control .field input').fill('4');
 | 
			
		||||
 | 
			
		||||
    await page.locator('div:nth-child(7) .c-form-row__controls .form-control .field input').click();
 | 
			
		||||
    await page.locator('div:nth-child(7) .c-form-row__controls .form-control .field input').fill('2');
 | 
			
		||||
    await page.locator('div:nth-child(7) .form-row .c-form-row__controls .form-control .field input').click();
 | 
			
		||||
    await page.locator('div:nth-child(7) .form-row .c-form-row__controls .form-control .field input').fill('2');
 | 
			
		||||
 | 
			
		||||
    // Click OK to make generator
 | 
			
		||||
 | 
			
		||||
@@ -233,8 +238,6 @@ async function saveOverlayPlot(page) {
 | 
			
		||||
/**
 | 
			
		||||
 * @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.
 | 
			
		||||
 
 | 
			
		||||
@@ -1,161 +0,0 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2022, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
Tests to verify log plot functionality when objects are missing
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
const { test } = require('../../../fixtures.js');
 | 
			
		||||
const { expect } = require('@playwright/test');
 | 
			
		||||
 | 
			
		||||
test.describe('Handle missing object for plots', () => {
 | 
			
		||||
    test('Displays empty div for missing stacked plot item', async ({ page }) => {
 | 
			
		||||
        const errorLogs = [];
 | 
			
		||||
 | 
			
		||||
        page.on("console", (message) => {
 | 
			
		||||
            if (message.type() === 'warning') {
 | 
			
		||||
                errorLogs.push(message.text());
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        //Make stacked plot
 | 
			
		||||
        await makeStackedPlot(page);
 | 
			
		||||
 | 
			
		||||
        //Gets local storage and deletes the last sine wave generator in the stacked plot
 | 
			
		||||
        const localStorage = await page.evaluate(() => window.localStorage);
 | 
			
		||||
        const parsedData = JSON.parse(localStorage.mct);
 | 
			
		||||
        const keys = Object.keys(parsedData);
 | 
			
		||||
        const lastKey = keys[keys.length - 1];
 | 
			
		||||
 | 
			
		||||
        delete parsedData[lastKey];
 | 
			
		||||
 | 
			
		||||
        //Sets local storage with missing object
 | 
			
		||||
        await page.evaluate(
 | 
			
		||||
            `window.localStorage.setItem('mct', '${JSON.stringify(parsedData)}')`
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        //Reloads page and clicks on stacked plot
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            page.reload(),
 | 
			
		||||
            page.waitForLoadState('networkidle')
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        //Verify Main section is there on load
 | 
			
		||||
        await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Stacked Plot');
 | 
			
		||||
 | 
			
		||||
        await page.locator('text=Open MCT My Items >> span').nth(3).click();
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            page.waitForNavigation(),
 | 
			
		||||
            page.locator('text=Unnamed Stacked Plot').first().click()
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        //Check that there is only one stacked item plot with a plot, the missing one will be empty
 | 
			
		||||
        await expect(page.locator(".c-plot--stacked-container:has(.gl-plot)")).toHaveCount(1);
 | 
			
		||||
        //Verify that console.warn is thrown
 | 
			
		||||
        await expect(errorLogs).toHaveLength(1);
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This is used the create a stacked plot object
 | 
			
		||||
 * @private
 | 
			
		||||
 */
 | 
			
		||||
async function makeStackedPlot(page) {
 | 
			
		||||
    // fresh page with time range from 2022-03-29 22:00:00.000Z to 2022-03-29 22:00:30.000Z
 | 
			
		||||
    await page.goto('/', { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
    // create stacked plot
 | 
			
		||||
    await page.locator('button.c-create-button').click();
 | 
			
		||||
    await page.locator('li:has-text("Stacked Plot")').click();
 | 
			
		||||
 | 
			
		||||
    await Promise.all([
 | 
			
		||||
        page.waitForNavigation({ waitUntil: 'networkidle'}),
 | 
			
		||||
        page.locator('text=OK').click(),
 | 
			
		||||
        //Wait for Save Banner to appear
 | 
			
		||||
        page.waitForSelector('.c-message-banner__message')
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    //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 stacked plot
 | 
			
		||||
    await saveStackedPlot(page);
 | 
			
		||||
 | 
			
		||||
    // create a sinewave generator
 | 
			
		||||
    await createSineWaveGenerator(page);
 | 
			
		||||
 | 
			
		||||
    // click on stacked plot
 | 
			
		||||
    await page.locator('text=Open MCT My Items >> span').nth(3).click();
 | 
			
		||||
    await Promise.all([
 | 
			
		||||
        page.waitForNavigation(),
 | 
			
		||||
        page.locator('text=Unnamed Stacked Plot').first().click()
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    // create a second sinewave generator
 | 
			
		||||
    await createSineWaveGenerator(page);
 | 
			
		||||
 | 
			
		||||
    // click on stacked plot
 | 
			
		||||
    await page.locator('text=Open MCT My Items >> span').nth(3).click();
 | 
			
		||||
    await Promise.all([
 | 
			
		||||
        page.waitForNavigation(),
 | 
			
		||||
        page.locator('text=Unnamed Stacked Plot').first().click()
 | 
			
		||||
    ]);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This is used to save a stacked plot object
 | 
			
		||||
 * @private
 | 
			
		||||
 */
 | 
			
		||||
async function saveStackedPlot(page) {
 | 
			
		||||
    // save stacked plot
 | 
			
		||||
    await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
 | 
			
		||||
 | 
			
		||||
    await Promise.all([
 | 
			
		||||
        page.locator('text=Save and Finish Editing').click(),
 | 
			
		||||
        //Wait for Save Banner to appear
 | 
			
		||||
        page.waitForSelector('.c-message-banner__message')
 | 
			
		||||
    ]);
 | 
			
		||||
    //Wait until Save Banner is gone
 | 
			
		||||
    await page.locator('.c-message-banner__close-button').click();
 | 
			
		||||
    await page.waitForSelector('.c-message-banner__message', { state: 'detached' });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * This is used to create a sine wave generator object
 | 
			
		||||
 * @private
 | 
			
		||||
 */
 | 
			
		||||
async function createSineWaveGenerator(page) {
 | 
			
		||||
    //Create sine wave generator
 | 
			
		||||
    await page.locator('button.c-create-button').click();
 | 
			
		||||
    await page.locator('li:has-text("Sine Wave Generator")').click();
 | 
			
		||||
 | 
			
		||||
    await Promise.all([
 | 
			
		||||
        page.waitForNavigation({ waitUntil: 'networkidle'}),
 | 
			
		||||
        page.locator('text=OK').click(),
 | 
			
		||||
        //Wait for Save Banner to appear
 | 
			
		||||
        page.waitForSelector('.c-message-banner__message')
 | 
			
		||||
    ]);
 | 
			
		||||
    //Wait until Save Banner is gone
 | 
			
		||||
    await page.locator('.c-message-banner__close-button').click();
 | 
			
		||||
    await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
 | 
			
		||||
}
 | 
			
		||||
@@ -1,95 +0,0 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2022, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
const { test } = require('../../../fixtures');
 | 
			
		||||
const { expect } = require('@playwright/test');
 | 
			
		||||
 | 
			
		||||
test.describe('Telemetry Table', () => {
 | 
			
		||||
    test('unpauses when paused by button and user changes bounds', async ({ page }) => {
 | 
			
		||||
        test.info().annotations.push({
 | 
			
		||||
            type: 'issue',
 | 
			
		||||
            description: 'https://github.com/nasa/openmct/issues/5113'
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const bannerMessage = '.c-message-banner__message';
 | 
			
		||||
        const createButton = 'button:has-text("Create")';
 | 
			
		||||
 | 
			
		||||
        await page.goto('/', { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
        // Click create button
 | 
			
		||||
        await page.locator(createButton).click();
 | 
			
		||||
        await page.locator('li:has-text("Telemetry Table")').click();
 | 
			
		||||
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            page.waitForNavigation(),
 | 
			
		||||
            page.locator('text=OK').click(),
 | 
			
		||||
            // Wait for Save Banner to appear
 | 
			
		||||
            page.waitForSelector(bannerMessage)
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        // Save (exit edit mode)
 | 
			
		||||
        await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(3).click();
 | 
			
		||||
        await page.locator('text=Save and Finish Editing').click();
 | 
			
		||||
 | 
			
		||||
        // Click create button
 | 
			
		||||
        await page.locator(createButton).click();
 | 
			
		||||
 | 
			
		||||
        // add Sine Wave Generator with defaults
 | 
			
		||||
        await page.locator('li:has-text("Sine Wave Generator")').click();
 | 
			
		||||
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            page.waitForNavigation(),
 | 
			
		||||
            page.locator('text=OK').click(),
 | 
			
		||||
            // Wait for Save Banner to appear
 | 
			
		||||
            page.waitForSelector(bannerMessage)
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        // focus the Telemetry Table
 | 
			
		||||
        await page.locator('text=Open MCT My Items >> span').nth(3).click();
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            page.waitForNavigation(),
 | 
			
		||||
            page.locator('text=Unnamed Telemetry Table').first().click()
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        // Click pause button
 | 
			
		||||
        const pauseButton = await page.locator('button.c-button.icon-pause');
 | 
			
		||||
        await pauseButton.click();
 | 
			
		||||
 | 
			
		||||
        const tableWrapper = await page.locator('div.c-table-wrapper');
 | 
			
		||||
        await expect(tableWrapper).toHaveClass(/is-paused/);
 | 
			
		||||
 | 
			
		||||
        // Arbitrarily change end date to some time in the future
 | 
			
		||||
        const endTimeInput = page.locator('input[type="text"].c-input--datetime').nth(1);
 | 
			
		||||
        await endTimeInput.click();
 | 
			
		||||
 | 
			
		||||
        let endDate = await endTimeInput.inputValue();
 | 
			
		||||
        endDate = new Date(endDate);
 | 
			
		||||
        endDate.setUTCDate(endDate.getUTCDate() + 1);
 | 
			
		||||
        endDate = endDate.toISOString().replace(/T.*/, '');
 | 
			
		||||
 | 
			
		||||
        await endTimeInput.fill('');
 | 
			
		||||
        await endTimeInput.fill(endDate);
 | 
			
		||||
        await page.keyboard.press('Enter');
 | 
			
		||||
 | 
			
		||||
        await expect(tableWrapper).not.toHaveClass(/is-paused/);
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@@ -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();
 | 
			
		||||
 | 
			
		||||
@@ -69,167 +68,45 @@ test.describe('Time conductor operations', () => {
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// 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
 | 
			
		||||
        //Go to baseURL
 | 
			
		||||
        await page.goto('/', { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
        // Switch to real-time mode
 | 
			
		||||
        await setRealTimeMode(page);
 | 
			
		||||
        // Set realtime "local clock" mode offsets
 | 
			
		||||
        const timeInputs = page.locator('input.c-input--datetime');
 | 
			
		||||
 | 
			
		||||
        // Set start time offset
 | 
			
		||||
        await setStartOffset(page, startOffset);
 | 
			
		||||
        // Click fixed timespan button
 | 
			
		||||
        await page.locator('.c-button__label >> text=Fixed Timespan').click();
 | 
			
		||||
 | 
			
		||||
        // Click local clock 
 | 
			
		||||
        await page.locator('.icon-clock >> text=Local Clock').click();
 | 
			
		||||
 | 
			
		||||
        // Click time offset button
 | 
			
		||||
        await page.locator('.c-conductor__delta-button >> text=00:30:00').click();
 | 
			
		||||
 | 
			
		||||
        // Input start time offset
 | 
			
		||||
        await page.fill('.pr-time-controls__secs', '23');
 | 
			
		||||
 | 
			
		||||
        // Click the check button
 | 
			
		||||
        await page.locator('.icon-check').click();
 | 
			
		||||
 | 
			
		||||
        // Verify time was updated on time offset button
 | 
			
		||||
        await expect(page.locator('data-testid=conductor-start-offset-button')).toContainText('00:30:23');
 | 
			
		||||
        await expect(page.locator('.c-conductor__delta-button').first()).toContainText('00:30:23');
 | 
			
		||||
 | 
			
		||||
        // Set end time offset
 | 
			
		||||
        await setEndOffset(page, endOffset);
 | 
			
		||||
        // Click time offset set preceding now button
 | 
			
		||||
        await page.locator('.c-conductor__delta-button >> text=00:00:30').click();
 | 
			
		||||
 | 
			
		||||
        // Input preceding time offset
 | 
			
		||||
        await page.fill('.pr-time-controls__secs', '31')
 | 
			
		||||
 | 
			
		||||
        // Click the check buttons
 | 
			
		||||
        await page.locator('.icon-check').click();
 | 
			
		||||
 | 
			
		||||
        // Verify time was updated on preceding time offset button
 | 
			
		||||
        await expect(page.locator('data-testid=conductor-end-offset-button')).toContainText('00:00:31');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Verify that offsets and url params are preserved when switching
 | 
			
		||||
     * between fixed timespan and real-time mode.
 | 
			
		||||
     */
 | 
			
		||||
    test('preserve offsets and url params when switching between fixed and real-time mode', async ({ page }) => {
 | 
			
		||||
        const startOffset = {
 | 
			
		||||
            mins: '30',
 | 
			
		||||
            secs: '23'
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const endOffset = {
 | 
			
		||||
            secs: '01'
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Convert offsets to milliseconds
 | 
			
		||||
        const startDelta = (30 * 60 * 1000) + (23 * 1000);
 | 
			
		||||
        const endDelta = (1 * 1000);
 | 
			
		||||
 | 
			
		||||
        // Go to baseURL
 | 
			
		||||
        await page.goto('/', { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
        // Switch to real-time mode
 | 
			
		||||
        await setRealTimeMode(page);
 | 
			
		||||
 | 
			
		||||
        // Set start time offset
 | 
			
		||||
        await setStartOffset(page, startOffset);
 | 
			
		||||
 | 
			
		||||
        // Set end time offset
 | 
			
		||||
        await setEndOffset(page, endOffset);
 | 
			
		||||
 | 
			
		||||
        // Switch to fixed timespan mode
 | 
			
		||||
        await setFixedTimeMode(page);
 | 
			
		||||
 | 
			
		||||
        // Switch back to real-time mode
 | 
			
		||||
        await setRealTimeMode(page);
 | 
			
		||||
 | 
			
		||||
        // Verify updated start time offset persists after mode switch
 | 
			
		||||
        await expect(page.locator('data-testid=conductor-start-offset-button')).toContainText('00:30:23');
 | 
			
		||||
 | 
			
		||||
        // Verify updated end time offset persists after mode switch
 | 
			
		||||
        await expect(page.locator('data-testid=conductor-end-offset-button')).toContainText('00:00:01');
 | 
			
		||||
 | 
			
		||||
        // Verify url parameters persist after mode switch
 | 
			
		||||
        await page.waitForNavigation();
 | 
			
		||||
        expect(page.url()).toContain(`startDelta=${startDelta}`);
 | 
			
		||||
        expect(page.url()).toContain(`endDelta=${endDelta}`);
 | 
			
		||||
        await expect(page.locator('.c-conductor__delta-button').nth(1)).toContainText('00:00:31');
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @typedef {Object} OffsetValues
 | 
			
		||||
 * @property {string | undefined} hours
 | 
			
		||||
 * @property {string | undefined} mins
 | 
			
		||||
 * @property {string | undefined} secs
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Set the values (hours, mins, secs) for the start time offset when in realtime mode
 | 
			
		||||
 * @param {import('@playwright/test').Page} page
 | 
			
		||||
 * @param {OffsetValues} offset
 | 
			
		||||
 */
 | 
			
		||||
async function setStartOffset(page, offset) {
 | 
			
		||||
    const startOffsetButton = page.locator('data-testid=conductor-start-offset-button');
 | 
			
		||||
    await setTimeConductorOffset(page, offset, startOffsetButton);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Set the values (hours, mins, secs) for the end time offset when in realtime mode
 | 
			
		||||
 * @param {import('@playwright/test').Page} page
 | 
			
		||||
 * @param {OffsetValues} offset
 | 
			
		||||
 */
 | 
			
		||||
async function setEndOffset(page, offset) {
 | 
			
		||||
    const endOffsetButton = page.locator('data-testid=conductor-end-offset-button');
 | 
			
		||||
    await setTimeConductorOffset(page, offset, endOffsetButton);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Set the time conductor to fixed timespan mode
 | 
			
		||||
 * @param {import('@playwright/test').Page} page
 | 
			
		||||
 */
 | 
			
		||||
async function setFixedTimeMode(page) {
 | 
			
		||||
    await setTimeConductorMode(page, true);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Set the time conductor to realtime mode
 | 
			
		||||
 * @param {import('@playwright/test').Page} page
 | 
			
		||||
 */
 | 
			
		||||
async function setRealTimeMode(page) {
 | 
			
		||||
    await setTimeConductorMode(page, false);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Set the values (hours, mins, secs) for the TimeConductor offsets when in realtime mode
 | 
			
		||||
 * @param {import('@playwright/test').Page} page
 | 
			
		||||
 * @param {OffsetValues} offset
 | 
			
		||||
 * @param {import('@playwright/test').Locator} offsetButton
 | 
			
		||||
 */
 | 
			
		||||
async function setTimeConductorOffset(page, {hours, mins, secs}, offsetButton) {
 | 
			
		||||
    await offsetButton.click();
 | 
			
		||||
 | 
			
		||||
    if (hours) {
 | 
			
		||||
        await page.fill('.pr-time-controls__hrs', hours);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (mins) {
 | 
			
		||||
        await page.fill('.pr-time-controls__mins', mins);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (secs) {
 | 
			
		||||
        await page.fill('.pr-time-controls__secs', secs);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Click the check button
 | 
			
		||||
    await page.locator('.icon-check').click();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Set the time conductor mode to either fixed timespan or realtime mode.
 | 
			
		||||
 * @param {import('@playwright/test').Page} page
 | 
			
		||||
 * @param {boolean} [isFixedTimespan=true] true for fixed timespan mode, false for realtime mode; default is true
 | 
			
		||||
 */
 | 
			
		||||
async function setTimeConductorMode(page, isFixedTimespan = true) {
 | 
			
		||||
    // Click 'mode' button
 | 
			
		||||
    await page.locator('.c-mode-button').click();
 | 
			
		||||
 | 
			
		||||
    // Switch time conductor mode
 | 
			
		||||
    if (isFixedTimespan) {
 | 
			
		||||
        await page.locator('data-testid=conductor-modeOption-fixed').click();
 | 
			
		||||
    } else {
 | 
			
		||||
        await page.locator('data-testid=conductor-modeOption-realtime').click();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,184 +0,0 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2022, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
const { test } = require('../../../fixtures.js');
 | 
			
		||||
const { expect } = require('@playwright/test');
 | 
			
		||||
 | 
			
		||||
test.describe('Timer', () => {
 | 
			
		||||
 | 
			
		||||
    test.beforeEach(async ({ page }) => {
 | 
			
		||||
        //Go to baseURL
 | 
			
		||||
        await page.goto('/', { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
        //Click the Create button
 | 
			
		||||
        await page.click('button:has-text("Create")');
 | 
			
		||||
 | 
			
		||||
        // Click 'Timer'
 | 
			
		||||
        await page.click('text=Timer');
 | 
			
		||||
 | 
			
		||||
        // Click text=OK
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            page.waitForNavigation({waitUntil: 'networkidle'}),
 | 
			
		||||
            page.click('text=OK')
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Timer');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('Can perform actions on the Timer', async ({ page }) => {
 | 
			
		||||
        test.info().annotations.push({
 | 
			
		||||
            type: 'issue',
 | 
			
		||||
            description: 'https://github.com/nasa/openmct/issues/4313'
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        await test.step("From the tree context menu", async () => {
 | 
			
		||||
            await triggerTimerContextMenuAction(page, 'Start');
 | 
			
		||||
            await triggerTimerContextMenuAction(page, 'Pause');
 | 
			
		||||
            await triggerTimerContextMenuAction(page, 'Restart at 0');
 | 
			
		||||
            await triggerTimerContextMenuAction(page, 'Stop');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        await test.step("From the 3dot menu", async () => {
 | 
			
		||||
            await triggerTimer3dotMenuAction(page, 'Start');
 | 
			
		||||
            await triggerTimer3dotMenuAction(page, 'Pause');
 | 
			
		||||
            await triggerTimer3dotMenuAction(page, 'Restart at 0');
 | 
			
		||||
            await triggerTimer3dotMenuAction(page, 'Stop');
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        await test.step("From the object view", async () => {
 | 
			
		||||
            await triggerTimerViewAction(page, 'Start');
 | 
			
		||||
            await triggerTimerViewAction(page, 'Pause');
 | 
			
		||||
            await triggerTimerViewAction(page, 'Restart at 0');
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Actions that can be performed on a timer from context menus.
 | 
			
		||||
 * @typedef {'Start' | 'Stop' | 'Pause' | 'Restart at 0'} TimerAction
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Actions that can be performed on a timer from the object view.
 | 
			
		||||
 * @typedef {'Start' | 'Pause' | 'Restart at 0'} TimerViewAction
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Open the timer context menu from the object tree.
 | 
			
		||||
 * Expands the 'My Items' folder if it is not already expanded.
 | 
			
		||||
 * @param {import('@playwright/test').Page} page
 | 
			
		||||
 */
 | 
			
		||||
async function openTimerContextMenu(page) {
 | 
			
		||||
    const myItemsFolder = page.locator('text=Open MCT My Items >> span').nth(3);
 | 
			
		||||
    const className = await myItemsFolder.getAttribute('class');
 | 
			
		||||
    if (!className.includes('c-disclosure-triangle--expanded')) {
 | 
			
		||||
        await myItemsFolder.click();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await page.locator(`a:has-text("Unnamed Timer")`).click({
 | 
			
		||||
        button: 'right'
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Trigger a timer action from the tree context menu
 | 
			
		||||
 * @param {import('@playwright/test').Page} page
 | 
			
		||||
 * @param {TimerAction} action
 | 
			
		||||
 */
 | 
			
		||||
async function triggerTimerContextMenuAction(page, action) {
 | 
			
		||||
    const menuAction = `.c-menu ul li >> text="${action}"`;
 | 
			
		||||
    await openTimerContextMenu(page);
 | 
			
		||||
    await page.locator(menuAction).click();
 | 
			
		||||
    assertTimerStateAfterAction(page, action);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Trigger a timer action from the 3dot menu
 | 
			
		||||
 * @param {import('@playwright/test').Page} page
 | 
			
		||||
 * @param {TimerAction} action
 | 
			
		||||
 */
 | 
			
		||||
async function triggerTimer3dotMenuAction(page, action) {
 | 
			
		||||
    const menuAction = `.c-menu ul li >> text="${action}"`;
 | 
			
		||||
    const threeDotMenuButton = 'button[title="More options"]';
 | 
			
		||||
    let isActionAvailable = false;
 | 
			
		||||
    let iterations = 0;
 | 
			
		||||
    // Dismiss/open the 3dot menu until the action is available
 | 
			
		||||
    // or a maxiumum number of iterations is reached
 | 
			
		||||
    while (!isActionAvailable && iterations <= 20) {
 | 
			
		||||
        await page.click('.c-object-view');
 | 
			
		||||
        await page.click(threeDotMenuButton);
 | 
			
		||||
        isActionAvailable = await page.locator(menuAction).isVisible();
 | 
			
		||||
        iterations++;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await page.locator(menuAction).click();
 | 
			
		||||
    assertTimerStateAfterAction(page, action);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Trigger a timer action from the object view
 | 
			
		||||
 * @param {import('@playwright/test').Page} page
 | 
			
		||||
 * @param {TimerViewAction} action
 | 
			
		||||
 */
 | 
			
		||||
async function triggerTimerViewAction(page, action) {
 | 
			
		||||
    const buttonTitle = buttonTitleFromAction(action);
 | 
			
		||||
    await page.click(`button[title="${buttonTitle}"]`);
 | 
			
		||||
    assertTimerStateAfterAction(page, action);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Takes in a TimerViewAction and returns the button title
 | 
			
		||||
 * @param {TimerViewAction} action
 | 
			
		||||
 */
 | 
			
		||||
function buttonTitleFromAction(action) {
 | 
			
		||||
    switch (action) {
 | 
			
		||||
    case 'Start':
 | 
			
		||||
        return 'Start';
 | 
			
		||||
    case 'Pause':
 | 
			
		||||
        return 'Pause';
 | 
			
		||||
    case 'Restart at 0':
 | 
			
		||||
        return 'Reset';
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Verify the timer state after a timer action has been performed.
 | 
			
		||||
 * @param {import('@playwright/test').Page} page
 | 
			
		||||
 * @param {TimerAction} action
 | 
			
		||||
 */
 | 
			
		||||
async function assertTimerStateAfterAction(page, action) {
 | 
			
		||||
    let timerStateClass;
 | 
			
		||||
    switch (action) {
 | 
			
		||||
    case 'Start':
 | 
			
		||||
    case 'Restart at 0':
 | 
			
		||||
        timerStateClass = "is-started";
 | 
			
		||||
        break;
 | 
			
		||||
    case 'Stop':
 | 
			
		||||
        timerStateClass = 'is-stopped';
 | 
			
		||||
        break;
 | 
			
		||||
    case 'Pause':
 | 
			
		||||
        timerStateClass = 'is-paused';
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await expect.soft(page.locator('.c-timer')).toHaveClass(new RegExp(timerStateClass));
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										22
									
								
								e2e/tests/recycled_storage.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								e2e/tests/recycled_storage.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
			
		||||
{
 | 
			
		||||
  "cookies": [],
 | 
			
		||||
  "origins": [
 | 
			
		||||
    {
 | 
			
		||||
      "origin": "http://localhost:8080",
 | 
			
		||||
      "localStorage": [
 | 
			
		||||
        {
 | 
			
		||||
          "name": "tcHistory",
 | 
			
		||||
          "value": "{\"utc\":[{\"start\":1651513945533,\"end\":1651515745533}]}"
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "name": "mct",
 | 
			
		||||
          "value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"c0f99e39-85e7-4ef7-99b1-ef52d4ed69b2\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1651515746374,\"modified\":1651515746374},\"c0f99e39-85e7-4ef7-99b1-ef52d4ed69b2\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"c0f99e39-85e7-4ef7-99b1-ef52d4ed69b2\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"e35a066b-eb0e-4b05-a4c9-cc31dc202572\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1651515746373,\"location\":\"mine\",\"persisted\":1651515746373}}"
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "name": "mct-tree-expanded",
 | 
			
		||||
          "value": "[]"
 | 
			
		||||
        }
 | 
			
		||||
      ]
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
@@ -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 }) => {
 | 
			
		||||
 | 
			
		||||
@@ -45,15 +44,6 @@ test('Verify that the create button appears and that the Folder Domain Object is
 | 
			
		||||
    await page.click('button:has-text("Create")');
 | 
			
		||||
 | 
			
		||||
    // Verify that Create Folder appears in the dropdown
 | 
			
		||||
    await expect(page.locator(':nth-match(:text("Folder"), 2)')).toBeEnabled();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test('Verify that My Items Tree appears @ipad', async ({ page }) => {
 | 
			
		||||
    //Test.slow annotation is currently broken. Needs to be fixed in https://github.com/nasa/openmct/issues/5374
 | 
			
		||||
    test.slow();
 | 
			
		||||
    //Go to baseURL
 | 
			
		||||
    await page.goto('/');
 | 
			
		||||
 | 
			
		||||
    //My Items to be visible
 | 
			
		||||
    await expect(page.locator('a:has-text("My Items")')).toBeEnabled();
 | 
			
		||||
    const locator = page.locator(':nth-match(:text("Folder"), 2)');
 | 
			
		||||
    await expect(locator).toBeEnabled();
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,111 +0,0 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2022, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
This test suite is dedicated to tests which verify search functionality.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
const { expect } = require('@playwright/test');
 | 
			
		||||
const { test } = require('../../../../fixtures');
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
  * Creates a notebook object and adds an entry.
 | 
			
		||||
  * @param {import('@playwright/test').Page} page
 | 
			
		||||
  */
 | 
			
		||||
async function createClockAndDisplayLayout(page) {
 | 
			
		||||
    //Go to baseURL
 | 
			
		||||
    await page.goto('/', { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
    // Click button:has-text("Create")
 | 
			
		||||
    await page.locator('button:has-text("Create")').click();
 | 
			
		||||
    // Click li:has-text("Notebook")
 | 
			
		||||
    await page.locator('li:has-text("Clock")').click();
 | 
			
		||||
    // Click button:has-text("OK")
 | 
			
		||||
    await Promise.all([
 | 
			
		||||
        page.waitForNavigation(),
 | 
			
		||||
        page.locator('button:has-text("OK")').click()
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    // Click a:has-text("My Items")
 | 
			
		||||
    await Promise.all([
 | 
			
		||||
        page.waitForNavigation(),
 | 
			
		||||
        page.locator('a:has-text("My Items") >> nth=0').click()
 | 
			
		||||
    ]);
 | 
			
		||||
    // Click button:has-text("Create")
 | 
			
		||||
    await page.locator('button:has-text("Create")').click();
 | 
			
		||||
    // Click li:has-text("Notebook")
 | 
			
		||||
    await page.locator('li:has-text("Display Layout")').click();
 | 
			
		||||
    // Click button:has-text("OK")
 | 
			
		||||
    await Promise.all([
 | 
			
		||||
        page.waitForNavigation(),
 | 
			
		||||
        page.locator('button:has-text("OK")').click()
 | 
			
		||||
    ]);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
test.describe('Grand Search', () => {
 | 
			
		||||
    test('Can search for objects, and subsequent search dropdown behaves properly', async ({ page }) => {
 | 
			
		||||
        await createClockAndDisplayLayout(page);
 | 
			
		||||
 | 
			
		||||
        // Click [aria-label="OpenMCT Search"] input[type="search"]
 | 
			
		||||
        await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
 | 
			
		||||
        // Fill [aria-label="OpenMCT Search"] input[type="search"]
 | 
			
		||||
        await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Cl');
 | 
			
		||||
        await expect(page.locator('[aria-label="Search Result"]')).toContainText('Clock');
 | 
			
		||||
        // Click text=Elements >> nth=0
 | 
			
		||||
        await page.locator('text=Elements').first().click();
 | 
			
		||||
        await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible();
 | 
			
		||||
 | 
			
		||||
        // Click [aria-label="OpenMCT Search"] [aria-label="Search Input"]
 | 
			
		||||
        await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click();
 | 
			
		||||
        // Click [aria-label="Unnamed Clock clock result"] >> text=Unnamed Clock
 | 
			
		||||
        await page.locator('[aria-label="Unnamed Clock clock result"] >> text=Unnamed Clock').click();
 | 
			
		||||
        await expect(page.locator('.js-preview-window')).toBeVisible();
 | 
			
		||||
 | 
			
		||||
        // Click [aria-label="Close"]
 | 
			
		||||
        await page.locator('[aria-label="Close"]').click();
 | 
			
		||||
        await expect(page.locator('[aria-label="Search Result"]')).toBeVisible();
 | 
			
		||||
        await expect(page.locator('[aria-label="Search Result"]')).toContainText('Cloc');
 | 
			
		||||
 | 
			
		||||
        // Click [aria-label="OpenMCT Search"] a >> nth=0
 | 
			
		||||
        await page.locator('[aria-label="OpenMCT Search"] a').first().click();
 | 
			
		||||
        await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible();
 | 
			
		||||
 | 
			
		||||
        // Fill [aria-label="OpenMCT Search"] input[type="search"]
 | 
			
		||||
        await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('foo');
 | 
			
		||||
        await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible();
 | 
			
		||||
 | 
			
		||||
        // Click text=Snapshot Save and Finish Editing Save and Continue Editing >> button >> nth=1
 | 
			
		||||
        await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
 | 
			
		||||
        // Click text=Save and Finish Editing
 | 
			
		||||
        await page.locator('text=Save and Finish Editing').click();
 | 
			
		||||
        // Click [aria-label="OpenMCT Search"] [aria-label="Search Input"]
 | 
			
		||||
        await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click();
 | 
			
		||||
        // Fill [aria-label="OpenMCT Search"] [aria-label="Search Input"]
 | 
			
		||||
        await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').fill('Cl');
 | 
			
		||||
        // Click text=Unnamed Clock
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            page.waitForNavigation(),
 | 
			
		||||
            page.locator('text=Unnamed Clock').click()
 | 
			
		||||
        ]);
 | 
			
		||||
        await expect(page.locator('.is-object-type-clock')).toBeVisible();
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@@ -1,76 +0,0 @@
 | 
			
		||||
/* eslint-disable no-undef */
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2022, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
Collection of Visual Tests set to run with modified init scripts to inject plugins not otherwise available in the default contexts.
 | 
			
		||||
 | 
			
		||||
These should only use functional expect statements to verify assumptions about the state
 | 
			
		||||
in a test and not for functional verification of correctness. Visual tests are not supposed
 | 
			
		||||
to "fail" on assertions. Instead, they should be used to detect changes between builds or branches.
 | 
			
		||||
 | 
			
		||||
Note: Larger testsuite sizes are OK due to the setup time associated with these tests.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
const { test } = require('@playwright/test');
 | 
			
		||||
const percySnapshot = require('@percy/playwright');
 | 
			
		||||
const path = require('path');
 | 
			
		||||
const sinon = require('sinon');
 | 
			
		||||
 | 
			
		||||
const VISUAL_GRACE_PERIOD = 5 * 1000; //Lets the application "simmer" before the snapshot is taken
 | 
			
		||||
 | 
			
		||||
const CUSTOM_NAME = 'CUSTOM_NAME';
 | 
			
		||||
 | 
			
		||||
// Snippet from https://github.com/microsoft/playwright/issues/6347#issuecomment-965887758
 | 
			
		||||
// Will replace with cy.clock() equivalent
 | 
			
		||||
test.beforeEach(async ({ context }) => {
 | 
			
		||||
    await context.addInitScript({
 | 
			
		||||
        path: path.join(__dirname, '../../..', './node_modules/sinon/pkg/sinon.js')
 | 
			
		||||
    });
 | 
			
		||||
    await context.addInitScript(() => {
 | 
			
		||||
        window.__clock = sinon.useFakeTimers({
 | 
			
		||||
            now: 0,
 | 
			
		||||
            shouldAdvanceTime: true
 | 
			
		||||
        }); //Set browser clock to UNIX Epoch
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test('Visual - Restricted Notebook is visually correct @addInit', async ({ page }) => {
 | 
			
		||||
    // eslint-disable-next-line no-undef
 | 
			
		||||
    await page.addInitScript({ path: path.join(__dirname, '../plugins/notebook', './addInitRestrictedNotebook.js') });
 | 
			
		||||
    //Go to baseURL
 | 
			
		||||
    await page.goto('/', { waitUntil: 'networkidle' });
 | 
			
		||||
    //Click the Create button
 | 
			
		||||
    await page.click('button:has-text("Create")');
 | 
			
		||||
    // Click text=CUSTOM_NAME
 | 
			
		||||
    await page.click(`text=${CUSTOM_NAME}`);
 | 
			
		||||
    // Click text=OK
 | 
			
		||||
    await Promise.all([
 | 
			
		||||
        page.waitForNavigation({waitUntil: 'networkidle'}),
 | 
			
		||||
        page.click('text=OK')
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    // Take a snapshot of the newly created CUSTOM_NAME notebook
 | 
			
		||||
    await page.waitForTimeout(VISUAL_GRACE_PERIOD);
 | 
			
		||||
    await percySnapshot(page, 'Restricted Notebook with CUSTOM_NAME');
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
@@ -1,70 +0,0 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2022, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
Collection of Visual Tests set to run in a default context. The tests within this suite
 | 
			
		||||
are only meant to run against openmct's app.js started by `npm run start` within the
 | 
			
		||||
`./e2e/playwright-visual.config.js` file.
 | 
			
		||||
 | 
			
		||||
These should only use functional expect statements to verify assumptions about the state
 | 
			
		||||
in a test and not for functional verification of correctness. Visual tests are not supposed
 | 
			
		||||
to "fail" on assertions. Instead, they should be used to detect changes between builds or branches.
 | 
			
		||||
 | 
			
		||||
Note: Larger testsuite sizes are OK due to the setup time associated with these tests.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
const { test, expect } = require('@playwright/test');
 | 
			
		||||
const percySnapshot = require('@percy/playwright');
 | 
			
		||||
const path = require('path');
 | 
			
		||||
const sinon = require('sinon');
 | 
			
		||||
 | 
			
		||||
// Snippet from https://github.com/microsoft/playwright/issues/6347#issuecomment-965887758
 | 
			
		||||
// Will replace with cy.clock() equivalent
 | 
			
		||||
test.beforeEach(async ({ context }) => {
 | 
			
		||||
    await context.addInitScript({
 | 
			
		||||
        // eslint-disable-next-line no-undef
 | 
			
		||||
        path: path.join(__dirname, '../../..', './node_modules/sinon/pkg/sinon.js')
 | 
			
		||||
    });
 | 
			
		||||
    await context.addInitScript(() => {
 | 
			
		||||
        window.__clock = sinon.useFakeTimers({
 | 
			
		||||
            now: 0, //Set browser clock to UNIX Epoch
 | 
			
		||||
            shouldAdvanceTime: false, //Don't advance the clock
 | 
			
		||||
            toFake: ["setTimeout", "nextTick"]
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
test.use({ storageState: './e2e/test-data/VisualTestData_storage.json' });
 | 
			
		||||
 | 
			
		||||
test('Visual - Overlay Plot Loading Indicator @localstorage', async ({ page }) => {
 | 
			
		||||
    // Go to baseURL
 | 
			
		||||
    await page.goto('/', { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
    await page.locator('a:has-text("Unnamed Overlay Plot Overlay Plot")').click();
 | 
			
		||||
    //Ensure that we're on the Unnamed Overlay Plot object
 | 
			
		||||
    await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Overlay Plot');
 | 
			
		||||
 | 
			
		||||
    //Wait for canvas to be rendered and stop animating
 | 
			
		||||
    await page.locator('canvas >> nth=1').hover({trial: true});
 | 
			
		||||
 | 
			
		||||
    //Take snapshot of Sine Wave Generator within Overlay Plot
 | 
			
		||||
    await percySnapshot(page, 'SineWaveInOverlayPlot');
 | 
			
		||||
});
 | 
			
		||||
@@ -32,8 +32,7 @@ to "fail" on assertions. Instead, they should be used to detect changes between
 | 
			
		||||
Note: Larger testsuite sizes are OK due to the setup time associated with these tests.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
const { test } = require('../../fixtures.js');
 | 
			
		||||
const { expect } = require('@playwright/test');
 | 
			
		||||
const { test, expect } = require('@playwright/test');
 | 
			
		||||
const percySnapshot = require('@percy/playwright');
 | 
			
		||||
const path = require('path');
 | 
			
		||||
const sinon = require('sinon');
 | 
			
		||||
@@ -48,10 +47,7 @@ test.beforeEach(async ({ context }) => {
 | 
			
		||||
        path: path.join(__dirname, '../../..', './node_modules/sinon/pkg/sinon.js')
 | 
			
		||||
    });
 | 
			
		||||
    await context.addInitScript(() => {
 | 
			
		||||
        window.__clock = sinon.useFakeTimers({
 | 
			
		||||
            now: 0,
 | 
			
		||||
            shouldAdvanceTime: true
 | 
			
		||||
        }); //Set browser clock to UNIX Epoch
 | 
			
		||||
        window.__clock = sinon.useFakeTimers(); //Set browser clock to UNIX Epoch
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@@ -60,7 +56,8 @@ test('Visual - Root and About', async ({ page }) => {
 | 
			
		||||
    await page.goto('/', { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
    // Verify that Create button is actionable
 | 
			
		||||
    await expect(page.locator('button:has-text("Create")')).toBeEnabled();
 | 
			
		||||
    const createButtonLocator = page.locator('button:has-text("Create")');
 | 
			
		||||
    await expect(createButtonLocator).toBeEnabled();
 | 
			
		||||
 | 
			
		||||
    // Take a snapshot of the Dashboard
 | 
			
		||||
    await page.waitForTimeout(VISUAL_GRACE_PERIOD);
 | 
			
		||||
@@ -97,11 +94,7 @@ test('Visual - Default Condition Set', async ({ page }) => {
 | 
			
		||||
    await percySnapshot(page, 'Default Condition Set');
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test.fixme('Visual - Default Condition Widget', async ({ page }) => {
 | 
			
		||||
    test.info().annotations.push({
 | 
			
		||||
        type: 'issue',
 | 
			
		||||
        description: 'https://github.com/nasa/openmct/issues/5349'
 | 
			
		||||
    });
 | 
			
		||||
test('Visual - Default Condition Widget', async ({ page }) => {
 | 
			
		||||
    //Go to baseURL
 | 
			
		||||
    await page.goto('/', { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
@@ -178,55 +171,3 @@ test('Visual - Sine Wave Generator Form', async ({ page }) => {
 | 
			
		||||
    await page.waitForTimeout(VISUAL_GRACE_PERIOD);
 | 
			
		||||
    await percySnapshot(page, 'removed amplitude property value');
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test('Visual - Save Successful Banner', async ({ page }) => {
 | 
			
		||||
    //Go to baseURL
 | 
			
		||||
    await page.goto('/', { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
    //Click the Create button
 | 
			
		||||
    await page.click('button:has-text("Create")');
 | 
			
		||||
 | 
			
		||||
    //NOTE Something other than example imagery
 | 
			
		||||
    await page.click('text=Timer');
 | 
			
		||||
 | 
			
		||||
    // Click text=OK
 | 
			
		||||
    await page.click('text=OK');
 | 
			
		||||
    await page.locator('.c-message-banner__message').hover({ trial: true });
 | 
			
		||||
    await percySnapshot(page, 'Banner message shown');
 | 
			
		||||
 | 
			
		||||
    //Wait until Save Banner is gone
 | 
			
		||||
    await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
 | 
			
		||||
    await percySnapshot(page, 'Banner message gone');
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test('Visual - Display Layout Icon is correct', async ({ page }) => {
 | 
			
		||||
    //Go to baseURL
 | 
			
		||||
    await page.goto('/', { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
    //Click the Create button
 | 
			
		||||
    await page.click('button:has-text("Create")');
 | 
			
		||||
 | 
			
		||||
    //Hover on Display Layout option.
 | 
			
		||||
    await page.locator('text=Display Layout').hover();
 | 
			
		||||
    await percySnapshot(page, 'Display Layout Create Menu');
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test('Visual - Default Gauge is correct', async ({ page }) => {
 | 
			
		||||
 | 
			
		||||
    //Go to baseURL
 | 
			
		||||
    await page.goto('/', { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
    //Click the Create button
 | 
			
		||||
    await page.click('button:has-text("Create")');
 | 
			
		||||
 | 
			
		||||
    await page.click('text=Gauge');
 | 
			
		||||
 | 
			
		||||
    await page.click('text=OK');
 | 
			
		||||
 | 
			
		||||
    // Take a snapshot of the newly created Gauge object
 | 
			
		||||
    await page.waitForTimeout(VISUAL_GRACE_PERIOD);
 | 
			
		||||
    await percySnapshot(page, 'Default Gauge');
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,95 +0,0 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2022, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
This test suite is dedicated to generating LocalStorage via Session Storage to be used
 | 
			
		||||
in some visual test suites like controlledClock.visual.spec.js. This suite should run to completion
 | 
			
		||||
and generate an artifact named ./e2e/test-data/VisualTestData_storage.json . This will run
 | 
			
		||||
on every Commit to ensure that this object still loads into tests correctly and will retain the
 | 
			
		||||
.e2e.spec.js suffix.
 | 
			
		||||
 | 
			
		||||
TODO: Provide additional validation of object properties as it grows.
 | 
			
		||||
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
const { test } = require('../../fixtures.js');
 | 
			
		||||
const { expect } = require('@playwright/test');
 | 
			
		||||
 | 
			
		||||
test('Generate Visual Test Data @localStorage', async ({ page, context }) => {
 | 
			
		||||
    //Go to baseURL
 | 
			
		||||
    await page.goto('/', { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
    await page.locator('button:has-text("Create")').click();
 | 
			
		||||
 | 
			
		||||
    // add overlay plot with defaults
 | 
			
		||||
    await page.locator('li:has-text("Overlay Plot")').click();
 | 
			
		||||
 | 
			
		||||
    // Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184
 | 
			
		||||
    await page.click('form[name="mctForm"] a:has-text("My Items")');
 | 
			
		||||
 | 
			
		||||
    await Promise.all([
 | 
			
		||||
        page.waitForNavigation(),
 | 
			
		||||
        page.locator('text=OK').click(),
 | 
			
		||||
        //Wait for Save Banner to appear1
 | 
			
		||||
        page.waitForSelector('.c-message-banner__message')
 | 
			
		||||
    ]);
 | 
			
		||||
    //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();
 | 
			
		||||
 | 
			
		||||
    //Add a 5000 ms Delay
 | 
			
		||||
    await page.locator('[aria-label="Loading Delay \\(ms\\)"]').fill('5000');
 | 
			
		||||
 | 
			
		||||
    // Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184
 | 
			
		||||
    await page.click('form[name="mctForm"] a:has-text("Overlay Plot")');
 | 
			
		||||
 | 
			
		||||
    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()
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Overlay Plot');
 | 
			
		||||
    //Save localStorage for future test execution
 | 
			
		||||
    await context.storageState({ path: './e2e/test-data/VisualTestData_storage.json' });
 | 
			
		||||
});
 | 
			
		||||
@@ -1,104 +0,0 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2022, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
This test suite is dedicated to tests which verify search functionality.
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
const { test, expect } = require('@playwright/test');
 | 
			
		||||
const percySnapshot = require('@percy/playwright');
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
  * Creates a notebook object and adds an entry.
 | 
			
		||||
  * @param {import('@playwright/test').Page} page
 | 
			
		||||
  */
 | 
			
		||||
async function createClockAndDisplayLayout(page) {
 | 
			
		||||
    //Go to baseURL
 | 
			
		||||
    await page.goto('/', { waitUntil: 'networkidle' });
 | 
			
		||||
 | 
			
		||||
    // Click button:has-text("Create")
 | 
			
		||||
    await page.locator('button:has-text("Create")').click();
 | 
			
		||||
    // Click li:has-text("Notebook")
 | 
			
		||||
    await page.locator('li:has-text("Clock")').click();
 | 
			
		||||
    // Click button:has-text("OK")
 | 
			
		||||
    await Promise.all([
 | 
			
		||||
        page.waitForNavigation(),
 | 
			
		||||
        page.locator('button:has-text("OK")').click()
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    // Click a:has-text("My Items")
 | 
			
		||||
    await Promise.all([
 | 
			
		||||
        page.waitForNavigation(),
 | 
			
		||||
        page.locator('a:has-text("My Items") >> nth=0').click()
 | 
			
		||||
    ]);
 | 
			
		||||
    // Click button:has-text("Create")
 | 
			
		||||
    await page.locator('button:has-text("Create")').click();
 | 
			
		||||
    // Click li:has-text("Notebook")
 | 
			
		||||
    await page.locator('li:has-text("Display Layout")').click();
 | 
			
		||||
    // Click button:has-text("OK")
 | 
			
		||||
    await Promise.all([
 | 
			
		||||
        page.waitForNavigation(),
 | 
			
		||||
        page.locator('button:has-text("OK")').click()
 | 
			
		||||
    ]);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
test.describe('Grand Search', () => {
 | 
			
		||||
    test('Can search for objects, and subsequent search dropdown behaves properly', async ({ page }) => {
 | 
			
		||||
        await createClockAndDisplayLayout(page);
 | 
			
		||||
 | 
			
		||||
        // Click [aria-label="OpenMCT Search"] input[type="search"]
 | 
			
		||||
        await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
 | 
			
		||||
        // Fill [aria-label="OpenMCT Search"] input[type="search"]
 | 
			
		||||
        await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Cl');
 | 
			
		||||
        await expect(page.locator('[aria-label="Search Result"]')).toContainText('Clock');
 | 
			
		||||
        await percySnapshot(page, 'Searching for Clocks');
 | 
			
		||||
        // Click text=Elements >> nth=0
 | 
			
		||||
        await page.locator('text=Elements').first().click();
 | 
			
		||||
        await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible();
 | 
			
		||||
 | 
			
		||||
        // Click [aria-label="OpenMCT Search"] [aria-label="Search Input"]
 | 
			
		||||
        await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click();
 | 
			
		||||
        // Click [aria-label="Unnamed Clock clock result"] >> text=Unnamed Clock
 | 
			
		||||
        await page.locator('[aria-label="Unnamed Clock clock result"] >> text=Unnamed Clock').click();
 | 
			
		||||
        await percySnapshot(page, 'Preview for clock should display when editing enabled and search item clicked');
 | 
			
		||||
 | 
			
		||||
        // Click [aria-label="Close"]
 | 
			
		||||
        await page.locator('[aria-label="Close"]').click();
 | 
			
		||||
        await percySnapshot(page, 'Search should still be showing after preview closed');
 | 
			
		||||
 | 
			
		||||
        // Click text=Snapshot Save and Finish Editing Save and Continue Editing >> button >> nth=1
 | 
			
		||||
        await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
 | 
			
		||||
        // Click text=Save and Finish Editing
 | 
			
		||||
        await page.locator('text=Save and Finish Editing').click();
 | 
			
		||||
        // Click [aria-label="OpenMCT Search"] [aria-label="Search Input"]
 | 
			
		||||
        await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click();
 | 
			
		||||
        // Fill [aria-label="OpenMCT Search"] [aria-label="Search Input"]
 | 
			
		||||
        await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').fill('Cl');
 | 
			
		||||
        // Click text=Unnamed Clock
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            page.waitForNavigation(),
 | 
			
		||||
            page.locator('text=Unnamed Clock').click()
 | 
			
		||||
        ]);
 | 
			
		||||
        await percySnapshot(page, 'Clicking on search results should navigate to them if not editing');
 | 
			
		||||
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@@ -1,33 +0,0 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2022, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
import availableTags from './tags.json';
 | 
			
		||||
/**
 | 
			
		||||
 * @returns {function} The plugin install function
 | 
			
		||||
 */
 | 
			
		||||
export default function exampleTagsPlugin() {
 | 
			
		||||
    return function install(openmct) {
 | 
			
		||||
        Object.keys(availableTags.tags).forEach(tagKey => {
 | 
			
		||||
            const tagDefinition = availableTags.tags[tagKey];
 | 
			
		||||
            openmct.annotation.defineTag(tagKey, tagDefinition);
 | 
			
		||||
        });
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
@@ -1,19 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
    "tags": {
 | 
			
		||||
        "46a62ad1-bb86-4f88-9a17-2a029e12669d": {
 | 
			
		||||
            "label": "Science",
 | 
			
		||||
            "backgroundColor": "#cc0000",
 | 
			
		||||
            "foregroundColor": "#ffffff"
 | 
			
		||||
        },
 | 
			
		||||
        "65f150ef-73b7-409a-b2e8-258cbd8b7323": {
 | 
			
		||||
            "label": "Driving",
 | 
			
		||||
            "backgroundColor": "#ffad32",
 | 
			
		||||
            "foregroundColor": "#333333"
 | 
			
		||||
        },
 | 
			
		||||
        "f156b038-c605-46db-88a6-67cf2489a371": {
 | 
			
		||||
            "label": "Drilling",
 | 
			
		||||
            "backgroundColor": "#b0ac4e",
 | 
			
		||||
            "foregroundColor": "#FFFFFF"
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -21,56 +21,19 @@
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
import EventEmitter from 'EventEmitter';
 | 
			
		||||
import { v4 as uuid } from 'uuid';
 | 
			
		||||
import uuid from 'uuid';
 | 
			
		||||
import createExampleUser from './exampleUserCreator';
 | 
			
		||||
 | 
			
		||||
const STATUSES = [{
 | 
			
		||||
    key: "NO_STATUS",
 | 
			
		||||
    label: "Not set",
 | 
			
		||||
    iconClass: "icon-question-mark",
 | 
			
		||||
    iconClassPoll: "icon-status-poll-question-mark"
 | 
			
		||||
}, {
 | 
			
		||||
    key: "GO",
 | 
			
		||||
    label: "GO",
 | 
			
		||||
    iconClass: "icon-check",
 | 
			
		||||
    iconClassPoll: "icon-status-poll-question-mark",
 | 
			
		||||
    statusClass: "s-status-ok",
 | 
			
		||||
    statusBgColor: "#33cc33",
 | 
			
		||||
    statusFgColor: "#000"
 | 
			
		||||
}, {
 | 
			
		||||
    key: "MAYBE",
 | 
			
		||||
    label: "MAYBE",
 | 
			
		||||
    iconClass: "icon-alert-triangle",
 | 
			
		||||
    iconClassPoll: "icon-status-poll-question-mark",
 | 
			
		||||
    statusClass: "s-status-warning",
 | 
			
		||||
    statusBgColor: "#ffb66c",
 | 
			
		||||
    statusFgColor: "#000"
 | 
			
		||||
}, {
 | 
			
		||||
    key: "NO_GO",
 | 
			
		||||
    label: "NO GO",
 | 
			
		||||
    iconClass: "icon-circle-slash",
 | 
			
		||||
    iconClassPoll: "icon-status-poll-question-mark",
 | 
			
		||||
    statusClass: "s-status-error",
 | 
			
		||||
    statusBgColor: "#9900cc",
 | 
			
		||||
    statusFgColor: "#fff"
 | 
			
		||||
}];
 | 
			
		||||
/**
 | 
			
		||||
 * @implements {StatusUserProvider}
 | 
			
		||||
 */
 | 
			
		||||
export default class ExampleUserProvider extends EventEmitter {
 | 
			
		||||
    constructor(openmct, {defaultStatusRole} = {defaultStatusRole: undefined}) {
 | 
			
		||||
    constructor(openmct) {
 | 
			
		||||
        super();
 | 
			
		||||
 | 
			
		||||
        this.openmct = openmct;
 | 
			
		||||
        this.user = undefined;
 | 
			
		||||
        this.loggedIn = false;
 | 
			
		||||
        this.autoLoginUser = undefined;
 | 
			
		||||
        this.status = STATUSES[1];
 | 
			
		||||
        this.pollQuestion = undefined;
 | 
			
		||||
        this.defaultStatusRole = defaultStatusRole;
 | 
			
		||||
 | 
			
		||||
        this.ExampleUser = createExampleUser(this.openmct.user.User);
 | 
			
		||||
        this.loginPromise = undefined;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    isLoggedIn() {
 | 
			
		||||
@@ -82,19 +45,11 @@ export default class ExampleUserProvider extends EventEmitter {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getCurrentUser() {
 | 
			
		||||
        if (!this.loginPromise) {
 | 
			
		||||
            this.loginPromise = this._login().then(() => this.user);
 | 
			
		||||
        if (this.loggedIn) {
 | 
			
		||||
            return Promise.resolve(this.user);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return this.loginPromise;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    canProvideStatusForRole() {
 | 
			
		||||
        return Promise.resolve(true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    canSetPollQuestion() {
 | 
			
		||||
        return Promise.resolve(true);
 | 
			
		||||
        return this._login().then(() => this.user);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    hasRole(roleId) {
 | 
			
		||||
@@ -105,55 +60,6 @@ export default class ExampleUserProvider extends EventEmitter {
 | 
			
		||||
        return Promise.resolve(this.user.getRoles().includes(roleId));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getStatusRoleForCurrentUser() {
 | 
			
		||||
        return Promise.resolve(this.defaultStatusRole);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getAllStatusRoles() {
 | 
			
		||||
        return Promise.resolve([this.defaultStatusRole]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getStatusForRole(role) {
 | 
			
		||||
        return Promise.resolve(this.status);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getDefaultStatusForRole(role) {
 | 
			
		||||
        const allRoles = await this.getPossibleStatuses();
 | 
			
		||||
 | 
			
		||||
        return allRoles?.[0];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setStatusForRole(role, status) {
 | 
			
		||||
        this.status = status;
 | 
			
		||||
        this.emit('statusChange', {
 | 
			
		||||
            role,
 | 
			
		||||
            status
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getPollQuestion() {
 | 
			
		||||
        return Promise.resolve({
 | 
			
		||||
            question: 'Set "GO" if your position is ready for a boarding action on the Klingon cruiser',
 | 
			
		||||
            timestamp: Date.now()
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setPollQuestion(pollQuestion) {
 | 
			
		||||
        this.pollQuestion = {
 | 
			
		||||
            question: pollQuestion,
 | 
			
		||||
            timestamp: Date.now()
 | 
			
		||||
        };
 | 
			
		||||
        this.emit("pollQuestionChange", this.pollQuestion);
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getPossibleStatuses() {
 | 
			
		||||
        return Promise.resolve(STATUSES);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _login() {
 | 
			
		||||
        const id = uuid();
 | 
			
		||||
 | 
			
		||||
@@ -202,6 +108,3 @@ export default class ExampleUserProvider extends EventEmitter {
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
/**
 | 
			
		||||
 * @typedef {import('@/api/user/StatusUserProvider').default} StatusUserProvider
 | 
			
		||||
 */
 | 
			
		||||
 
 | 
			
		||||
@@ -22,19 +22,8 @@
 | 
			
		||||
 | 
			
		||||
import ExampleUserProvider from './ExampleUserProvider';
 | 
			
		||||
 | 
			
		||||
export default function ExampleUserPlugin({autoLoginUser, defaultStatusRole} = {
 | 
			
		||||
    autoLoginUser: 'guest',
 | 
			
		||||
    defaultStatusRole: 'test-role'
 | 
			
		||||
}) {
 | 
			
		||||
export default function ExampleUserPlugin() {
 | 
			
		||||
    return function install(openmct) {
 | 
			
		||||
        const userProvider = new ExampleUserProvider(openmct, {
 | 
			
		||||
            defaultStatusRole
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (autoLoginUser !== undefined) {
 | 
			
		||||
            userProvider.autoLogin(autoLoginUser);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        openmct.user.setProvider(userProvider);
 | 
			
		||||
        openmct.user.setProvider(new ExampleUserProvider(openmct));
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -26,7 +26,7 @@ import {
 | 
			
		||||
} from '../../src/utils/testing';
 | 
			
		||||
import ExampleUserProvider from './ExampleUserProvider';
 | 
			
		||||
 | 
			
		||||
describe("The Example User Plugin", () => {
 | 
			
		||||
xdescribe("The Example User Plugin", () => {
 | 
			
		||||
    let openmct;
 | 
			
		||||
 | 
			
		||||
    beforeEach(() => {
 | 
			
		||||
@@ -47,4 +47,9 @@ describe("The Example User Plugin", () => {
 | 
			
		||||
        });
 | 
			
		||||
        openmct.install(openmct.plugins.example.ExampleUser());
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // The rest of the functionality of the ExampleUser Plugin is
 | 
			
		||||
    // tested in both the UserAPISpec.js and in the UserIndicatorPlugin spec.
 | 
			
		||||
    // If that changes, those tests can be moved here.
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,83 +0,0 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2022, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
export default function () {
 | 
			
		||||
    return function install(openmct) {
 | 
			
		||||
        openmct.install(openmct.plugins.FaultManagement());
 | 
			
		||||
 | 
			
		||||
        openmct.faults.addProvider({
 | 
			
		||||
            request(domainObject, options) {
 | 
			
		||||
                const faults = JSON.parse(localStorage.getItem('faults'));
 | 
			
		||||
 | 
			
		||||
                return Promise.resolve(faults.alarms);
 | 
			
		||||
            },
 | 
			
		||||
            subscribe(domainObject, callback) {
 | 
			
		||||
                const faultsData = JSON.parse(localStorage.getItem('faults')).alarms;
 | 
			
		||||
 | 
			
		||||
                function getRandomIndex(start, end) {
 | 
			
		||||
                    return Math.floor(start + (Math.random() * (end - start + 1)));
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                let id = setInterval(() => {
 | 
			
		||||
                    const index = getRandomIndex(0, faultsData.length - 1);
 | 
			
		||||
                    const randomFaultData = faultsData[index];
 | 
			
		||||
                    const randomFault = randomFaultData.fault;
 | 
			
		||||
                    randomFault.currentValueInfo.value = Math.random();
 | 
			
		||||
                    callback({
 | 
			
		||||
                        fault: randomFault,
 | 
			
		||||
                        type: 'alarms'
 | 
			
		||||
                    });
 | 
			
		||||
                }, 300);
 | 
			
		||||
 | 
			
		||||
                return () => {
 | 
			
		||||
                    clearInterval(id);
 | 
			
		||||
                };
 | 
			
		||||
            },
 | 
			
		||||
            supportsRequest(domainObject) {
 | 
			
		||||
                const faults = localStorage.getItem('faults');
 | 
			
		||||
 | 
			
		||||
                return faults && domainObject.type === 'faultManagement';
 | 
			
		||||
            },
 | 
			
		||||
            supportsSubscribe(domainObject) {
 | 
			
		||||
                const faults = localStorage.getItem('faults');
 | 
			
		||||
 | 
			
		||||
                return faults && domainObject.type === 'faultManagement';
 | 
			
		||||
            },
 | 
			
		||||
            acknowledgeFault(fault, { comment = '' }) {
 | 
			
		||||
                console.log('acknowledgeFault', fault);
 | 
			
		||||
                console.log('comment', comment);
 | 
			
		||||
 | 
			
		||||
                return Promise.resolve({
 | 
			
		||||
                    success: true
 | 
			
		||||
                });
 | 
			
		||||
            },
 | 
			
		||||
            shelveFault(fault, shelveData) {
 | 
			
		||||
                console.log('shelveFault', fault);
 | 
			
		||||
                console.log('shelveData', shelveData);
 | 
			
		||||
 | 
			
		||||
                return Promise.resolve({
 | 
			
		||||
                    success: true
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
@@ -1,47 +0,0 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2022, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
    createOpenMct,
 | 
			
		||||
    resetApplicationState
 | 
			
		||||
} from '../../src/utils/testing';
 | 
			
		||||
 | 
			
		||||
describe("The Example Fault Source Plugin", () => {
 | 
			
		||||
    let openmct;
 | 
			
		||||
 | 
			
		||||
    beforeEach(() => {
 | 
			
		||||
        openmct = createOpenMct();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(() => {
 | 
			
		||||
        return resetApplicationState(openmct);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('is not installed by default', () => {
 | 
			
		||||
        expect(openmct.faults.provider).toBeUndefined();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('can be installed', () => {
 | 
			
		||||
        openmct.install(openmct.plugins.example.ExampleFaultSource());
 | 
			
		||||
        expect(openmct.faults.provider).not.toBeUndefined();
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@@ -29,12 +29,12 @@ define([
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    key: "wavelengths",
 | 
			
		||||
                    name: "Wavelength",
 | 
			
		||||
                    unit: "nm",
 | 
			
		||||
                    format: 'string[]',
 | 
			
		||||
                    key: "cos",
 | 
			
		||||
                    name: "Cosine",
 | 
			
		||||
                    unit: "deg",
 | 
			
		||||
                    formatString: '%0.2f',
 | 
			
		||||
                    hints: {
 | 
			
		||||
                        range: 4
 | 
			
		||||
                        domain: 3
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
                // Need to enable "LocalTimeSystem" plugin to make use of this
 | 
			
		||||
@@ -64,14 +64,6 @@ define([
 | 
			
		||||
                    hints: {
 | 
			
		||||
                        range: 2
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    key: "intensities",
 | 
			
		||||
                    name: "Intensities",
 | 
			
		||||
                    format: 'number[]',
 | 
			
		||||
                    hints: {
 | 
			
		||||
                        range: 3
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            ]
 | 
			
		||||
        },
 | 
			
		||||
 
 | 
			
		||||
@@ -32,8 +32,7 @@ define([
 | 
			
		||||
        offset: 0,
 | 
			
		||||
        dataRateInHz: 1,
 | 
			
		||||
        randomness: 0,
 | 
			
		||||
        phase: 0,
 | 
			
		||||
        loadDelay: 0
 | 
			
		||||
        phase: 0
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    function GeneratorProvider(openmct) {
 | 
			
		||||
@@ -54,9 +53,8 @@ define([
 | 
			
		||||
            'period',
 | 
			
		||||
            'offset',
 | 
			
		||||
            'dataRateInHz',
 | 
			
		||||
            'randomness',
 | 
			
		||||
            'phase',
 | 
			
		||||
            'loadDelay'
 | 
			
		||||
            'randomness'
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        request = request || {};
 | 
			
		||||
 
 | 
			
		||||
@@ -23,7 +23,7 @@
 | 
			
		||||
define([
 | 
			
		||||
    'uuid'
 | 
			
		||||
], function (
 | 
			
		||||
    { v4: uuid }
 | 
			
		||||
    uuid
 | 
			
		||||
) {
 | 
			
		||||
    function WorkerInterface(openmct) {
 | 
			
		||||
        // eslint-disable-next-line no-undef
 | 
			
		||||
 
 | 
			
		||||
@@ -77,8 +77,7 @@
 | 
			
		||||
                            utc: nextStep,
 | 
			
		||||
                            yesterday: nextStep - 60 * 60 * 24 * 1000,
 | 
			
		||||
                            sin: sin(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness),
 | 
			
		||||
                            wavelengths: wavelengths(),
 | 
			
		||||
                            intensities: intensities(),
 | 
			
		||||
                            wavelength: wavelength(start, nextStep),
 | 
			
		||||
                            cos: cos(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness)
 | 
			
		||||
                        }
 | 
			
		||||
                    });
 | 
			
		||||
@@ -116,7 +115,6 @@
 | 
			
		||||
        var dataRateInHz = request.dataRateInHz;
 | 
			
		||||
        var phase = request.phase;
 | 
			
		||||
        var randomness = request.randomness;
 | 
			
		||||
        var loadDelay = Math.max(request.loadDelay, 0);
 | 
			
		||||
 | 
			
		||||
        var step = 1000 / dataRateInHz;
 | 
			
		||||
        var nextStep = start - (start % step) + step;
 | 
			
		||||
@@ -128,20 +126,11 @@
 | 
			
		||||
                utc: nextStep,
 | 
			
		||||
                yesterday: nextStep - 60 * 60 * 24 * 1000,
 | 
			
		||||
                sin: sin(nextStep, period, amplitude, offset, phase, randomness),
 | 
			
		||||
                wavelengths: wavelengths(),
 | 
			
		||||
                intensities: intensities(),
 | 
			
		||||
                wavelength: wavelength(start, nextStep),
 | 
			
		||||
                cos: cos(nextStep, period, amplitude, offset, phase, randomness)
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (loadDelay === 0) {
 | 
			
		||||
            postOnRequest(message, request, data);
 | 
			
		||||
        } else {
 | 
			
		||||
            setTimeout(() => postOnRequest(message, request, data), loadDelay);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function postOnRequest(message, request, data) {
 | 
			
		||||
        self.postMessage({
 | 
			
		||||
            id: message.id,
 | 
			
		||||
            data: request.spectra ? {
 | 
			
		||||
@@ -165,28 +154,8 @@
 | 
			
		||||
            * Math.sin(phase + (timestamp / period / 1000 * Math.PI * 2)) + (amplitude * Math.random() * randomness) + offset;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function wavelengths() {
 | 
			
		||||
        let values = [];
 | 
			
		||||
        while (values.length < 5) {
 | 
			
		||||
            const randomValue = Math.random() * 100;
 | 
			
		||||
            if (!values.includes(randomValue)) {
 | 
			
		||||
                values.push(String(randomValue));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return values;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function intensities() {
 | 
			
		||||
        let values = [];
 | 
			
		||||
        while (values.length < 5) {
 | 
			
		||||
            const randomValue = Math.random() * 10;
 | 
			
		||||
            if (!values.includes(randomValue)) {
 | 
			
		||||
                values.push(String(randomValue));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return values;
 | 
			
		||||
    function wavelength(start, nextStep) {
 | 
			
		||||
        return (nextStep - start) / 10;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function sendError(error, message) {
 | 
			
		||||
 
 | 
			
		||||
@@ -81,7 +81,7 @@ define([
 | 
			
		||||
                {
 | 
			
		||||
                    name: "Amplitude",
 | 
			
		||||
                    control: "numberfield",
 | 
			
		||||
                    cssClass: "l-numeric",
 | 
			
		||||
                    cssClass: "l-input-sm l-numeric",
 | 
			
		||||
                    key: "amplitude",
 | 
			
		||||
                    required: true,
 | 
			
		||||
                    property: [
 | 
			
		||||
@@ -92,7 +92,7 @@ define([
 | 
			
		||||
                {
 | 
			
		||||
                    name: "Offset",
 | 
			
		||||
                    control: "numberfield",
 | 
			
		||||
                    cssClass: "l-numeric",
 | 
			
		||||
                    cssClass: "l-input-sm l-numeric",
 | 
			
		||||
                    key: "offset",
 | 
			
		||||
                    required: true,
 | 
			
		||||
                    property: [
 | 
			
		||||
@@ -132,17 +132,6 @@ define([
 | 
			
		||||
                        "telemetry",
 | 
			
		||||
                        "randomness"
 | 
			
		||||
                    ]
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    name: "Loading Delay (ms)",
 | 
			
		||||
                    control: "numberfield",
 | 
			
		||||
                    cssClass: "l-input-sm l-numeric",
 | 
			
		||||
                    key: "loadDelay",
 | 
			
		||||
                    required: true,
 | 
			
		||||
                    property: [
 | 
			
		||||
                        "telemetry",
 | 
			
		||||
                        "loadDelay"
 | 
			
		||||
                    ]
 | 
			
		||||
                }
 | 
			
		||||
            ],
 | 
			
		||||
            initialize: function (object) {
 | 
			
		||||
@@ -152,8 +141,7 @@ define([
 | 
			
		||||
                    offset: 0,
 | 
			
		||||
                    dataRateInHz: 1,
 | 
			
		||||
                    phase: 0,
 | 
			
		||||
                    randomness: 0,
 | 
			
		||||
                    loadDelay: 0
 | 
			
		||||
                    randomness: 0
 | 
			
		||||
                };
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 
 | 
			
		||||
@@ -59,8 +59,7 @@ export default function () {
 | 
			
		||||
                object.configuration = {
 | 
			
		||||
                    imageLocation: '',
 | 
			
		||||
                    imageLoadDelayInMilliSeconds: DEFAULT_IMAGE_LOAD_DELAY_IN_MILISECONDS,
 | 
			
		||||
                    imageSamples: [],
 | 
			
		||||
                    layers: []
 | 
			
		||||
                    imageSamples: []
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
                object.telemetry = {
 | 
			
		||||
@@ -91,21 +90,7 @@ export default function () {
 | 
			
		||||
                            format: 'image',
 | 
			
		||||
                            hints: {
 | 
			
		||||
                                image: 1
 | 
			
		||||
                            },
 | 
			
		||||
                            layers: [
 | 
			
		||||
                                {
 | 
			
		||||
                                    source: 'dist/imagery/example-imagery-layer-16x9.png',
 | 
			
		||||
                                    name: '16:9'
 | 
			
		||||
                                },
 | 
			
		||||
                                {
 | 
			
		||||
                                    source: 'dist/imagery/example-imagery-layer-safe.png',
 | 
			
		||||
                                    name: 'Safe'
 | 
			
		||||
                                },
 | 
			
		||||
                                {
 | 
			
		||||
                                    source: 'dist/imagery/example-imagery-layer-scale.png',
 | 
			
		||||
                                    name: 'Scale'
 | 
			
		||||
                                }
 | 
			
		||||
                            ]
 | 
			
		||||
                            }
 | 
			
		||||
                        },
 | 
			
		||||
                        {
 | 
			
		||||
                            name: 'Image Download Name',
 | 
			
		||||
@@ -168,7 +153,7 @@ function getImageUrlListFromConfig(configuration) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getImageLoadDelay(domainObject) {
 | 
			
		||||
    const imageLoadDelay = Math.trunc(Number(domainObject.configuration.imageLoadDelayInMilliSeconds));
 | 
			
		||||
    const imageLoadDelay = domainObject.configuration.imageLoadDelayInMilliSeconds;
 | 
			
		||||
    if (!imageLoadDelay) {
 | 
			
		||||
        openmctInstance.objects.mutate(domainObject, 'configuration.imageLoadDelayInMilliSeconds', DEFAULT_IMAGE_LOAD_DELAY_IN_MILISECONDS);
 | 
			
		||||
 | 
			
		||||
@@ -190,9 +175,7 @@ function getRealtimeProvider() {
 | 
			
		||||
        subscribe: (domainObject, callback) => {
 | 
			
		||||
            const delay = getImageLoadDelay(domainObject);
 | 
			
		||||
            const interval = setInterval(() => {
 | 
			
		||||
                const imageSamples = getImageSamples(domainObject.configuration);
 | 
			
		||||
                const datum = pointForTimestamp(Date.now(), domainObject.name, imageSamples, delay);
 | 
			
		||||
                callback(datum);
 | 
			
		||||
                callback(pointForTimestamp(Date.now(), domainObject.name, getImageSamples(domainObject.configuration), delay));
 | 
			
		||||
            }, delay);
 | 
			
		||||
 | 
			
		||||
            return () => {
 | 
			
		||||
@@ -231,9 +214,8 @@ function getLadProvider() {
 | 
			
		||||
        },
 | 
			
		||||
        request: (domainObject, options) => {
 | 
			
		||||
            const delay = getImageLoadDelay(domainObject);
 | 
			
		||||
            const datum = pointForTimestamp(Date.now(), domainObject.name, getImageSamples(domainObject.configuration), delay);
 | 
			
		||||
 | 
			
		||||
            return Promise.resolve([datum]);
 | 
			
		||||
            return Promise.resolve([pointForTimestamp(Date.now(), domainObject.name, delay)]);
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -75,12 +75,12 @@
 | 
			
		||||
        const TWO_HOURS = ONE_HOUR * 2;
 | 
			
		||||
        const ONE_DAY = ONE_HOUR * 24;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        openmct.install(openmct.plugins.LocalStorage());
 | 
			
		||||
 | 
			
		||||
        openmct.install(openmct.plugins.example.Generator());
 | 
			
		||||
        openmct.install(openmct.plugins.example.EventGeneratorPlugin());
 | 
			
		||||
        openmct.install(openmct.plugins.example.ExampleImagery());
 | 
			
		||||
        openmct.install(openmct.plugins.example.ExampleTags());
 | 
			
		||||
 | 
			
		||||
        openmct.install(openmct.plugins.Espresso());
 | 
			
		||||
        openmct.install(openmct.plugins.MyItems());
 | 
			
		||||
@@ -191,13 +191,11 @@
 | 
			
		||||
        openmct.install(openmct.plugins.ObjectMigration());
 | 
			
		||||
        openmct.install(openmct.plugins.ClearData(
 | 
			
		||||
            ['table', 'telemetry.plot.overlay', 'telemetry.plot.stacked', 'example.imagery'],
 | 
			
		||||
            { indicator: true }
 | 
			
		||||
            {indicator: true}
 | 
			
		||||
        ));
 | 
			
		||||
        openmct.install(openmct.plugins.Clock({ enableClockIndicator: true }));
 | 
			
		||||
        openmct.install(openmct.plugins.Timer());
 | 
			
		||||
        openmct.install(openmct.plugins.Timelist());
 | 
			
		||||
        openmct.install(openmct.plugins.BarChart());
 | 
			
		||||
        openmct.install(openmct.plugins.ScatterPlot());
 | 
			
		||||
        openmct.start();
 | 
			
		||||
    </script>
 | 
			
		||||
</html>
 | 
			
		||||
 
 | 
			
		||||
@@ -74,8 +74,13 @@ module.exports = (config) => {
 | 
			
		||||
        },
 | 
			
		||||
        coverageIstanbulReporter: {
 | 
			
		||||
            fixWebpackSourcePaths: true,
 | 
			
		||||
            dir: "coverage/unit",
 | 
			
		||||
            reports: ['lcovonly']
 | 
			
		||||
            dir: "dist/reports/coverage",
 | 
			
		||||
            reports: ['lcovonly', 'text-summary'],
 | 
			
		||||
            thresholds: {
 | 
			
		||||
                global: {
 | 
			
		||||
                    lines: 52
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        specReporter: {
 | 
			
		||||
            maxLogLines: 5,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										55
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										55
									
								
								package.json
									
									
									
									
									
								
							@@ -1,23 +1,23 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "openmct",
 | 
			
		||||
  "version": "2.0.5",
 | 
			
		||||
  "version": "2.0.4-SNAPSHOT",
 | 
			
		||||
  "description": "The Open MCT core platform",
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@babel/eslint-parser": "7.18.2",
 | 
			
		||||
    "@babel/eslint-parser": "7.16.3",
 | 
			
		||||
    "@braintree/sanitize-url": "6.0.0",
 | 
			
		||||
    "@percy/cli": "1.2.1",
 | 
			
		||||
    "@percy/playwright": "1.0.4",
 | 
			
		||||
    "@playwright/test": "1.23.0",
 | 
			
		||||
    "@percy/cli": "1.0.4",
 | 
			
		||||
    "@percy/playwright": "1.0.2",
 | 
			
		||||
    "@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",
 | 
			
		||||
    "allure-playwright": "2.0.0-beta.15",
 | 
			
		||||
    "babel-loader": "8.2.3",
 | 
			
		||||
    "babel-plugin-istanbul": "6.1.1",
 | 
			
		||||
    "comma-separated-values": "3.6.4",
 | 
			
		||||
    "codecov":"3.8.3",
 | 
			
		||||
    "copy-webpack-plugin": "11.0.0",
 | 
			
		||||
    "copy-webpack-plugin": "10.2.0",
 | 
			
		||||
    "cross-env": "7.0.3",
 | 
			
		||||
    "css-loader": "4.0.0",
 | 
			
		||||
    "d3-axis": "3.0.0",
 | 
			
		||||
@@ -26,20 +26,21 @@
 | 
			
		||||
    "eslint": "8.13.0",
 | 
			
		||||
    "eslint-plugin-compat": "4.0.2",
 | 
			
		||||
    "eslint-plugin-playwright": "0.9.0",
 | 
			
		||||
    "eslint-plugin-vue": "9.1.0",
 | 
			
		||||
    "eslint-plugin-vue": "8.5.0",
 | 
			
		||||
    "eslint-plugin-you-dont-need-lodash-underscore": "6.12.0",
 | 
			
		||||
    "eventemitter3": "1.2.0",
 | 
			
		||||
    "exports-loader": "0.7.0",
 | 
			
		||||
    "express": "4.13.1",
 | 
			
		||||
    "file-saver": "2.0.5",
 | 
			
		||||
    "git-rev-sync": "3.0.2",
 | 
			
		||||
    "html2canvas": "1.4.1",
 | 
			
		||||
    "imports-loader": "0.8.0",
 | 
			
		||||
    "jasmine-core": "4.1.1",
 | 
			
		||||
    "jasmine-core": "4.0.1",
 | 
			
		||||
    "jsdoc": "3.5.5",
 | 
			
		||||
    "karma": "6.3.20",
 | 
			
		||||
    "karma": "6.3.18",
 | 
			
		||||
    "karma-chrome-launcher": "3.1.1",
 | 
			
		||||
    "karma-cli": "2.0.0",
 | 
			
		||||
    "karma-coverage": "2.2.0",
 | 
			
		||||
    "karma-coverage": "2.1.1",
 | 
			
		||||
    "karma-coverage-istanbul-reporter": "3.0.3",
 | 
			
		||||
    "karma-firefox-launcher": "2.1.2",
 | 
			
		||||
    "karma-jasmine": "4.0.1",
 | 
			
		||||
@@ -47,7 +48,7 @@
 | 
			
		||||
    "karma-sourcemap-loader": "0.3.8",
 | 
			
		||||
    "karma-spec-reporter": "0.0.34",
 | 
			
		||||
    "karma-webpack": "5.0.0",
 | 
			
		||||
    "lighthouse": "9.6.1",
 | 
			
		||||
    "lighthouse": "9.5.0",
 | 
			
		||||
    "location-bar": "3.0.1",
 | 
			
		||||
    "lodash": "4.17.21",
 | 
			
		||||
    "mini-css-extract-plugin": "2.6.0",
 | 
			
		||||
@@ -56,33 +57,33 @@
 | 
			
		||||
    "moment-timezone": "0.5.34",
 | 
			
		||||
    "node-bourbon": "4.2.3",
 | 
			
		||||
    "painterro": "1.2.56",
 | 
			
		||||
    "nyc":"15.1.0",
 | 
			
		||||
    "plotly.js-basic-dist": "2.12.0",
 | 
			
		||||
    "plotly.js-gl2d-dist": "2.12.0",
 | 
			
		||||
    "printj": "1.3.1",
 | 
			
		||||
    "request": "2.88.2",
 | 
			
		||||
    "resolve-url-loader": "5.0.0",
 | 
			
		||||
    "sass": "1.52.2",
 | 
			
		||||
    "sass": "1.49.9",
 | 
			
		||||
    "sass-loader": "12.6.0",
 | 
			
		||||
    "sinon": "14.0.0",
 | 
			
		||||
    "sinon": "13.0.1",
 | 
			
		||||
    "style-loader": "^1.0.1",
 | 
			
		||||
    "uuid": "8.3.2",
 | 
			
		||||
    "uuid": "3.3.3",
 | 
			
		||||
    "vue": "2.6.14",
 | 
			
		||||
    "vue-eslint-parser": "9.0.2",
 | 
			
		||||
    "vue-eslint-parser": "8.3.0",
 | 
			
		||||
    "vue-loader": "15.9.8",
 | 
			
		||||
    "vue-template-compiler": "2.6.14",
 | 
			
		||||
    "webpack": "5.68.0",
 | 
			
		||||
    "webpack-cli": "4.9.2",
 | 
			
		||||
    "webpack-dev-middleware": "5.3.3",
 | 
			
		||||
    "webpack-dev-middleware": "5.3.1",
 | 
			
		||||
    "webpack-hot-middleware": "2.25.1",
 | 
			
		||||
    "webpack-merge": "5.8.0"
 | 
			
		||||
    "webpack-merge": "5.8.0",
 | 
			
		||||
    "zepto": "1.2.0"
 | 
			
		||||
  },
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "clean": "rm -rf ./dist ./node_modules ./package-lock.json",
 | 
			
		||||
    "clean-test-lint": "npm run clean; npm install; npm run test; npm run lint",
 | 
			
		||||
    "start": "node app.js",
 | 
			
		||||
    "lint": "eslint example src e2e --ext .js,.vue openmct.js --max-warnings=0",
 | 
			
		||||
    "lint: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",
 | 
			
		||||
@@ -91,23 +92,17 @@
 | 
			
		||||
    "test": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run",
 | 
			
		||||
    "test:firefox": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run --browsers=FirefoxHeadless",
 | 
			
		||||
    "test:debug": "cross-env NODE_ENV=debug karma start --no-single-run",
 | 
			
		||||
    "test:e2e": "npx playwright test",
 | 
			
		||||
    "test:e2e:ci": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome smoke branding default condition timeConductor clock exampleImagery persistence performance grandsearch notebook/tags",
 | 
			
		||||
    "test:e2e:ci": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome smoke default condition timeConductor branding clock",
 | 
			
		||||
    "test:e2e:local": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome",
 | 
			
		||||
    "test:e2e:updatesnapshots": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @snapshot --update-snapshots",
 | 
			
		||||
    "test:e2e:visual": "percy exec --config ./e2e/.percy.yml -- npx playwright test --config=e2e/playwright-visual.config.js",
 | 
			
		||||
    "test:e2e:updatesnapshots": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome --grep @snapshot --update-snapshots",
 | 
			
		||||
    "test:e2e:visual": "percy exec --config ./e2e/.percy.yml -- npx playwright test --config=e2e/playwright-visual.config.js default",
 | 
			
		||||
    "test:e2e:full": "npx playwright test --config=e2e/playwright-ci.config.js",
 | 
			
		||||
    "test:perf": "npx playwright test --config=e2e/playwright-performance.config.js",
 | 
			
		||||
    "test:watch": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --no-single-run",
 | 
			
		||||
    "jsdoc": "jsdoc -c jsdoc.json -R API.md -r -d dist/docs/api",
 | 
			
		||||
    "update-about-dialog-copyright": "perl -pi -e 's/20\\d\\d\\-202\\d/2014\\-2022/gm' ./src/ui/layout/AboutDialog.vue",
 | 
			
		||||
    "update-copyright-date": "npm run update-about-dialog-copyright && grep -lr --null --include=*.{js,scss,vue,ts,sh,html,md,frag} 'Copyright (c) 20' . | xargs -r0 perl -pi -e 's/Copyright\\s\\(c\\)\\s20\\d\\d\\-20\\d\\d/Copyright \\(c\\)\\ 2014\\-2022/gm'",
 | 
			
		||||
    "otherdoc": "node docs/gendocs.js --in docs/src --out dist/docs --suppress-toc 'docs/src/index.md|docs/src/process/index.md'",
 | 
			
		||||
    "docs": "npm run jsdoc ; npm run otherdoc",
 | 
			
		||||
    "cov:e2e:report":"nyc report --reporter=lcovonly --report-dir=./coverage/e2e",
 | 
			
		||||
    "cov:e2e:full:publish":"codecov --disable=gcov -f ./coverage/e2e/lcov.info -F e2e-full",
 | 
			
		||||
    "cov:e2e:ci:publish":"codecov --disable=gcov -f ./coverage/e2e/lcov.info -F e2e-ci",
 | 
			
		||||
    "cov:unit:publish":"codecov --disable=gcov -f ./coverage/unit/lcov.info -F unit",
 | 
			
		||||
    "prepare": "npm run build:prod"
 | 
			
		||||
  },
 | 
			
		||||
  "repository": {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										15
									
								
								src/MCT.js
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								src/MCT.js
									
									
									
									
									
								
							@@ -42,7 +42,6 @@ define([
 | 
			
		||||
    './plugins/duplicate/plugin',
 | 
			
		||||
    './plugins/importFromJSONAction/plugin',
 | 
			
		||||
    './plugins/exportAsJSONAction/plugin',
 | 
			
		||||
    './ui/components/components',
 | 
			
		||||
    'vue'
 | 
			
		||||
], function (
 | 
			
		||||
    EventEmitter,
 | 
			
		||||
@@ -66,7 +65,6 @@ define([
 | 
			
		||||
    DuplicateActionPlugin,
 | 
			
		||||
    ImportFromJSONAction,
 | 
			
		||||
    ExportAsJSONAction,
 | 
			
		||||
    components,
 | 
			
		||||
    Vue
 | 
			
		||||
) {
 | 
			
		||||
    /**
 | 
			
		||||
@@ -238,22 +236,13 @@ define([
 | 
			
		||||
        this.priority = api.PriorityAPI;
 | 
			
		||||
 | 
			
		||||
        this.router = new ApplicationRouter(this);
 | 
			
		||||
        this.faults = new api.FaultManagementAPI.default(this);
 | 
			
		||||
        this.forms = new api.FormsAPI.default(this);
 | 
			
		||||
 | 
			
		||||
        this.branding = BrandingAPI.default;
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * MCT's annotation API that enables
 | 
			
		||||
         * human-created comments and categorization linked to data products
 | 
			
		||||
         * @type {module:openmct.AnnotationAPI}
 | 
			
		||||
         * @memberof module:openmct.MCT#
 | 
			
		||||
         * @name annotation
 | 
			
		||||
         */
 | 
			
		||||
        this.annotation = new api.AnnotationAPI(this);
 | 
			
		||||
 | 
			
		||||
        // 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());
 | 
			
		||||
@@ -281,7 +270,6 @@ define([
 | 
			
		||||
        this.install(this.plugins.ObjectInterceptors());
 | 
			
		||||
        this.install(this.plugins.DeviceClassifier());
 | 
			
		||||
        this.install(this.plugins.UserIndicator());
 | 
			
		||||
        this.install(this.plugins.Gauge());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    MCT.prototype = Object.create(EventEmitter.prototype);
 | 
			
		||||
@@ -390,7 +378,6 @@ define([
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    MCT.prototype.plugins = plugins;
 | 
			
		||||
    MCT.prototype.components = components.default;
 | 
			
		||||
 | 
			
		||||
    return MCT;
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -85,6 +85,8 @@ class ActionCollection extends EventEmitter {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    destroy() {
 | 
			
		||||
        super.removeAllListeners();
 | 
			
		||||
 | 
			
		||||
        if (!this.skipEnvironmentObservers) {
 | 
			
		||||
            this.objectUnsubscribes.forEach(unsubscribe => {
 | 
			
		||||
                unsubscribe();
 | 
			
		||||
@@ -94,7 +96,6 @@ class ActionCollection extends EventEmitter {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.emit('destroy', this.view);
 | 
			
		||||
        this.removeAllListeners();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getVisibleActions() {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,277 +0,0 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2022, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
import { v4 as uuid } from 'uuid';
 | 
			
		||||
import EventEmitter from 'EventEmitter';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @readonly
 | 
			
		||||
 * @enum {String} AnnotationType
 | 
			
		||||
 * @property {String} NOTEBOOK The notebook annotation type
 | 
			
		||||
 * @property {String} GEOSPATIAL The geospatial annotation type
 | 
			
		||||
 * @property {String} PIXEL_SPATIAL The pixel-spatial annotation type
 | 
			
		||||
 * @property {String} TEMPORAL The temporal annotation type
 | 
			
		||||
 * @property {String} PLOT_SPATIAL The plot-spatial annotation type
 | 
			
		||||
 */
 | 
			
		||||
const ANNOTATION_TYPES = Object.freeze({
 | 
			
		||||
    NOTEBOOK: 'NOTEBOOK',
 | 
			
		||||
    GEOSPATIAL: 'GEOSPATIAL',
 | 
			
		||||
    PIXEL_SPATIAL: 'PIXEL_SPATIAL',
 | 
			
		||||
    TEMPORAL: 'TEMPORAL',
 | 
			
		||||
    PLOT_SPATIAL: 'PLOT_SPATIAL'
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @typedef {Object} Tag
 | 
			
		||||
 * @property {String} key a unique identifier for the tag
 | 
			
		||||
 * @property {String} backgroundColor eg. "#cc0000"
 | 
			
		||||
 * @property {String} foregroundColor eg. "#ffffff"
 | 
			
		||||
 */
 | 
			
		||||
export default class AnnotationAPI extends EventEmitter {
 | 
			
		||||
    constructor(openmct) {
 | 
			
		||||
        super();
 | 
			
		||||
        this.openmct = openmct;
 | 
			
		||||
        this.availableTags = {};
 | 
			
		||||
 | 
			
		||||
        this.ANNOTATION_TYPES = ANNOTATION_TYPES;
 | 
			
		||||
 | 
			
		||||
        this.openmct.types.addType('annotation', {
 | 
			
		||||
            name: 'Annotation',
 | 
			
		||||
            description: 'A user created note or comment about time ranges, pixel space, and geospatial features.',
 | 
			
		||||
            creatable: false,
 | 
			
		||||
            cssClass: 'icon-notebook',
 | 
			
		||||
            initialize: function (domainObject) {
 | 
			
		||||
                domainObject.targets = domainObject.targets || {};
 | 
			
		||||
                domainObject.originalContextPath = domainObject.originalContextPath || '';
 | 
			
		||||
                domainObject.tags = domainObject.tags || [];
 | 
			
		||||
                domainObject.contentText = domainObject.contentText || '';
 | 
			
		||||
                domainObject.annotationType = domainObject.annotationType || 'plotspatial';
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
    * Create the a generic annotation
 | 
			
		||||
    * @typedef {Object} CreateAnnotationOptions
 | 
			
		||||
    * @property {String} name a name for the new parameter
 | 
			
		||||
    * @property {import('../objects/ObjectAPI').DomainObject} domainObject the domain object to create
 | 
			
		||||
    * @property {ANNOTATION_TYPES} annotationType the type of annotation to create
 | 
			
		||||
    * @property {Tag[]} tags
 | 
			
		||||
    * @property {String} contentText
 | 
			
		||||
    * @property {import('../objects/ObjectAPI').Identifier[]} targets
 | 
			
		||||
    */
 | 
			
		||||
    /**
 | 
			
		||||
    * @method create
 | 
			
		||||
    * @param {CreateAnnotationOptions} options
 | 
			
		||||
    * @returns {Promise<import('../objects/ObjectAPI').DomainObject>} a promise which will resolve when the domain object
 | 
			
		||||
    *          has been created, or be rejected if it cannot be saved
 | 
			
		||||
    */
 | 
			
		||||
    async create({name, domainObject, annotationType, tags, contentText, targets}) {
 | 
			
		||||
        if (!Object.keys(this.ANNOTATION_TYPES).includes(annotationType)) {
 | 
			
		||||
            throw new Error(`Unknown annotation type: ${annotationType}`);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!Object.keys(targets).length) {
 | 
			
		||||
            throw new Error(`At least one target is required to create an annotation`);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const domainObjectKeyString = this.openmct.objects.makeKeyString(domainObject.identifier);
 | 
			
		||||
        const originalPathObjects = await this.openmct.objects.getOriginalPath(domainObjectKeyString);
 | 
			
		||||
        const originalContextPath = this.openmct.objects.getRelativePath(originalPathObjects);
 | 
			
		||||
        const namespace = domainObject.identifier.namespace;
 | 
			
		||||
        const type = 'annotation';
 | 
			
		||||
        const typeDefinition = this.openmct.types.get(type);
 | 
			
		||||
        const definition = typeDefinition.definition;
 | 
			
		||||
 | 
			
		||||
        const createdObject = {
 | 
			
		||||
            name,
 | 
			
		||||
            type,
 | 
			
		||||
            identifier: {
 | 
			
		||||
                key: uuid(),
 | 
			
		||||
                namespace
 | 
			
		||||
            },
 | 
			
		||||
            tags,
 | 
			
		||||
            annotationType,
 | 
			
		||||
            contentText,
 | 
			
		||||
            originalContextPath
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        if (definition.initialize) {
 | 
			
		||||
            definition.initialize(createdObject);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        createdObject.targets = targets;
 | 
			
		||||
        createdObject.originalContextPath = originalContextPath;
 | 
			
		||||
 | 
			
		||||
        const success = await this.openmct.objects.save(createdObject);
 | 
			
		||||
        if (success) {
 | 
			
		||||
            this.emit('annotationCreated', createdObject);
 | 
			
		||||
 | 
			
		||||
            return createdObject;
 | 
			
		||||
        } else {
 | 
			
		||||
            throw new Error('Failed to create object');
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    defineTag(tagKey, tagsDefinition) {
 | 
			
		||||
        this.availableTags[tagKey] = tagsDefinition;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getAvailableTags() {
 | 
			
		||||
        if (this.availableTags) {
 | 
			
		||||
            const rearrangedToArray = Object.keys(this.availableTags).map(tagKey => {
 | 
			
		||||
                return {
 | 
			
		||||
                    id: tagKey,
 | 
			
		||||
                    ...this.availableTags[tagKey]
 | 
			
		||||
                };
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            return rearrangedToArray;
 | 
			
		||||
        } else {
 | 
			
		||||
            return [];
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getAnnotation(query, searchType) {
 | 
			
		||||
        let foundAnnotation = null;
 | 
			
		||||
 | 
			
		||||
        const searchResults = (await Promise.all(this.openmct.objects.search(query, null, searchType))).flat();
 | 
			
		||||
        if (searchResults) {
 | 
			
		||||
            foundAnnotation = searchResults[0];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return foundAnnotation;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async addAnnotationTag(existingAnnotation, targetDomainObject, targetSpecificDetails, annotationType, tag) {
 | 
			
		||||
        if (!existingAnnotation) {
 | 
			
		||||
            const targets = {};
 | 
			
		||||
            const targetKeyString = this.openmct.objects.makeKeyString(targetDomainObject.identifier);
 | 
			
		||||
            targets[targetKeyString] = targetSpecificDetails;
 | 
			
		||||
            const contentText = `${annotationType} tag`;
 | 
			
		||||
            const annotationCreationArguments = {
 | 
			
		||||
                name: contentText,
 | 
			
		||||
                domainObject: targetDomainObject,
 | 
			
		||||
                annotationType,
 | 
			
		||||
                tags: [tag],
 | 
			
		||||
                contentText,
 | 
			
		||||
                targets
 | 
			
		||||
            };
 | 
			
		||||
            const newAnnotation = await this.create(annotationCreationArguments);
 | 
			
		||||
 | 
			
		||||
            return newAnnotation;
 | 
			
		||||
        } else {
 | 
			
		||||
            const tagArray = [tag, ...existingAnnotation.tags];
 | 
			
		||||
            this.openmct.objects.mutate(existingAnnotation, 'tags', tagArray);
 | 
			
		||||
 | 
			
		||||
            return existingAnnotation;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    removeAnnotationTag(existingAnnotation, tagToRemove) {
 | 
			
		||||
        if (existingAnnotation && existingAnnotation.tags.includes(tagToRemove)) {
 | 
			
		||||
            const cleanedArray = existingAnnotation.tags.filter(extantTag => extantTag !== tagToRemove);
 | 
			
		||||
            this.openmct.objects.mutate(existingAnnotation, 'tags', cleanedArray);
 | 
			
		||||
        } else {
 | 
			
		||||
            throw new Error(`Asked to remove tag (${tagToRemove}) that doesn't exist`, existingAnnotation);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    removeAnnotationTags(existingAnnotation) {
 | 
			
		||||
        // just removes tags on the annotation as we can't really delete objects
 | 
			
		||||
        if (existingAnnotation && existingAnnotation.tags) {
 | 
			
		||||
            this.openmct.objects.mutate(existingAnnotation, 'tags', []);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #getMatchingTags(query) {
 | 
			
		||||
        if (!query) {
 | 
			
		||||
            return [];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const matchingTags = Object.keys(this.availableTags).filter(tagKey => {
 | 
			
		||||
            if (this.availableTags[tagKey] && this.availableTags[tagKey].label) {
 | 
			
		||||
                return this.availableTags[tagKey].label.toLowerCase().includes(query.toLowerCase());
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return false;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return matchingTags;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #addTagMetaInformationToResults(results, matchingTagKeys) {
 | 
			
		||||
        const tagsAddedToResults = results.map(result => {
 | 
			
		||||
            const fullTagModels = result.tags.map(tagKey => {
 | 
			
		||||
                const tagModel = this.availableTags[tagKey];
 | 
			
		||||
                tagModel.tagID = tagKey;
 | 
			
		||||
 | 
			
		||||
                return tagModel;
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            return {
 | 
			
		||||
                fullTagModels,
 | 
			
		||||
                matchingTagKeys,
 | 
			
		||||
                ...result
 | 
			
		||||
            };
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return tagsAddedToResults;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async #addTargetModelsToResults(results) {
 | 
			
		||||
        const modelAddedToResults = await Promise.all(results.map(async result => {
 | 
			
		||||
            const targetModels = await Promise.all(Object.keys(result.targets).map(async (targetID) => {
 | 
			
		||||
                const targetModel = await this.openmct.objects.get(targetID);
 | 
			
		||||
                const targetKeyString = this.openmct.objects.makeKeyString(targetModel.identifier);
 | 
			
		||||
                const originalPathObjects = await this.openmct.objects.getOriginalPath(targetKeyString);
 | 
			
		||||
 | 
			
		||||
                return {
 | 
			
		||||
                    originalPath: originalPathObjects,
 | 
			
		||||
                    ...targetModel
 | 
			
		||||
                };
 | 
			
		||||
            }));
 | 
			
		||||
 | 
			
		||||
            return {
 | 
			
		||||
                targetModels,
 | 
			
		||||
                ...result
 | 
			
		||||
            };
 | 
			
		||||
        }));
 | 
			
		||||
 | 
			
		||||
        return modelAddedToResults;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
    * @method searchForTags
 | 
			
		||||
    * @param {String} query A query to match against tags. E.g., "dr" will match the tags "drilling" and "driving"
 | 
			
		||||
    * @param {Object} abortController An optional abort method to stop the query
 | 
			
		||||
    * @returns {Promise} returns a model of matching tags with their target domain objects attached
 | 
			
		||||
    */
 | 
			
		||||
    async searchForTags(query, abortController) {
 | 
			
		||||
        const matchingTagKeys = this.#getMatchingTags(query);
 | 
			
		||||
        const searchResults = (await Promise.all(this.openmct.objects.search(matchingTagKeys, abortController, this.openmct.objects.SEARCH_TYPES.TAGS))).flat();
 | 
			
		||||
        const appliedTagSearchResults = this.#addTagMetaInformationToResults(searchResults, matchingTagKeys);
 | 
			
		||||
        const appliedTargetsModels = await this.#addTargetModelsToResults(appliedTagSearchResults);
 | 
			
		||||
 | 
			
		||||
        return appliedTargetsModels;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,176 +0,0 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2022, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
import { createOpenMct, resetApplicationState } from '../../utils/testing';
 | 
			
		||||
import ExampleTagsPlugin from "../../../example/exampleTags/plugin";
 | 
			
		||||
 | 
			
		||||
describe("The Annotation API", () => {
 | 
			
		||||
    let openmct;
 | 
			
		||||
    let mockObjectProvider;
 | 
			
		||||
    let mockDomainObject;
 | 
			
		||||
    let mockAnnotationObject;
 | 
			
		||||
 | 
			
		||||
    beforeEach((done) => {
 | 
			
		||||
        openmct = createOpenMct();
 | 
			
		||||
        openmct.install(new ExampleTagsPlugin());
 | 
			
		||||
        const availableTags = openmct.annotation.getAvailableTags();
 | 
			
		||||
        mockDomainObject = {
 | 
			
		||||
            type: 'notebook',
 | 
			
		||||
            name: 'fooRabbitNotebook',
 | 
			
		||||
            identifier: {
 | 
			
		||||
                key: 'some-object',
 | 
			
		||||
                namespace: 'fooNameSpace'
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
        mockAnnotationObject = {
 | 
			
		||||
            type: 'annotation',
 | 
			
		||||
            name: 'Some Notebook Annotation',
 | 
			
		||||
            annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,
 | 
			
		||||
            tags: [availableTags[0].id, availableTags[1].id],
 | 
			
		||||
            identifier: {
 | 
			
		||||
                key: 'anAnnotationKey',
 | 
			
		||||
                namespace: 'fooNameSpace'
 | 
			
		||||
            },
 | 
			
		||||
            targets: {
 | 
			
		||||
                'fooNameSpace:some-object': {
 | 
			
		||||
                    entryId: 'fooBarEntry'
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        mockObjectProvider = jasmine.createSpyObj("mock provider", [
 | 
			
		||||
            "create",
 | 
			
		||||
            "update",
 | 
			
		||||
            "get"
 | 
			
		||||
        ]);
 | 
			
		||||
        // eslint-disable-next-line require-await
 | 
			
		||||
        mockObjectProvider.get = async (identifier) => {
 | 
			
		||||
            if (identifier.key === mockDomainObject.identifier.key) {
 | 
			
		||||
                return mockDomainObject;
 | 
			
		||||
            } else if (identifier.key === mockAnnotationObject.identifier.key) {
 | 
			
		||||
                return mockAnnotationObject;
 | 
			
		||||
            } else {
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        mockObjectProvider.create.and.returnValue(Promise.resolve(true));
 | 
			
		||||
        mockObjectProvider.update.and.returnValue(Promise.resolve(true));
 | 
			
		||||
 | 
			
		||||
        openmct.objects.addProvider('fooNameSpace', mockObjectProvider);
 | 
			
		||||
        openmct.on('start', done);
 | 
			
		||||
        openmct.startHeadless();
 | 
			
		||||
    });
 | 
			
		||||
    afterEach(async () => {
 | 
			
		||||
        openmct.objects.providers = {};
 | 
			
		||||
        await resetApplicationState(openmct);
 | 
			
		||||
    });
 | 
			
		||||
    it("is defined", () => {
 | 
			
		||||
        expect(openmct.annotation).toBeDefined();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe("Creation", () => {
 | 
			
		||||
        it("can create annotations", async () => {
 | 
			
		||||
            const annotationCreationArguments = {
 | 
			
		||||
                name: 'Test Annotation',
 | 
			
		||||
                domainObject: mockDomainObject,
 | 
			
		||||
                annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,
 | 
			
		||||
                tags: ['sometag'],
 | 
			
		||||
                contentText: "fooContext",
 | 
			
		||||
                targets: {'fooTarget': {}}
 | 
			
		||||
            };
 | 
			
		||||
            const annotationObject = await openmct.annotation.create(annotationCreationArguments);
 | 
			
		||||
            expect(annotationObject).toBeDefined();
 | 
			
		||||
            expect(annotationObject.type).toEqual('annotation');
 | 
			
		||||
        });
 | 
			
		||||
        it("fails if annotation is an unknown type", async () => {
 | 
			
		||||
            try {
 | 
			
		||||
                await openmct.annotation.create('Garbage Annotation', mockDomainObject, 'garbageAnnotation', ['sometag'], "fooContext", {'fooTarget': {}});
 | 
			
		||||
            } catch (error) {
 | 
			
		||||
                expect(error).toBeDefined();
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe("Tagging", () => {
 | 
			
		||||
        it("can create a tag", async () => {
 | 
			
		||||
            const annotationObject = await openmct.annotation.addAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
 | 
			
		||||
            expect(annotationObject).toBeDefined();
 | 
			
		||||
            expect(annotationObject.type).toEqual('annotation');
 | 
			
		||||
            expect(annotationObject.tags).toContain('aWonderfulTag');
 | 
			
		||||
        });
 | 
			
		||||
        it("can delete a tag", async () => {
 | 
			
		||||
            const originalAnnotationObject = await openmct.annotation.addAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
 | 
			
		||||
            const annotationObject = await openmct.annotation.addAnnotationTag(originalAnnotationObject, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'anotherTagToRemove');
 | 
			
		||||
            expect(annotationObject).toBeDefined();
 | 
			
		||||
            openmct.annotation.removeAnnotationTag(annotationObject, 'anotherTagToRemove');
 | 
			
		||||
            expect(annotationObject.tags).toEqual(['aWonderfulTag']);
 | 
			
		||||
            openmct.annotation.removeAnnotationTag(annotationObject, 'aWonderfulTag');
 | 
			
		||||
            expect(annotationObject.tags).toEqual([]);
 | 
			
		||||
        });
 | 
			
		||||
        it("throws an error if deleting non-existent tag", async () => {
 | 
			
		||||
            const annotationObject = await openmct.annotation.addAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
 | 
			
		||||
            expect(annotationObject).toBeDefined();
 | 
			
		||||
            expect(() => {
 | 
			
		||||
                openmct.annotation.removeAnnotationTag(annotationObject, 'ThisTagShouldNotExist');
 | 
			
		||||
            }).toThrow();
 | 
			
		||||
        });
 | 
			
		||||
        it("can remove all tags", async () => {
 | 
			
		||||
            const annotationObject = await openmct.annotation.addAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
 | 
			
		||||
            expect(annotationObject).toBeDefined();
 | 
			
		||||
            expect(() => {
 | 
			
		||||
                openmct.annotation.removeAnnotationTags(annotationObject);
 | 
			
		||||
            }).not.toThrow();
 | 
			
		||||
            expect(annotationObject.tags).toEqual([]);
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe("Search", () => {
 | 
			
		||||
        let sharedWorkerToRestore;
 | 
			
		||||
        beforeEach(async () => {
 | 
			
		||||
            // use local worker
 | 
			
		||||
            sharedWorkerToRestore = openmct.objects.inMemorySearchProvider.worker;
 | 
			
		||||
            openmct.objects.inMemorySearchProvider.worker = null;
 | 
			
		||||
            await openmct.objects.inMemorySearchProvider.index(mockDomainObject);
 | 
			
		||||
            await openmct.objects.inMemorySearchProvider.index(mockAnnotationObject);
 | 
			
		||||
        });
 | 
			
		||||
        afterEach(() => {
 | 
			
		||||
            openmct.objects.inMemorySearchProvider.worker = sharedWorkerToRestore;
 | 
			
		||||
        });
 | 
			
		||||
        it("can search for tags", async () => {
 | 
			
		||||
            const results = await openmct.annotation.searchForTags('S');
 | 
			
		||||
            expect(results).toBeDefined();
 | 
			
		||||
            expect(results.length).toEqual(1);
 | 
			
		||||
        });
 | 
			
		||||
        it("can get notebook annotations", async () => {
 | 
			
		||||
            const targetKeyString = openmct.objects.makeKeyString(mockDomainObject.identifier);
 | 
			
		||||
            const query = {
 | 
			
		||||
                targetKeyString,
 | 
			
		||||
                entryId: 'fooBarEntry'
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            const results = await openmct.annotation.getAnnotation(query, openmct.objects.SEARCH_TYPES.NOTEBOOK_ANNOTATIONS);
 | 
			
		||||
            expect(results).toBeDefined();
 | 
			
		||||
            expect(results.tags.length).toEqual(2);
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@@ -24,7 +24,6 @@ define([
 | 
			
		||||
    './actions/ActionsAPI',
 | 
			
		||||
    './composition/CompositionAPI',
 | 
			
		||||
    './Editor',
 | 
			
		||||
    './faultmanagement/FaultManagementAPI',
 | 
			
		||||
    './forms/FormsAPI',
 | 
			
		||||
    './indicators/IndicatorAPI',
 | 
			
		||||
    './menu/MenuAPI',
 | 
			
		||||
@@ -35,13 +34,11 @@ define([
 | 
			
		||||
    './telemetry/TelemetryAPI',
 | 
			
		||||
    './time/TimeAPI',
 | 
			
		||||
    './types/TypeRegistry',
 | 
			
		||||
    './user/UserAPI',
 | 
			
		||||
    './annotation/AnnotationAPI'
 | 
			
		||||
    './user/UserAPI'
 | 
			
		||||
], function (
 | 
			
		||||
    ActionsAPI,
 | 
			
		||||
    CompositionAPI,
 | 
			
		||||
    EditorAPI,
 | 
			
		||||
    FaultManagementAPI,
 | 
			
		||||
    FormsAPI,
 | 
			
		||||
    IndicatorAPI,
 | 
			
		||||
    MenuAPI,
 | 
			
		||||
@@ -52,16 +49,14 @@ define([
 | 
			
		||||
    TelemetryAPI,
 | 
			
		||||
    TimeAPI,
 | 
			
		||||
    TypeRegistry,
 | 
			
		||||
    UserAPI,
 | 
			
		||||
    AnnotationAPI
 | 
			
		||||
    UserAPI
 | 
			
		||||
) {
 | 
			
		||||
    return {
 | 
			
		||||
        ActionsAPI: ActionsAPI.default,
 | 
			
		||||
        CompositionAPI: CompositionAPI,
 | 
			
		||||
        EditorAPI: EditorAPI,
 | 
			
		||||
        FaultManagementAPI: FaultManagementAPI,
 | 
			
		||||
        FormsAPI: FormsAPI,
 | 
			
		||||
        IndicatorAPI: IndicatorAPI.default,
 | 
			
		||||
        IndicatorAPI: IndicatorAPI,
 | 
			
		||||
        MenuAPI: MenuAPI.default,
 | 
			
		||||
        NotificationAPI: NotificationAPI.default,
 | 
			
		||||
        ObjectAPI: ObjectAPI,
 | 
			
		||||
@@ -70,7 +65,6 @@ define([
 | 
			
		||||
        TelemetryAPI: TelemetryAPI,
 | 
			
		||||
        TimeAPI: TimeAPI.default,
 | 
			
		||||
        TypeRegistry: TypeRegistry,
 | 
			
		||||
        UserAPI: UserAPI.default,
 | 
			
		||||
        AnnotationAPI: AnnotationAPI.default
 | 
			
		||||
        UserAPI: UserAPI.default
 | 
			
		||||
    };
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,106 +0,0 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2022, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * "License"); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
export default class FaultManagementAPI {
 | 
			
		||||
    constructor(openmct) {
 | 
			
		||||
        this.openmct = openmct;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    addProvider(provider) {
 | 
			
		||||
        this.provider = provider;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    supportsActions() {
 | 
			
		||||
        return this.provider?.acknowledgeFault !== undefined && this.provider?.shelveFault !== undefined;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    request(domainObject) {
 | 
			
		||||
        if (!this.provider?.supportsRequest(domainObject)) {
 | 
			
		||||
            return Promise.reject();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return this.provider.request(domainObject);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    subscribe(domainObject, callback) {
 | 
			
		||||
        if (!this.provider?.supportsSubscribe(domainObject)) {
 | 
			
		||||
            return Promise.reject();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return this.provider.subscribe(domainObject, callback);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    acknowledgeFault(fault, ackData) {
 | 
			
		||||
        return this.provider.acknowledgeFault(fault, ackData);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    shelveFault(fault, shelveData) {
 | 
			
		||||
        return this.provider.shelveFault(fault, shelveData);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** @typedef {object} Fault
 | 
			
		||||
 * @property {string} type
 | 
			
		||||
 * @property {object} fault
 | 
			
		||||
 * @property {boolean} fault.acknowledged
 | 
			
		||||
 * @property {object} fault.currentValueInfo
 | 
			
		||||
 * @property {number} fault.currentValueInfo.value
 | 
			
		||||
 * @property {string} fault.currentValueInfo.rangeCondition
 | 
			
		||||
 * @property {string} fault.currentValueInfo.monitoringResult
 | 
			
		||||
 * @property {string} fault.id
 | 
			
		||||
 * @property {string} fault.name
 | 
			
		||||
 * @property {string} fault.namespace
 | 
			
		||||
 * @property {number} fault.seqNum
 | 
			
		||||
 * @property {string} fault.severity
 | 
			
		||||
 * @property {boolean} fault.shelved
 | 
			
		||||
 * @property {string} fault.shortDescription
 | 
			
		||||
 * @property {string} fault.triggerTime
 | 
			
		||||
 * @property {object} fault.triggerValueInfo
 | 
			
		||||
 * @property {number} fault.triggerValueInfo.value
 | 
			
		||||
 * @property {string} fault.triggerValueInfo.rangeCondition
 | 
			
		||||
 * @property {string} fault.triggerValueInfo.monitoringResult
 | 
			
		||||
 * @example
 | 
			
		||||
 *  {
 | 
			
		||||
 *     "type": "",
 | 
			
		||||
 *     "fault": {
 | 
			
		||||
 *         "acknowledged": true,
 | 
			
		||||
 *         "currentValueInfo": {
 | 
			
		||||
 *             "value": 0,
 | 
			
		||||
 *             "rangeCondition": "",
 | 
			
		||||
 *             "monitoringResult": ""
 | 
			
		||||
 *         },
 | 
			
		||||
 *         "id": "",
 | 
			
		||||
 *         "name": "",
 | 
			
		||||
 *         "namespace": "",
 | 
			
		||||
 *         "seqNum": 0,
 | 
			
		||||
 *         "severity": "",
 | 
			
		||||
 *         "shelved": true,
 | 
			
		||||
 *         "shortDescription": "",
 | 
			
		||||
 *         "triggerTime": "",
 | 
			
		||||
 *         "triggerValueInfo": {
 | 
			
		||||
 *             "value": 0,
 | 
			
		||||
 *             "rangeCondition": "",
 | 
			
		||||
 *             "monitoringResult": ""
 | 
			
		||||
 *         }
 | 
			
		||||
 *     }
 | 
			
		||||
 * }
 | 
			
		||||
 */
 | 
			
		||||
@@ -1,144 +0,0 @@
 | 
			
		||||
/*****************************************************************************
 | 
			
		||||
 * Open MCT, Copyright (c) 2014-2022, United States Government
 | 
			
		||||
 * as represented by the Administrator of the National Aeronautics and Space
 | 
			
		||||
 * Administration. All rights reserved.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT is licensed under the Apache License, Version 2.0 (the
 | 
			
		||||
 * License); you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0.
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an AS IS BASIS, WITHOUT
 | 
			
		||||
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
 * License for the specific language governing permissions and limitations
 | 
			
		||||
 * under the License.
 | 
			
		||||
 *
 | 
			
		||||
 * Open MCT includes source code licensed under additional open source
 | 
			
		||||
 * licenses. See the Open Source Licenses file (LICENSES.md) included with
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
    createOpenMct,
 | 
			
		||||
    resetApplicationState
 | 
			
		||||
} from '../../utils/testing';
 | 
			
		||||
 | 
			
		||||
const faultName = 'super duper fault';
 | 
			
		||||
const aFault = {
 | 
			
		||||
    type: '',
 | 
			
		||||
    fault: {
 | 
			
		||||
        acknowledged: true,
 | 
			
		||||
        currentValueInfo: {
 | 
			
		||||
            value: 0,
 | 
			
		||||
            rangeCondition: '',
 | 
			
		||||
            monitoringResult: ''
 | 
			
		||||
        },
 | 
			
		||||
        id: '',
 | 
			
		||||
        name: faultName,
 | 
			
		||||
        namespace: '',
 | 
			
		||||
        seqNum: 0,
 | 
			
		||||
        severity: '',
 | 
			
		||||
        shelved: true,
 | 
			
		||||
        shortDescription: '',
 | 
			
		||||
        triggerTime: '',
 | 
			
		||||
        triggerValueInfo: {
 | 
			
		||||
            value: 0,
 | 
			
		||||
            rangeCondition: '',
 | 
			
		||||
            monitoringResult: ''
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
const faultDomainObject = {
 | 
			
		||||
    name: 'it is not your fault',
 | 
			
		||||
    type: 'faultManagement',
 | 
			
		||||
    identifier: {
 | 
			
		||||
        key: 'nobodies',
 | 
			
		||||
        namespace: 'fault'
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
const aComment = 'THIS is my fault.';
 | 
			
		||||
const faultManagementProvider = {
 | 
			
		||||
    request() {
 | 
			
		||||
        return Promise.resolve([aFault]);
 | 
			
		||||
    },
 | 
			
		||||
    subscribe(domainObject, callback) {
 | 
			
		||||
        return () => {};
 | 
			
		||||
    },
 | 
			
		||||
    supportsRequest(domainObject) {
 | 
			
		||||
        return domainObject.type === 'faultManagement';
 | 
			
		||||
    },
 | 
			
		||||
    supportsSubscribe(domainObject) {
 | 
			
		||||
        return domainObject.type === 'faultManagement';
 | 
			
		||||
    },
 | 
			
		||||
    acknowledgeFault(fault, { comment = '' }) {
 | 
			
		||||
        return Promise.resolve({
 | 
			
		||||
            success: true
 | 
			
		||||
        });
 | 
			
		||||
    },
 | 
			
		||||
    shelveFault(fault, shelveData) {
 | 
			
		||||
        return Promise.resolve({
 | 
			
		||||
            success: true
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
describe('The Fault Management API', () => {
 | 
			
		||||
    let openmct;
 | 
			
		||||
 | 
			
		||||
    beforeEach(() => {
 | 
			
		||||
        openmct = createOpenMct();
 | 
			
		||||
        openmct.install(openmct.plugins.FaultManagement());
 | 
			
		||||
        // openmct.install(openmct.plugins.example.ExampleFaultSource());
 | 
			
		||||
        openmct.faults.addProvider(faultManagementProvider);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(() => {
 | 
			
		||||
        return resetApplicationState(openmct);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('allows you to request a fault', async () => {
 | 
			
		||||
        spyOn(faultManagementProvider, 'supportsRequest').and.callThrough();
 | 
			
		||||
 | 
			
		||||
        let faultResponse = await openmct.faults.request(faultDomainObject);
 | 
			
		||||
 | 
			
		||||
        expect(faultManagementProvider.supportsRequest).toHaveBeenCalledWith(faultDomainObject);
 | 
			
		||||
        expect(faultResponse[0].fault.name).toEqual(faultName);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('allows you to subscribe to a fault', () => {
 | 
			
		||||
        spyOn(faultManagementProvider, 'subscribe').and.callThrough();
 | 
			
		||||
        spyOn(faultManagementProvider, 'supportsSubscribe').and.callThrough();
 | 
			
		||||
 | 
			
		||||
        let unsubscribe = openmct.faults.subscribe(faultDomainObject, () => {});
 | 
			
		||||
 | 
			
		||||
        expect(unsubscribe).toEqual(jasmine.any(Function));
 | 
			
		||||
        expect(faultManagementProvider.supportsSubscribe).toHaveBeenCalledWith(faultDomainObject);
 | 
			
		||||
        expect(faultManagementProvider.subscribe).toHaveBeenCalledOnceWith(faultDomainObject, jasmine.any(Function));
 | 
			
		||||
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('will tell you if the fault management provider supports actions', () => {
 | 
			
		||||
        expect(openmct.faults.supportsActions()).toBeTrue();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('will allow you to acknowledge a fault', async () => {
 | 
			
		||||
        spyOn(faultManagementProvider, 'acknowledgeFault').and.callThrough();
 | 
			
		||||
 | 
			
		||||
        let ackResponse = await openmct.faults.acknowledgeFault(aFault, aComment);
 | 
			
		||||
 | 
			
		||||
        expect(faultManagementProvider.acknowledgeFault).toHaveBeenCalledWith(aFault, aComment);
 | 
			
		||||
        expect(ackResponse.success).toBeTrue();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('will allow you to shelve a fault', async () => {
 | 
			
		||||
        spyOn(faultManagementProvider, 'shelveFault').and.callThrough();
 | 
			
		||||
 | 
			
		||||
        let shelveResponse = await openmct.faults.shelveFault(aFault, aComment);
 | 
			
		||||
 | 
			
		||||
        expect(faultManagementProvider.shelveFault).toHaveBeenCalledWith(aFault, aComment);
 | 
			
		||||
        expect(shelveResponse.success).toBeTrue();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
@@ -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,9 +21,9 @@
 | 
			
		||||
*****************************************************************************/
 | 
			
		||||
 | 
			
		||||
<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
 | 
			
		||||
@@ -44,14 +44,18 @@
 | 
			
		||||
            >
 | 
			
		||||
                {{ section.name }}
 | 
			
		||||
            </h2>
 | 
			
		||||
            <FormRow
 | 
			
		||||
            <div
 | 
			
		||||
                v-for="(row, index) in section.rows"
 | 
			
		||||
                :key="row.id"
 | 
			
		||||
                :css-class="row.cssClass"
 | 
			
		||||
                :first="index < 1"
 | 
			
		||||
                :row="row"
 | 
			
		||||
                @onChange="onChange"
 | 
			
		||||
            />
 | 
			
		||||
                class="u-contents"
 | 
			
		||||
            >
 | 
			
		||||
                <FormRow
 | 
			
		||||
                    :css-class="section.cssClass"
 | 
			
		||||
                    :first="index < 1"
 | 
			
		||||
                    :row="row"
 | 
			
		||||
                    @onChange="onChange"
 | 
			
		||||
                />
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </form>
 | 
			
		||||
 | 
			
		||||
@@ -60,15 +64,13 @@
 | 
			
		||||
            tabindex="0"
 | 
			
		||||
            :disabled="isInvalid"
 | 
			
		||||
            class="c-button c-button--major"
 | 
			
		||||
            aria-label="Save"
 | 
			
		||||
            @click="onSave"
 | 
			
		||||
        >
 | 
			
		||||
            {{ submitLabel }}
 | 
			
		||||
        </button>
 | 
			
		||||
        <button
 | 
			
		||||
            tabindex="0"
 | 
			
		||||
            class="c-button js-cancel-button"
 | 
			
		||||
            aria-label="Cancel"
 | 
			
		||||
            class="c-button"
 | 
			
		||||
            @click="onDismiss"
 | 
			
		||||
        >
 | 
			
		||||
            {{ cancelLabel }}
 | 
			
		||||
@@ -79,7 +81,7 @@
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import FormRow from "@/api/forms/components/FormRow.vue";
 | 
			
		||||
import { v4 as uuid } from 'uuid';
 | 
			
		||||
import uuid from 'uuid';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    components: {
 | 
			
		||||
 
 | 
			
		||||
@@ -23,10 +23,7 @@
 | 
			
		||||
<template>
 | 
			
		||||
<div
 | 
			
		||||
    class="form-row c-form__row"
 | 
			
		||||
    :class="[
 | 
			
		||||
        { 'first': first },
 | 
			
		||||
        cssClass
 | 
			
		||||
    ]"
 | 
			
		||||
    :class="[{ 'first': first }]"
 | 
			
		||||
    @onChange="onChange"
 | 
			
		||||
>
 | 
			
		||||
    <div
 | 
			
		||||
@@ -37,7 +34,7 @@
 | 
			
		||||
    </div>
 | 
			
		||||
    <div
 | 
			
		||||
        class="c-form-row__state-indicator"
 | 
			
		||||
        :class="reqClass"
 | 
			
		||||
        :class="rowClass"
 | 
			
		||||
    >
 | 
			
		||||
    </div>
 | 
			
		||||
    <div
 | 
			
		||||
@@ -79,22 +76,24 @@ export default {
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
    computed: {
 | 
			
		||||
        reqClass() {
 | 
			
		||||
            let reqClass = 'req';
 | 
			
		||||
        rowClass() {
 | 
			
		||||
            let cssClass = this.cssClass;
 | 
			
		||||
 | 
			
		||||
            if (!this.row.required) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            cssClass = `${cssClass} req`;
 | 
			
		||||
 | 
			
		||||
            if (this.visited && this.valid !== undefined) {
 | 
			
		||||
                if (this.valid === true) {
 | 
			
		||||
                    reqClass = 'valid';
 | 
			
		||||
                    cssClass = `${cssClass} valid`;
 | 
			
		||||
                } else {
 | 
			
		||||
                    reqClass = 'invalid';
 | 
			
		||||
                    cssClass = `${cssClass} invalid`;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return reqClass;
 | 
			
		||||
            return cssClass;
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    mounted() {
 | 
			
		||||
 
 | 
			
		||||
@@ -19,47 +19,35 @@
 | 
			
		||||
* this source code distribution or the Licensing information page available
 | 
			
		||||
* at runtime from the About dialog for additional information.
 | 
			
		||||
*****************************************************************************/
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
<div
 | 
			
		||||
    ref="autoCompleteForm"
 | 
			
		||||
    class="form-control c-input--autocomplete js-autocomplete"
 | 
			
		||||
>
 | 
			
		||||
    <div
 | 
			
		||||
        class="c-input--autocomplete__wrapper"
 | 
			
		||||
    >
 | 
			
		||||
<div class="form-control autocomplete">
 | 
			
		||||
    <span class="autocompleteInputAndArrow">
 | 
			
		||||
        <input
 | 
			
		||||
            ref="autoCompleteInput"
 | 
			
		||||
            v-model="field"
 | 
			
		||||
            class="c-input--autocomplete__input js-autocomplete__input"
 | 
			
		||||
            class="autocompleteInput"
 | 
			
		||||
            type="text"
 | 
			
		||||
            :placeholder="placeHolderText"
 | 
			
		||||
            @click="inputClicked()"
 | 
			
		||||
            @keydown="keyDown($event)"
 | 
			
		||||
        >
 | 
			
		||||
        <div
 | 
			
		||||
            class="icon-arrow-down c-icon-button c-input--autocomplete__afford-arrow js-autocomplete__afford-arrow"
 | 
			
		||||
        <span
 | 
			
		||||
            class="icon-arrow-down"
 | 
			
		||||
            @click="arrowClicked()"
 | 
			
		||||
        ></div>
 | 
			
		||||
    </div>
 | 
			
		||||
        ></span>
 | 
			
		||||
    </span>
 | 
			
		||||
    <div
 | 
			
		||||
        v-if="!hideOptions"
 | 
			
		||||
        class="c-menu c-input--autocomplete__options"
 | 
			
		||||
        aria-label="Autocomplete Options"
 | 
			
		||||
        class="autocompleteOptions"
 | 
			
		||||
        @blur="hideOptions = true"
 | 
			
		||||
    >
 | 
			
		||||
        <ul>
 | 
			
		||||
        <ul v-if="!hideOptions">
 | 
			
		||||
            <li
 | 
			
		||||
                v-for="opt in filteredOptions"
 | 
			
		||||
                :key="opt.optionId"
 | 
			
		||||
                :class="[
 | 
			
		||||
                    {'optionPreSelected': optionIndex === opt.optionId},
 | 
			
		||||
                    itemCssClass
 | 
			
		||||
                ]"
 | 
			
		||||
                :style="itemStyle(opt)"
 | 
			
		||||
                :class="{'optionPreSelected': optionIndex === opt.optionId}"
 | 
			
		||||
                @click="fillInputWithString(opt.name)"
 | 
			
		||||
                @mouseover="optionMouseover(opt.optionId)"
 | 
			
		||||
            >
 | 
			
		||||
                {{ opt.name }}
 | 
			
		||||
                <span class="optionText">{{ opt.name }}</span>
 | 
			
		||||
            </li>
 | 
			
		||||
        </ul>
 | 
			
		||||
    </div>
 | 
			
		||||
@@ -77,23 +65,7 @@ export default {
 | 
			
		||||
    props: {
 | 
			
		||||
        model: {
 | 
			
		||||
            type: Object,
 | 
			
		||||
            required: true,
 | 
			
		||||
            default() {
 | 
			
		||||
                return {};
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        placeHolderText: {
 | 
			
		||||
            type: String,
 | 
			
		||||
            default() {
 | 
			
		||||
                return "";
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        itemCssClass: {
 | 
			
		||||
            type: String,
 | 
			
		||||
            required: false,
 | 
			
		||||
            default() {
 | 
			
		||||
                return "";
 | 
			
		||||
            }
 | 
			
		||||
            required: true
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    data() {
 | 
			
		||||
@@ -106,40 +78,31 @@ export default {
 | 
			
		||||
    },
 | 
			
		||||
    computed: {
 | 
			
		||||
        filteredOptions() {
 | 
			
		||||
            const fullOptions = this.options || [];
 | 
			
		||||
            const options = this.optionNames || [];
 | 
			
		||||
            if (this.showFilteredOptions) {
 | 
			
		||||
                const optionsFiltered = fullOptions
 | 
			
		||||
                return options
 | 
			
		||||
                    .filter(option => {
 | 
			
		||||
                        if (option.name && this.field) {
 | 
			
		||||
                            return option.name.toLowerCase().indexOf(this.field.toLowerCase()) >= 0;
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        return false;
 | 
			
		||||
                        return option.toLowerCase().indexOf(this.field.toLowerCase()) >= 0;
 | 
			
		||||
                    }).map((option, index) => {
 | 
			
		||||
                        return {
 | 
			
		||||
                            optionId: index,
 | 
			
		||||
                            name: option.name,
 | 
			
		||||
                            color: option.color
 | 
			
		||||
                            name: option
 | 
			
		||||
                        };
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
                return optionsFiltered;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const optionsFiltered = fullOptions.map((option, index) => {
 | 
			
		||||
            return options.map((option, index) => {
 | 
			
		||||
                return {
 | 
			
		||||
                    optionId: index,
 | 
			
		||||
                    name: option.name,
 | 
			
		||||
                    color: option.color
 | 
			
		||||
                    name: option
 | 
			
		||||
                };
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            return optionsFiltered;
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    watch: {
 | 
			
		||||
        field(newValue, oldValue) {
 | 
			
		||||
            if (newValue !== oldValue) {
 | 
			
		||||
 | 
			
		||||
                const data = {
 | 
			
		||||
                    model: this.model,
 | 
			
		||||
                    value: newValue
 | 
			
		||||
@@ -160,17 +123,17 @@ export default {
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    mounted() {
 | 
			
		||||
        this.autocompleteInputAndArrow = this.$refs.autoCompleteForm;
 | 
			
		||||
        this.autocompleteInputElement = this.$refs.autoCompleteInput;
 | 
			
		||||
        if (this.model.options && this.model.options.length && !this.model.options[0].name) {
 | 
			
		||||
            // If options is only an array of string.
 | 
			
		||||
            this.options = this.model.options.map((option) => {
 | 
			
		||||
                return {
 | 
			
		||||
                    name: option
 | 
			
		||||
                };
 | 
			
		||||
        this.options = this.model.options;
 | 
			
		||||
        this.autocompleteInputAndArrow = this.$el.getElementsByClassName('autocompleteInputAndArrow')[0];
 | 
			
		||||
        this.autocompleteInputElement = this.$el.getElementsByClassName('autocompleteInput')[0];
 | 
			
		||||
        if (this.options[0].name) {
 | 
			
		||||
        // If "options" include name, value pair
 | 
			
		||||
            this.optionNames = this.options.map((opt) => {
 | 
			
		||||
                return opt.name;
 | 
			
		||||
            });
 | 
			
		||||
        } else {
 | 
			
		||||
            this.options = this.model.options;
 | 
			
		||||
        // If options is only an array of string.
 | 
			
		||||
            this.optionNames = this.options;
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    destroyed() {
 | 
			
		||||
@@ -259,12 +222,6 @@ export default {
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
        itemStyle(option) {
 | 
			
		||||
            if (option.color) {
 | 
			
		||||
 | 
			
		||||
                return { '--optionIconColor': option.color };
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -40,12 +40,6 @@
 | 
			
		||||
        >
 | 
			
		||||
            {{ name }}
 | 
			
		||||
        </button>
 | 
			
		||||
        <button
 | 
			
		||||
            v-if="removable"
 | 
			
		||||
            class="c-button icon-trash"
 | 
			
		||||
            title="Remove file"
 | 
			
		||||
            @click="removeFile"
 | 
			
		||||
        ></button>
 | 
			
		||||
    </span>
 | 
			
		||||
</span>
 | 
			
		||||
</template>
 | 
			
		||||
@@ -69,9 +63,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 +97,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);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -28,7 +28,6 @@
 | 
			
		||||
    >
 | 
			
		||||
        <input
 | 
			
		||||
            v-model="field"
 | 
			
		||||
            :aria-label="model.name"
 | 
			
		||||
            type="number"
 | 
			
		||||
            :min="model.min"
 | 
			
		||||
            :max="model.max"
 | 
			
		||||
 
 | 
			
		||||
@@ -39,7 +39,7 @@
 | 
			
		||||
import toggleMixin from '../../toggle-check-box-mixin';
 | 
			
		||||
import ToggleSwitch from '@/ui/components/ToggleSwitch.vue';
 | 
			
		||||
 | 
			
		||||
import { v4 as uuid } from 'uuid';
 | 
			
		||||
import uuid from 'uuid';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    components: {
 | 
			
		||||
 
 | 
			
		||||
@@ -19,27 +19,27 @@
 | 
			
		||||
 * this source code distribution or the Licensing information page available
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
import EventEmitter from "EventEmitter";
 | 
			
		||||
import SimpleIndicator from "./SimpleIndicator";
 | 
			
		||||
 | 
			
		||||
class IndicatorAPI extends EventEmitter {
 | 
			
		||||
    constructor(openmct) {
 | 
			
		||||
        super();
 | 
			
		||||
 | 
			
		||||
define([
 | 
			
		||||
    './SimpleIndicator',
 | 
			
		||||
    'lodash'
 | 
			
		||||
], function (
 | 
			
		||||
    SimpleIndicator,
 | 
			
		||||
    _
 | 
			
		||||
) {
 | 
			
		||||
    function IndicatorAPI(openmct) {
 | 
			
		||||
        this.openmct = openmct;
 | 
			
		||||
        this.indicatorObjects = [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getIndicatorObjectsByPriority() {
 | 
			
		||||
    IndicatorAPI.prototype.getIndicatorObjectsByPriority = function () {
 | 
			
		||||
        const sortedIndicators = this.indicatorObjects.sort((a, b) => b.priority - a.priority);
 | 
			
		||||
 | 
			
		||||
        return sortedIndicators;
 | 
			
		||||
    }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    simpleIndicator() {
 | 
			
		||||
    IndicatorAPI.prototype.simpleIndicator = function () {
 | 
			
		||||
        return new SimpleIndicator(this.openmct);
 | 
			
		||||
    }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Accepts an indicator object, which is a simple object
 | 
			
		||||
@@ -62,16 +62,14 @@ class IndicatorAPI extends EventEmitter {
 | 
			
		||||
     * myIndicator.iconClass("icon-info");
 | 
			
		||||
     *
 | 
			
		||||
     */
 | 
			
		||||
    add(indicator) {
 | 
			
		||||
    IndicatorAPI.prototype.add = function (indicator) {
 | 
			
		||||
        if (!indicator.priority) {
 | 
			
		||||
            indicator.priority = this.openmct.priority.DEFAULT;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.indicatorObjects.push(indicator);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
        this.emit('addIndicator', indicator);
 | 
			
		||||
    }
 | 
			
		||||
    return IndicatorAPI;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default IndicatorAPI;
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -20,101 +20,82 @@
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
import EventEmitter from 'EventEmitter';
 | 
			
		||||
import indicatorTemplate from './res/indicator-template.html';
 | 
			
		||||
import { convertTemplateToHTML } from '@/utils/template/templateHelpers';
 | 
			
		||||
define(['zepto', './res/indicator-template.html'],
 | 
			
		||||
    function ($, indicatorTemplate) {
 | 
			
		||||
        const DEFAULT_ICON_CLASS = 'icon-info';
 | 
			
		||||
 | 
			
		||||
const DEFAULT_ICON_CLASS = 'icon-info';
 | 
			
		||||
        function SimpleIndicator(openmct) {
 | 
			
		||||
            this.openmct = openmct;
 | 
			
		||||
            this.element = $(indicatorTemplate)[0];
 | 
			
		||||
            this.priority = openmct.priority.DEFAULT;
 | 
			
		||||
 | 
			
		||||
class SimpleIndicator extends EventEmitter {
 | 
			
		||||
    constructor(openmct) {
 | 
			
		||||
        super();
 | 
			
		||||
            this.textElement = this.element.querySelector('.js-indicator-text');
 | 
			
		||||
 | 
			
		||||
        this.openmct = openmct;
 | 
			
		||||
        this.element = convertTemplateToHTML(indicatorTemplate)[0];
 | 
			
		||||
        this.priority = openmct.priority.DEFAULT;
 | 
			
		||||
 | 
			
		||||
        this.textElement = this.element.querySelector('.js-indicator-text');
 | 
			
		||||
 | 
			
		||||
        //Set defaults
 | 
			
		||||
        this.text('New Indicator');
 | 
			
		||||
        this.description('');
 | 
			
		||||
        this.iconClass(DEFAULT_ICON_CLASS);
 | 
			
		||||
 | 
			
		||||
        this.click = this.click.bind(this);
 | 
			
		||||
 | 
			
		||||
        this.element.addEventListener('click', this.click);
 | 
			
		||||
        openmct.once('destroy', () => {
 | 
			
		||||
            this.removeAllListeners();
 | 
			
		||||
            this.element.removeEventListener('click', this.click);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    text(text) {
 | 
			
		||||
        if (text !== undefined && text !== this.textValue) {
 | 
			
		||||
            this.textValue = text;
 | 
			
		||||
            this.textElement.innerText = text;
 | 
			
		||||
 | 
			
		||||
            if (!text) {
 | 
			
		||||
                this.element.classList.add('hidden');
 | 
			
		||||
            } else {
 | 
			
		||||
                this.element.classList.remove('hidden');
 | 
			
		||||
            }
 | 
			
		||||
            //Set defaults
 | 
			
		||||
            this.text('New Indicator');
 | 
			
		||||
            this.description('');
 | 
			
		||||
            this.iconClass(DEFAULT_ICON_CLASS);
 | 
			
		||||
            this.statusClass('');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return this.textValue;
 | 
			
		||||
    }
 | 
			
		||||
        SimpleIndicator.prototype.text = function (text) {
 | 
			
		||||
            if (text !== undefined && text !== this.textValue) {
 | 
			
		||||
                this.textValue = text;
 | 
			
		||||
                this.textElement.innerText = text;
 | 
			
		||||
 | 
			
		||||
    description(description) {
 | 
			
		||||
        if (description !== undefined && description !== this.descriptionValue) {
 | 
			
		||||
            this.descriptionValue = description;
 | 
			
		||||
            this.element.title = description;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return this.descriptionValue;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    iconClass(iconClass) {
 | 
			
		||||
        if (iconClass !== undefined && iconClass !== this.iconClassValue) {
 | 
			
		||||
            // element.classList is precious and throws errors if you try and add
 | 
			
		||||
            // or remove empty strings
 | 
			
		||||
            if (this.iconClassValue) {
 | 
			
		||||
                this.element.classList.remove(this.iconClassValue);
 | 
			
		||||
                if (!text) {
 | 
			
		||||
                    this.element.classList.add('hidden');
 | 
			
		||||
                } else {
 | 
			
		||||
                    this.element.classList.remove('hidden');
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (iconClass) {
 | 
			
		||||
                this.element.classList.add(iconClass);
 | 
			
		||||
            return this.textValue;
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        SimpleIndicator.prototype.description = function (description) {
 | 
			
		||||
            if (description !== undefined && description !== this.descriptionValue) {
 | 
			
		||||
                this.descriptionValue = description;
 | 
			
		||||
                this.element.title = description;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.iconClassValue = iconClass;
 | 
			
		||||
        }
 | 
			
		||||
            return this.descriptionValue;
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        return this.iconClassValue;
 | 
			
		||||
    }
 | 
			
		||||
        SimpleIndicator.prototype.iconClass = function (iconClass) {
 | 
			
		||||
            if (iconClass !== undefined && iconClass !== this.iconClassValue) {
 | 
			
		||||
                // element.classList is precious and throws errors if you try and add
 | 
			
		||||
                // or remove empty strings
 | 
			
		||||
                if (this.iconClassValue) {
 | 
			
		||||
                    this.element.classList.remove(this.iconClassValue);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
    statusClass(statusClass) {
 | 
			
		||||
        if (arguments.length === 1 && statusClass !== this.statusClassValue) {
 | 
			
		||||
            if (this.statusClassValue) {
 | 
			
		||||
                this.element.classList.remove(this.statusClassValue);
 | 
			
		||||
                if (iconClass) {
 | 
			
		||||
                    this.element.classList.add(iconClass);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                this.iconClassValue = iconClass;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (statusClass !== undefined) {
 | 
			
		||||
                this.element.classList.add(statusClass);
 | 
			
		||||
            return this.iconClassValue;
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        SimpleIndicator.prototype.statusClass = function (statusClass) {
 | 
			
		||||
            if (statusClass !== undefined && statusClass !== this.statusClassValue) {
 | 
			
		||||
                if (this.statusClassValue) {
 | 
			
		||||
                    this.element.classList.remove(this.statusClassValue);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (statusClass) {
 | 
			
		||||
                    this.element.classList.add(statusClass);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                this.statusClassValue = statusClass;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            this.statusClassValue = statusClass;
 | 
			
		||||
        }
 | 
			
		||||
            return this.statusClassValue;
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        return this.statusClassValue;
 | 
			
		||||
        return SimpleIndicator;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    click(event) {
 | 
			
		||||
        this.emit('click', event);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getElement() {
 | 
			
		||||
        return this.element;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default SimpleIndicator;
 | 
			
		||||
);
 | 
			
		||||
 
 | 
			
		||||
@@ -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();
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,6 @@
 | 
			
		||||
                :key="action.name"
 | 
			
		||||
                :class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
 | 
			
		||||
                :title="action.description"
 | 
			
		||||
                :data-testid="action.testId || false"
 | 
			
		||||
                @click="action.onItemClicked"
 | 
			
		||||
            >
 | 
			
		||||
                {{ action.name }}
 | 
			
		||||
@@ -36,9 +35,8 @@
 | 
			
		||||
        <li
 | 
			
		||||
            v-for="action in options.actions"
 | 
			
		||||
            :key="action.name"
 | 
			
		||||
            :class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
 | 
			
		||||
            :class="action.cssClass"
 | 
			
		||||
            :title="action.description"
 | 
			
		||||
            :data-testid="action.testId || false"
 | 
			
		||||
            @click="action.onItemClicked"
 | 
			
		||||
        >
 | 
			
		||||
            {{ action.name }}
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,6 @@
 | 
			
		||||
                :key="action.name"
 | 
			
		||||
                :class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
 | 
			
		||||
                :title="action.description"
 | 
			
		||||
                :data-testid="action.testId || false"
 | 
			
		||||
                @click="action.onItemClicked"
 | 
			
		||||
                @mouseover="toggleItemDescription(action)"
 | 
			
		||||
                @mouseleave="toggleItemDescription()"
 | 
			
		||||
@@ -46,7 +45,6 @@
 | 
			
		||||
            :key="action.name"
 | 
			
		||||
            :class="action.cssClass"
 | 
			
		||||
            :title="action.description"
 | 
			
		||||
            :data-testid="action.testId || false"
 | 
			
		||||
            @click="action.onItemClicked"
 | 
			
		||||
            @mouseover="toggleItemDescription(action)"
 | 
			
		||||
            @mouseleave="toggleItemDescription()"
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,7 @@
 | 
			
		||||
 * at runtime from the About dialog for additional information.
 | 
			
		||||
 *****************************************************************************/
 | 
			
		||||
 | 
			
		||||
import { v4 as uuid } from 'uuid';
 | 
			
		||||
import uuid from 'uuid';
 | 
			
		||||
 | 
			
		||||
class InMemorySearchProvider {
 | 
			
		||||
    /**
 | 
			
		||||
@@ -39,10 +39,11 @@ class InMemorySearchProvider {
 | 
			
		||||
         * If max results is not specified in query, use this as default.
 | 
			
		||||
         */
 | 
			
		||||
        this.DEFAULT_MAX_RESULTS = 100;
 | 
			
		||||
 | 
			
		||||
        this.openmct = openmct;
 | 
			
		||||
 | 
			
		||||
        this.indexedIds = {};
 | 
			
		||||
        this.indexedCompositions = {};
 | 
			
		||||
        this.indexedTags = {};
 | 
			
		||||
        this.idsToIndex = [];
 | 
			
		||||
        this.pendingIndex = {};
 | 
			
		||||
        this.pendingRequests = 0;
 | 
			
		||||
@@ -51,18 +52,11 @@ class InMemorySearchProvider {
 | 
			
		||||
        /**
 | 
			
		||||
         * If we don't have SharedWorkers available (e.g., iOS)
 | 
			
		||||
         */
 | 
			
		||||
        this.localIndexedDomainObjects = {};
 | 
			
		||||
        this.localIndexedAnnotationsByDomainObject = {};
 | 
			
		||||
        this.localIndexedAnnotationsByTag = {};
 | 
			
		||||
        this.localIndexedItems = {};
 | 
			
		||||
 | 
			
		||||
        this.pendingQueries = {};
 | 
			
		||||
        this.onWorkerMessage = this.onWorkerMessage.bind(this);
 | 
			
		||||
        this.onWorkerMessageError = this.onWorkerMessageError.bind(this);
 | 
			
		||||
        this.localSearchForObjects = this.localSearchForObjects.bind(this);
 | 
			
		||||
        this.localSearchForAnnotations = this.localSearchForAnnotations.bind(this);
 | 
			
		||||
        this.localSearchForTags = this.localSearchForTags.bind(this);
 | 
			
		||||
        this.localSearchForNotebookAnnotations = this.localSearchForNotebookAnnotations.bind(this);
 | 
			
		||||
        this.onAnnotationCreation = this.onAnnotationCreation.bind(this);
 | 
			
		||||
        this.onerror = this.onWorkerError.bind(this);
 | 
			
		||||
        this.startIndexing = this.startIndexing.bind(this);
 | 
			
		||||
 | 
			
		||||
@@ -82,39 +76,13 @@ class InMemorySearchProvider {
 | 
			
		||||
 | 
			
		||||
    startIndexing() {
 | 
			
		||||
        const rootObject = this.openmct.objects.rootProvider.rootObject;
 | 
			
		||||
 | 
			
		||||
        this.searchTypes = this.openmct.objects.SEARCH_TYPES;
 | 
			
		||||
 | 
			
		||||
        this.supportedSearchTypes = [this.searchTypes.OBJECTS, this.searchTypes.ANNOTATIONS, this.searchTypes.NOTEBOOK_ANNOTATIONS, this.searchTypes.TAGS];
 | 
			
		||||
 | 
			
		||||
        this.scheduleForIndexing(rootObject.identifier);
 | 
			
		||||
 | 
			
		||||
        this.indexAnnotations();
 | 
			
		||||
 | 
			
		||||
        if (typeof SharedWorker !== 'undefined') {
 | 
			
		||||
            this.worker = this.startSharedWorker();
 | 
			
		||||
        } else {
 | 
			
		||||
            // we must be on iOS
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.openmct.annotation.on('annotationCreated', this.onAnnotationCreation);
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    indexAnnotations() {
 | 
			
		||||
        const theInMemorySearchProvider = this;
 | 
			
		||||
        Object.values(this.openmct.objects.providers).forEach(objectProvider => {
 | 
			
		||||
            if (objectProvider.getAllObjects) {
 | 
			
		||||
                const allObjects = objectProvider.getAllObjects();
 | 
			
		||||
                if (allObjects) {
 | 
			
		||||
                    Object.values(allObjects).forEach(domainObject => {
 | 
			
		||||
                        if (domainObject.type === 'annotation') {
 | 
			
		||||
                            theInMemorySearchProvider.scheduleForIndexing(domainObject.identifier);
 | 
			
		||||
                        }
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -130,60 +98,51 @@ class InMemorySearchProvider {
 | 
			
		||||
        return intermediateResponse;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    search(query, searchType) {
 | 
			
		||||
    /**
 | 
			
		||||
     * Query the search provider for results.
 | 
			
		||||
     *
 | 
			
		||||
     * @param {String} input the string to search by.
 | 
			
		||||
     * @param {Number} maxResults max number of results to return.
 | 
			
		||||
     * @returns {Promise} a promise for a modelResults object.
 | 
			
		||||
     */
 | 
			
		||||
    query(input, maxResults) {
 | 
			
		||||
        if (!maxResults) {
 | 
			
		||||
            maxResults = this.DEFAULT_MAX_RESULTS;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const queryId = uuid();
 | 
			
		||||
        const pendingQuery = this.getIntermediateResponse();
 | 
			
		||||
        this.pendingQueries[queryId] = pendingQuery;
 | 
			
		||||
        const searchOptions = {
 | 
			
		||||
            queryId,
 | 
			
		||||
            searchType,
 | 
			
		||||
            query,
 | 
			
		||||
            maxResults: this.DEFAULT_MAX_RESULTS
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        if (this.worker) {
 | 
			
		||||
            this.#dispatchSearchToWorker(searchOptions);
 | 
			
		||||
            this.dispatchSearch(queryId, input, maxResults);
 | 
			
		||||
        } else {
 | 
			
		||||
            this.#localQueryFallBack(searchOptions);
 | 
			
		||||
            this.localSearch(queryId, input, maxResults);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return pendingQuery.promise;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #localQueryFallBack({queryId, searchType, query, maxResults}) {
 | 
			
		||||
        if (searchType === this.searchTypes.OBJECTS) {
 | 
			
		||||
            return this.localSearchForObjects(queryId, query, maxResults);
 | 
			
		||||
        } else if (searchType === this.searchTypes.ANNOTATIONS) {
 | 
			
		||||
            return this.localSearchForAnnotations(queryId, query, maxResults);
 | 
			
		||||
        } else if (searchType === this.searchTypes.NOTEBOOK_ANNOTATIONS) {
 | 
			
		||||
            return this.localSearchForNotebookAnnotations(queryId, query, maxResults);
 | 
			
		||||
        } else if (searchType === this.searchTypes.TAGS) {
 | 
			
		||||
            return this.localSearchForTags(queryId, query, maxResults);
 | 
			
		||||
        } else {
 | 
			
		||||
            throw new Error(`🤷♂️ Unknown search type passed: ${searchType}`);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    supportsSearchType(searchType) {
 | 
			
		||||
        return this.supportedSearchTypes.includes(searchType);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Handle messages from the worker.
 | 
			
		||||
     * Handle messages from the worker.  Only really knows how to handle search
 | 
			
		||||
     * results, which are parsed, transformed into a modelResult object, which
 | 
			
		||||
     * is used to resolve the corresponding promise.
 | 
			
		||||
     * @private
 | 
			
		||||
     */
 | 
			
		||||
    async onWorkerMessage(event) {
 | 
			
		||||
        if (event.data.request !== 'search') {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const pendingQuery = this.pendingQueries[event.data.queryId];
 | 
			
		||||
        const modelResults = {
 | 
			
		||||
            total: event.data.total
 | 
			
		||||
        };
 | 
			
		||||
        modelResults.hits = await Promise.all(event.data.results.map(async (hit) => {
 | 
			
		||||
            if (hit && hit.keyString) {
 | 
			
		||||
                const identifier = this.openmct.objects.parseKeyString(hit.keyString);
 | 
			
		||||
                const domainObject = await this.openmct.objects.get(identifier);
 | 
			
		||||
            const identifier = this.openmct.objects.parseKeyString(hit.keyString);
 | 
			
		||||
            const domainObject = await this.openmct.objects.get(identifier);
 | 
			
		||||
 | 
			
		||||
                return domainObject;
 | 
			
		||||
            }
 | 
			
		||||
            return domainObject;
 | 
			
		||||
        }));
 | 
			
		||||
 | 
			
		||||
        pendingQuery.resolve(modelResults);
 | 
			
		||||
@@ -224,8 +183,7 @@ class InMemorySearchProvider {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Schedule an id to be indexed at a later date.  If there are less
 | 
			
		||||
     * pending requests than the maximum allowed, this will kick off an indexing request.
 | 
			
		||||
     * This is done only when indexing first begins and we need to index a lot of objects.
 | 
			
		||||
     * pending requests then allowed, will kick off an indexing request.
 | 
			
		||||
     *
 | 
			
		||||
     * @private
 | 
			
		||||
     * @param {identifier} id to be indexed.
 | 
			
		||||
@@ -258,15 +216,6 @@ class InMemorySearchProvider {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    onAnnotationCreation(annotationObject) {
 | 
			
		||||
 | 
			
		||||
        const objectProvider = this.openmct.objects.getProvider(annotationObject.identifier);
 | 
			
		||||
        if (objectProvider === undefined || objectProvider.search === undefined) {
 | 
			
		||||
            const provider = this;
 | 
			
		||||
            provider.index(annotationObject);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    onNameMutation(domainObject, name) {
 | 
			
		||||
        const provider = this;
 | 
			
		||||
 | 
			
		||||
@@ -274,13 +223,6 @@ class InMemorySearchProvider {
 | 
			
		||||
        provider.index(domainObject);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    onTagMutation(domainObject, newTags) {
 | 
			
		||||
        domainObject.tags = newTags;
 | 
			
		||||
        const provider = this;
 | 
			
		||||
 | 
			
		||||
        provider.index(domainObject);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    onCompositionMutation(domainObject, composition) {
 | 
			
		||||
        const provider = this;
 | 
			
		||||
        const indexedComposition = domainObject.composition;
 | 
			
		||||
@@ -317,13 +259,6 @@ class InMemorySearchProvider {
 | 
			
		||||
                'composition',
 | 
			
		||||
                this.onCompositionMutation.bind(this, domainObject)
 | 
			
		||||
            );
 | 
			
		||||
            if (domainObject.type === 'annotation') {
 | 
			
		||||
                this.indexedTags[keyString] = this.openmct.objects.observe(
 | 
			
		||||
                    domainObject,
 | 
			
		||||
                    'tags',
 | 
			
		||||
                    this.onTagMutation.bind(this, domainObject)
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ((keyString !== 'ROOT')) {
 | 
			
		||||
@@ -382,83 +317,26 @@ class InMemorySearchProvider {
 | 
			
		||||
     * @private
 | 
			
		||||
     * @returns {String} a unique query Id for the query.
 | 
			
		||||
     */
 | 
			
		||||
    #dispatchSearchToWorker({queryId, searchType, query, maxResults}) {
 | 
			
		||||
    dispatchSearch(queryId, searchInput, maxResults) {
 | 
			
		||||
        const message = {
 | 
			
		||||
            request: searchType.toString(),
 | 
			
		||||
            input: query,
 | 
			
		||||
            request: 'search',
 | 
			
		||||
            input: searchInput,
 | 
			
		||||
            maxResults,
 | 
			
		||||
            queryId
 | 
			
		||||
        };
 | 
			
		||||
        this.worker.port.postMessage(message);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    localIndexTags(keyString, objectToIndex, model) {
 | 
			
		||||
        // add new tags
 | 
			
		||||
        model.tags.forEach(tagID => {
 | 
			
		||||
            if (!this.localIndexedAnnotationsByTag[tagID]) {
 | 
			
		||||
                this.localIndexedAnnotationsByTag[tagID] = [];
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const existsInIndex = this.localIndexedAnnotationsByTag[tagID].some(indexedObject => {
 | 
			
		||||
                return indexedObject.keyString === objectToIndex.keyString;
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (!existsInIndex) {
 | 
			
		||||
                this.localIndexedAnnotationsByTag[tagID].push(objectToIndex);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        });
 | 
			
		||||
        const tagsToRemoveFromIndex = Object.keys(this.localIndexedAnnotationsByTag).filter(indexedTag => {
 | 
			
		||||
            return !(model.tags.includes(indexedTag));
 | 
			
		||||
        });
 | 
			
		||||
        tagsToRemoveFromIndex.forEach(tagToRemoveFromIndex => {
 | 
			
		||||
            this.localIndexedAnnotationsByTag[tagToRemoveFromIndex] = this.localIndexedAnnotationsByTag[tagToRemoveFromIndex].filter(indexedAnnotation => {
 | 
			
		||||
                const shouldKeep = indexedAnnotation.keyString !== keyString;
 | 
			
		||||
 | 
			
		||||
                return shouldKeep;
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    localIndexAnnotation(objectToIndex, model) {
 | 
			
		||||
        Object.keys(model.targets).forEach(targetID => {
 | 
			
		||||
            if (!this.localIndexedAnnotationsByDomainObject[targetID]) {
 | 
			
		||||
                this.localIndexedAnnotationsByDomainObject[targetID] = [];
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            objectToIndex.targets = model.targets;
 | 
			
		||||
            objectToIndex.tags = model.tags;
 | 
			
		||||
            const existsInIndex = this.localIndexedAnnotationsByDomainObject[targetID].some(indexedObject => {
 | 
			
		||||
                return indexedObject.keyString === objectToIndex.keyString;
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (!existsInIndex) {
 | 
			
		||||
                this.localIndexedAnnotationsByDomainObject[targetID].push(objectToIndex);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * A local version of the same SharedWorker function
 | 
			
		||||
     * if we don't have SharedWorkers available (e.g., iOS)
 | 
			
		||||
     */
 | 
			
		||||
    localIndexItem(keyString, model) {
 | 
			
		||||
        const objectToIndex = {
 | 
			
		||||
        this.localIndexedItems[keyString] = {
 | 
			
		||||
            type: model.type,
 | 
			
		||||
            name: model.name,
 | 
			
		||||
            keyString
 | 
			
		||||
        };
 | 
			
		||||
        if (model && (model.type === 'annotation')) {
 | 
			
		||||
            if (model.targets) {
 | 
			
		||||
                this.localIndexAnnotation(objectToIndex, model);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (model.tags) {
 | 
			
		||||
                this.localIndexTags(keyString, objectToIndex, model);
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            this.localIndexedDomainObjects[keyString] = objectToIndex;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -468,122 +346,21 @@ class InMemorySearchProvider {
 | 
			
		||||
     * Gets search results from the indexedItems based on provided search
 | 
			
		||||
     * input. Returns matching results from indexedItems
 | 
			
		||||
     */
 | 
			
		||||
    localSearchForObjects(queryId, searchInput, maxResults) {
 | 
			
		||||
    localSearch(queryId, searchInput, maxResults) {
 | 
			
		||||
        // This results dictionary will have domain object ID keys which
 | 
			
		||||
        // point to the value the domain object's score.
 | 
			
		||||
        let results = [];
 | 
			
		||||
        let results;
 | 
			
		||||
        const input = searchInput.trim().toLowerCase();
 | 
			
		||||
        const message = {
 | 
			
		||||
            request: 'searchForObjects',
 | 
			
		||||
            results: [],
 | 
			
		||||
            request: 'search',
 | 
			
		||||
            results: {},
 | 
			
		||||
            total: 0,
 | 
			
		||||
            queryId
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        results = Object.values(this.localIndexedDomainObjects).filter((indexedItem) => {
 | 
			
		||||
        results = Object.values(this.localIndexedItems).filter((indexedItem) => {
 | 
			
		||||
            return indexedItem.name.toLowerCase().includes(input);
 | 
			
		||||
        }) || [];
 | 
			
		||||
 | 
			
		||||
        message.total = results.length;
 | 
			
		||||
        message.results = results
 | 
			
		||||
            .slice(0, maxResults);
 | 
			
		||||
        const eventToReturn = {
 | 
			
		||||
            data: message
 | 
			
		||||
        };
 | 
			
		||||
        this.onWorkerMessage(eventToReturn);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * A local version of the same SharedWorker function
 | 
			
		||||
     * if we don't have SharedWorkers available (e.g., iOS)
 | 
			
		||||
     */
 | 
			
		||||
    localSearchForAnnotations(queryId, searchInput, maxResults) {
 | 
			
		||||
        // This results dictionary will have domain object ID keys which
 | 
			
		||||
        // point to the value the domain object's score.
 | 
			
		||||
        let results = [];
 | 
			
		||||
        const message = {
 | 
			
		||||
            request: 'searchForAnnotations',
 | 
			
		||||
            results: [],
 | 
			
		||||
            total: 0,
 | 
			
		||||
            queryId
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        results = this.localIndexedAnnotationsByDomainObject[searchInput] || [];
 | 
			
		||||
 | 
			
		||||
        message.total = results.length;
 | 
			
		||||
        message.results = results
 | 
			
		||||
            .slice(0, maxResults);
 | 
			
		||||
        const eventToReturn = {
 | 
			
		||||
            data: message
 | 
			
		||||
        };
 | 
			
		||||
        this.onWorkerMessage(eventToReturn);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * A local version of the same SharedWorker function
 | 
			
		||||
     * if we don't have SharedWorkers available (e.g., iOS)
 | 
			
		||||
     */
 | 
			
		||||
    localSearchForTags(queryId, matchingTagKeys, maxResults) {
 | 
			
		||||
        let results = [];
 | 
			
		||||
        const message = {
 | 
			
		||||
            request: 'searchForTags',
 | 
			
		||||
            results: [],
 | 
			
		||||
            total: 0,
 | 
			
		||||
            queryId
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        if (matchingTagKeys) {
 | 
			
		||||
            matchingTagKeys.forEach(matchingTag => {
 | 
			
		||||
                const matchingAnnotations = this.localIndexedAnnotationsByTag[matchingTag];
 | 
			
		||||
                if (matchingAnnotations) {
 | 
			
		||||
                    matchingAnnotations.forEach(matchingAnnotation => {
 | 
			
		||||
                        const existsInResults = results.some(indexedObject => {
 | 
			
		||||
                            return matchingAnnotation.keyString === indexedObject.keyString;
 | 
			
		||||
                        });
 | 
			
		||||
                        if (!existsInResults) {
 | 
			
		||||
                            results.push(matchingAnnotation);
 | 
			
		||||
                        }
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        message.total = results.length;
 | 
			
		||||
        message.results = results
 | 
			
		||||
            .slice(0, maxResults);
 | 
			
		||||
        const eventToReturn = {
 | 
			
		||||
            data: message
 | 
			
		||||
        };
 | 
			
		||||
        this.onWorkerMessage(eventToReturn);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * A local version of the same SharedWorker function
 | 
			
		||||
     * if we don't have SharedWorkers available (e.g., iOS)
 | 
			
		||||
     */
 | 
			
		||||
    localSearchForNotebookAnnotations(queryId, {entryId, targetKeyString}, maxResults) {
 | 
			
		||||
        // This results dictionary will have domain object ID keys which
 | 
			
		||||
        // point to the value the domain object's score.
 | 
			
		||||
        let results = [];
 | 
			
		||||
        const message = {
 | 
			
		||||
            request: 'searchForNotebookAnnotations',
 | 
			
		||||
            results: [],
 | 
			
		||||
            total: 0,
 | 
			
		||||
            queryId
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const matchingAnnotations = this.localIndexedAnnotationsByDomainObject[targetKeyString];
 | 
			
		||||
        if (matchingAnnotations) {
 | 
			
		||||
            results = matchingAnnotations.filter(matchingAnnotation => {
 | 
			
		||||
                if (!matchingAnnotation.targets) {
 | 
			
		||||
                    return false;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                const target = matchingAnnotation.targets[targetKeyString];
 | 
			
		||||
 | 
			
		||||
                return (target && target.entryId && (target.entryId === entryId));
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        message.total = results.length;
 | 
			
		||||
        message.results = results
 | 
			
		||||
 
 | 
			
		||||
@@ -26,27 +26,16 @@
 | 
			
		||||
(function () {
 | 
			
		||||
    // An object composed of domain object IDs and models
 | 
			
		||||
    // {id: domainObject's ID, name: domainObject's name}
 | 
			
		||||
    const indexedDomainObjects = {};
 | 
			
		||||
    const indexedAnnotationsByDomainObject = {};
 | 
			
		||||
    const indexedAnnotationsByTag = {};
 | 
			
		||||
    const indexedItems = {};
 | 
			
		||||
 | 
			
		||||
    self.onconnect = function (e) {
 | 
			
		||||
        const port = e.ports[0];
 | 
			
		||||
 | 
			
		||||
        port.onmessage = function (event) {
 | 
			
		||||
            const requestType = event.data.request;
 | 
			
		||||
            if (requestType === 'index') {
 | 
			
		||||
            if (event.data.request === 'index') {
 | 
			
		||||
                indexItem(event.data.keyString, event.data.model);
 | 
			
		||||
            } else if (requestType === 'OBJECTS') {
 | 
			
		||||
                port.postMessage(searchForObjects(event.data));
 | 
			
		||||
            } else if (requestType === 'ANNOTATIONS') {
 | 
			
		||||
                port.postMessage(searchForAnnotations(event.data));
 | 
			
		||||
            } else if (requestType === 'TAGS') {
 | 
			
		||||
                port.postMessage(searchForTags(event.data));
 | 
			
		||||
            } else if (requestType === 'NOTEBOOK_ANNOTATIONS') {
 | 
			
		||||
                port.postMessage(searchForNotebookAnnotations(event.data));
 | 
			
		||||
            } else {
 | 
			
		||||
                throw new Error(`Unknown request ${event.data.request}`);
 | 
			
		||||
            } else if (event.data.request === 'search') {
 | 
			
		||||
                port.postMessage(search(event.data));
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
@@ -59,70 +48,12 @@
 | 
			
		||||
        console.error('Error on feed', error);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    function indexAnnotation(objectToIndex, model) {
 | 
			
		||||
        Object.keys(model.targets).forEach(targetID => {
 | 
			
		||||
            if (!indexedAnnotationsByDomainObject[targetID]) {
 | 
			
		||||
                indexedAnnotationsByDomainObject[targetID] = [];
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            objectToIndex.targets = model.targets;
 | 
			
		||||
            objectToIndex.tags = model.tags;
 | 
			
		||||
            const existsInIndex = indexedAnnotationsByDomainObject[targetID].some(indexedObject => {
 | 
			
		||||
                return indexedObject.keyString === objectToIndex.keyString;
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (!existsInIndex) {
 | 
			
		||||
                indexedAnnotationsByDomainObject[targetID].push(objectToIndex);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function indexTags(keyString, objectToIndex, model) {
 | 
			
		||||
        // add new tags
 | 
			
		||||
        model.tags.forEach(tagID => {
 | 
			
		||||
            if (!indexedAnnotationsByTag[tagID]) {
 | 
			
		||||
                indexedAnnotationsByTag[tagID] = [];
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const existsInIndex = indexedAnnotationsByTag[tagID].some(indexedObject => {
 | 
			
		||||
                return indexedObject.keyString === objectToIndex.keyString;
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (!existsInIndex) {
 | 
			
		||||
                indexedAnnotationsByTag[tagID].push(objectToIndex);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        });
 | 
			
		||||
        // remove old tags
 | 
			
		||||
        const tagsToRemoveFromIndex = Object.keys(indexedAnnotationsByTag).filter(indexedTag => {
 | 
			
		||||
            return !(model.tags.includes(indexedTag));
 | 
			
		||||
        });
 | 
			
		||||
        tagsToRemoveFromIndex.forEach(tagToRemoveFromIndex => {
 | 
			
		||||
            indexedAnnotationsByTag[tagToRemoveFromIndex] = indexedAnnotationsByTag[tagToRemoveFromIndex].filter(indexedAnnotation => {
 | 
			
		||||
                const shouldKeep = indexedAnnotation.keyString !== keyString;
 | 
			
		||||
 | 
			
		||||
                return shouldKeep;
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function indexItem(keyString, model) {
 | 
			
		||||
        const objectToIndex = {
 | 
			
		||||
        indexedItems[keyString] = {
 | 
			
		||||
            type: model.type,
 | 
			
		||||
            name: model.name,
 | 
			
		||||
            keyString
 | 
			
		||||
        };
 | 
			
		||||
        if (model && (model.type === 'annotation')) {
 | 
			
		||||
            if (model.targets) {
 | 
			
		||||
                indexAnnotation(objectToIndex, model);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (model.tags) {
 | 
			
		||||
                indexTags(keyString, objectToIndex, model);
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            indexedDomainObjects[keyString] = objectToIndex;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -134,98 +65,21 @@
 | 
			
		||||
     *           * maxResults: The maximum number of search results desired
 | 
			
		||||
     *           * queryId: an id identifying this query, will be returned.
 | 
			
		||||
     */
 | 
			
		||||
    function searchForObjects(data) {
 | 
			
		||||
        let results = [];
 | 
			
		||||
    function search(data) {
 | 
			
		||||
        // This results dictionary will have domain object ID keys which
 | 
			
		||||
        // point to the value the domain object's score.
 | 
			
		||||
        let results;
 | 
			
		||||
        const input = data.input.trim().toLowerCase();
 | 
			
		||||
        const message = {
 | 
			
		||||
            request: 'searchForObjects',
 | 
			
		||||
            results: [],
 | 
			
		||||
            total: 0,
 | 
			
		||||
            queryId: data.queryId
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        results = Object.values(indexedDomainObjects).filter((indexedItem) => {
 | 
			
		||||
            return indexedItem.name.toLowerCase().includes(input);
 | 
			
		||||
        }) || [];
 | 
			
		||||
 | 
			
		||||
        message.total = results.length;
 | 
			
		||||
        message.results = results
 | 
			
		||||
            .slice(0, data.maxResults);
 | 
			
		||||
 | 
			
		||||
        return message;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function searchForAnnotations(data) {
 | 
			
		||||
        let results = [];
 | 
			
		||||
        const message = {
 | 
			
		||||
            request: 'searchForAnnotations',
 | 
			
		||||
            results: [],
 | 
			
		||||
            total: 0,
 | 
			
		||||
            queryId: data.queryId
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        results = indexedAnnotationsByDomainObject[data.input] || [];
 | 
			
		||||
 | 
			
		||||
        message.total = results.length;
 | 
			
		||||
        message.results = results
 | 
			
		||||
            .slice(0, data.maxResults);
 | 
			
		||||
 | 
			
		||||
        return message;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function searchForTags(data) {
 | 
			
		||||
        let results = [];
 | 
			
		||||
        const message = {
 | 
			
		||||
            request: 'searchForTags',
 | 
			
		||||
            results: [],
 | 
			
		||||
            total: 0,
 | 
			
		||||
            queryId: data.queryId
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        if (data.input) {
 | 
			
		||||
            data.input.forEach(matchingTag => {
 | 
			
		||||
                const matchingAnnotations = indexedAnnotationsByTag[matchingTag];
 | 
			
		||||
                if (matchingAnnotations) {
 | 
			
		||||
                    matchingAnnotations.forEach(matchingAnnotation => {
 | 
			
		||||
                        const existsInResults = results.some(indexedObject => {
 | 
			
		||||
                            return matchingAnnotation.keyString === indexedObject.keyString;
 | 
			
		||||
                        });
 | 
			
		||||
                        if (!existsInResults) {
 | 
			
		||||
                            results.push(matchingAnnotation);
 | 
			
		||||
                        }
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        message.total = results.length;
 | 
			
		||||
        message.results = results
 | 
			
		||||
            .slice(0, data.maxResults);
 | 
			
		||||
 | 
			
		||||
        return message;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function searchForNotebookAnnotations(data) {
 | 
			
		||||
        let results = [];
 | 
			
		||||
        const message = {
 | 
			
		||||
            request: 'searchForNotebookAnnotations',
 | 
			
		||||
            request: 'search',
 | 
			
		||||
            results: {},
 | 
			
		||||
            total: 0,
 | 
			
		||||
            queryId: data.queryId
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const matchingAnnotations = indexedAnnotationsByDomainObject[data.input.targetKeyString];
 | 
			
		||||
        if (matchingAnnotations) {
 | 
			
		||||
            results = matchingAnnotations.filter(matchingAnnotation => {
 | 
			
		||||
                if (!matchingAnnotation.targets) {
 | 
			
		||||
                    return false;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                const target = matchingAnnotation.targets[data.input.targetKeyString];
 | 
			
		||||
 | 
			
		||||
                return (target && target.entryId && (target.entryId === data.input.entryId));
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        results = Object.values(indexedItems).filter((indexedItem) => {
 | 
			
		||||
            return indexedItem.name.toLowerCase().includes(input);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        message.total = results.length;
 | 
			
		||||
        message.results = results
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -17,16 +17,13 @@ describe("The Object API Search Function", () => {
 | 
			
		||||
            openmct = createOpenMct();
 | 
			
		||||
 | 
			
		||||
            mockObjectProvider = jasmine.createSpyObj("mock object provider", [
 | 
			
		||||
                "search", "supportsSearchType"
 | 
			
		||||
                "search"
 | 
			
		||||
            ]);
 | 
			
		||||
            anotherMockObjectProvider = jasmine.createSpyObj("another mock object provider", [
 | 
			
		||||
                "search", "supportsSearchType"
 | 
			
		||||
                "search"
 | 
			
		||||
            ]);
 | 
			
		||||
            openmct.objects.addProvider('objects', mockObjectProvider);
 | 
			
		||||
            openmct.objects.addProvider('other-objects', anotherMockObjectProvider);
 | 
			
		||||
            mockObjectProvider.supportsSearchType.and.callFake(() => {
 | 
			
		||||
                return true;
 | 
			
		||||
            });
 | 
			
		||||
            mockObjectProvider.search.and.callFake(() => {
 | 
			
		||||
                return new Promise(resolve => {
 | 
			
		||||
                    const mockProviderSearch = {
 | 
			
		||||
@@ -41,9 +38,6 @@ describe("The Object API Search Function", () => {
 | 
			
		||||
                    }, MOCK_PROVIDER_SEARCH_DELAY);
 | 
			
		||||
                });
 | 
			
		||||
            });
 | 
			
		||||
            anotherMockObjectProvider.supportsSearchType.and.callFake(() => {
 | 
			
		||||
                return true;
 | 
			
		||||
            });
 | 
			
		||||
            anotherMockObjectProvider.search.and.callFake(() => {
 | 
			
		||||
                return new Promise(resolve => {
 | 
			
		||||
                    const anotherMockProviderSearch = {
 | 
			
		||||
@@ -116,8 +110,8 @@ describe("The Object API Search Function", () => {
 | 
			
		||||
                namespace: ''
 | 
			
		||||
            });
 | 
			
		||||
            openmct.objects.addProvider('foo', defaultObjectProvider);
 | 
			
		||||
            spyOn(openmct.objects.inMemorySearchProvider, "search").and.callThrough();
 | 
			
		||||
            spyOn(openmct.objects.inMemorySearchProvider, "localSearchForObjects").and.callThrough();
 | 
			
		||||
            spyOn(openmct.objects.inMemorySearchProvider, "query").and.callThrough();
 | 
			
		||||
            spyOn(openmct.objects.inMemorySearchProvider, "localSearch").and.callThrough();
 | 
			
		||||
 | 
			
		||||
            openmct.on('start', async () => {
 | 
			
		||||
                mockIdentifier1 = {
 | 
			
		||||
@@ -161,7 +155,7 @@ describe("The Object API Search Function", () => {
 | 
			
		||||
 | 
			
		||||
        it("can provide indexing without a provider", () => {
 | 
			
		||||
            openmct.objects.search('foo');
 | 
			
		||||
            expect(openmct.objects.inMemorySearchProvider.search).toHaveBeenCalled();
 | 
			
		||||
            expect(openmct.objects.inMemorySearchProvider.query).toHaveBeenCalled();
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("can do partial search", async () => {
 | 
			
		||||
@@ -183,22 +177,16 @@ describe("The Object API Search Function", () => {
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        describe("Without Shared Workers", () => {
 | 
			
		||||
            let sharedWorkerToRestore;
 | 
			
		||||
            beforeEach(async () => {
 | 
			
		||||
                // use local worker
 | 
			
		||||
                sharedWorkerToRestore = openmct.objects.inMemorySearchProvider.worker;
 | 
			
		||||
                openmct.objects.inMemorySearchProvider.worker = null;
 | 
			
		||||
                // reindex locally
 | 
			
		||||
                await openmct.objects.inMemorySearchProvider.index(mockDomainObject1);
 | 
			
		||||
                await openmct.objects.inMemorySearchProvider.index(mockDomainObject2);
 | 
			
		||||
                await openmct.objects.inMemorySearchProvider.index(mockDomainObject3);
 | 
			
		||||
            });
 | 
			
		||||
            afterEach(() => {
 | 
			
		||||
                openmct.objects.inMemorySearchProvider.worker = sharedWorkerToRestore;
 | 
			
		||||
            });
 | 
			
		||||
            it("calls local search", () => {
 | 
			
		||||
                openmct.objects.search('foo');
 | 
			
		||||
                expect(openmct.objects.inMemorySearchProvider.localSearchForObjects).toHaveBeenCalled();
 | 
			
		||||
                expect(openmct.objects.inMemorySearchProvider.localSearch).toHaveBeenCalled();
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            it("can do partial search", async () => {
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,6 @@
 | 
			
		||||
    <div class="c-overlay__outer">
 | 
			
		||||
        <button
 | 
			
		||||
            v-if="dismissable"
 | 
			
		||||
            aria-label="Close"
 | 
			
		||||
            class="c-click-icon c-overlay__close-button icon-x"
 | 
			
		||||
            @click="destroy"
 | 
			
		||||
        ></button>
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
        });
 | 
			
		||||
@@ -529,7 +529,7 @@ define([
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.openmct.notifications.error(message);
 | 
			
		||||
        console.warn(detailMessage);
 | 
			
		||||
        console.error(detailMessage);
 | 
			
		||||
 | 
			
		||||
        return Promise.resolve([]);
 | 
			
		||||
    };
 | 
			
		||||
 
 | 
			
		||||
@@ -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. */
 | 
			
		||||
 | 
			
		||||
@@ -49,7 +53,6 @@ export class TelemetryCollection extends EventEmitter {
 | 
			
		||||
        this.pageState = undefined;
 | 
			
		||||
        this.lastBounds = undefined;
 | 
			
		||||
        this.requestAbort = undefined;
 | 
			
		||||
        this.isStrategyLatest = this.options.strategy === 'latest';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -58,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());
 | 
			
		||||
@@ -174,14 +177,12 @@ export class TelemetryCollection extends EventEmitter {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let latestBoundedDatum = this.boundedTelemetry[this.boundedTelemetry.length - 1];
 | 
			
		||||
        let data = Array.isArray(telemetryData) ? telemetryData : [telemetryData];
 | 
			
		||||
        let parsedValue;
 | 
			
		||||
        let beforeStartOfBounds;
 | 
			
		||||
        let afterEndOfBounds;
 | 
			
		||||
        let added = [];
 | 
			
		||||
 | 
			
		||||
        // loop through, sort and dedupe
 | 
			
		||||
        for (let datum of data) {
 | 
			
		||||
            parsedValue = this.parseTime(datum);
 | 
			
		||||
            beforeStartOfBounds = parsedValue < this.lastBounds.start;
 | 
			
		||||
@@ -221,17 +222,7 @@ export class TelemetryCollection extends EventEmitter {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (added.length) {
 | 
			
		||||
            // if latest strategy is requested, we need to check if the value is the latest unmitted value
 | 
			
		||||
            if (this.isStrategyLatest) {
 | 
			
		||||
                this.boundedTelemetry = [this.boundedTelemetry[this.boundedTelemetry.length - 1]];
 | 
			
		||||
 | 
			
		||||
                // if true, then this value has yet to be emitted
 | 
			
		||||
                if (this.boundedTelemetry[0] !== latestBoundedDatum) {
 | 
			
		||||
                    this.emit('add', this.boundedTelemetry);
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                this.emit('add', added);
 | 
			
		||||
            }
 | 
			
		||||
            this.emit('add', added);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -276,10 +267,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;
 | 
			
		||||
@@ -291,20 +278,13 @@ export class TelemetryCollection extends EventEmitter {
 | 
			
		||||
 | 
			
		||||
            if (startChanged) {
 | 
			
		||||
                testDatum[this.timeKey] = bounds.start;
 | 
			
		||||
 | 
			
		||||
                // a little more complicated if not latest strategy
 | 
			
		||||
                if (!this.isStrategyLatest) {
 | 
			
		||||
                    // Calculate the new index of the first item within the bounds
 | 
			
		||||
                    startIndex = _.sortedIndexBy(
 | 
			
		||||
                        this.boundedTelemetry,
 | 
			
		||||
                        testDatum,
 | 
			
		||||
                        datum => this.parseTime(datum)
 | 
			
		||||
                    );
 | 
			
		||||
                    discarded = this.boundedTelemetry.splice(0, startIndex);
 | 
			
		||||
                } else if (this.parseTime(testDatum) > this.parseTime(this.boundedTelemetry[0])) {
 | 
			
		||||
                    discarded = this.boundedTelemetry;
 | 
			
		||||
                    this.boundedTelemetry = [];
 | 
			
		||||
                }
 | 
			
		||||
                // Calculate the new index of the first item within the bounds
 | 
			
		||||
                startIndex = _.sortedIndexBy(
 | 
			
		||||
                    this.boundedTelemetry,
 | 
			
		||||
                    testDatum,
 | 
			
		||||
                    datum => this.parseTime(datum)
 | 
			
		||||
                );
 | 
			
		||||
                discarded = this.boundedTelemetry.splice(0, startIndex);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (endChanged) {
 | 
			
		||||
@@ -316,6 +296,7 @@ export class TelemetryCollection extends EventEmitter {
 | 
			
		||||
                    datum => this.parseTime(datum)
 | 
			
		||||
                );
 | 
			
		||||
                added = this.futureBuffer.splice(0, endIndex);
 | 
			
		||||
                this.boundedTelemetry = [...this.boundedTelemetry, ...added];
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (discarded.length > 0) {
 | 
			
		||||
@@ -323,15 +304,9 @@ export class TelemetryCollection extends EventEmitter {
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (added.length > 0) {
 | 
			
		||||
                if (!this.isStrategyLatest) {
 | 
			
		||||
                    this.boundedTelemetry = [...this.boundedTelemetry, ...added];
 | 
			
		||||
                } else {
 | 
			
		||||
                    added = [added[added.length - 1]];
 | 
			
		||||
                    this.boundedTelemetry = added;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                this.emit('add', added);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        } else {
 | 
			
		||||
            // user bounds change, reset
 | 
			
		||||
            this._reset();
 | 
			
		||||
@@ -348,26 +323,16 @@ export class TelemetryCollection extends EventEmitter {
 | 
			
		||||
     * @private
 | 
			
		||||
     */
 | 
			
		||||
    _setTimeSystem(timeSystem) {
 | 
			
		||||
        let domains = [];
 | 
			
		||||
        let metadataValue = { format: timeSystem.key };
 | 
			
		||||
 | 
			
		||||
        if (this.metadata) {
 | 
			
		||||
            domains = this.metadata.valuesForHints(['domain']);
 | 
			
		||||
            metadataValue = this.metadata.value(timeSystem.key) || { format: timeSystem.key };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let domains = this.metadata.valuesForHints(['domain']);
 | 
			
		||||
        let domain = domains.find((d) => d.key === timeSystem.key);
 | 
			
		||||
 | 
			
		||||
        if (domain !== undefined) {
 | 
			
		||||
            // 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);
 | 
			
		||||
 | 
			
		||||
        this.parseTime = (datum) => {
 | 
			
		||||
@@ -437,8 +402,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();
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
@@ -121,18 +121,6 @@ define([
 | 
			
		||||
        return _.sortBy(matchingMetadata, ...iteratees);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * check out of a given metadata has array values
 | 
			
		||||
     */
 | 
			
		||||
    TelemetryMetadataManager.prototype.isArrayValue = function (metadata) {
 | 
			
		||||
        const regex = /\[\]$/g;
 | 
			
		||||
        if (!metadata.format && !metadata.formatString) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return (metadata.format || metadata.formatString).match(regex) !== null;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    TelemetryMetadataManager.prototype.getFilterableValues = function () {
 | 
			
		||||
        return this.valueMetadatas.filter(metadatum => metadatum.filters && metadatum.filters.length > 0);
 | 
			
		||||
    };
 | 
			
		||||
@@ -150,7 +138,7 @@ define([
 | 
			
		||||
            valueMetadata = this.values()[0];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return valueMetadata;
 | 
			
		||||
        return valueMetadata.key;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return TelemetryMetadataManager;
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user