mirror of
https://github.com/nasa/openmct.git
synced 2025-07-01 21:01:11 +00:00
Compare commits
17 Commits
git-error-
...
fix-github
Author | SHA1 | Date | |
---|---|---|---|
521202f492 | |||
6955801834 | |||
872d0d2e06 | |||
f7effe8964 | |||
c94daac241 | |||
35e29e73ed | |||
ad44cd8062 | |||
ae553e2b82 | |||
bc6856f0f1 | |||
8eba1a83a3 | |||
1b187241dc | |||
3f6152c6ff | |||
4e32637469 | |||
722690ca15 | |||
eb7da39ceb | |||
8896a0dcfa | |||
3de70079dd |
@ -2,7 +2,7 @@ version: 2.1
|
|||||||
executors:
|
executors:
|
||||||
pw-focal-development:
|
pw-focal-development:
|
||||||
docker:
|
docker:
|
||||||
- image: mcr.microsoft.com/playwright:v1.21.1-focal
|
- image: mcr.microsoft.com/playwright:v1.19.2-focal
|
||||||
environment:
|
environment:
|
||||||
NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed
|
NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed
|
||||||
parameters:
|
parameters:
|
||||||
@ -23,7 +23,7 @@ commands:
|
|||||||
- node/install:
|
- node/install:
|
||||||
install-npm: true
|
install-npm: true
|
||||||
node-version: << parameters.node-version >>
|
node-version: << parameters.node-version >>
|
||||||
- run: npm install --prefer-offline --no-audit --progress=false
|
- run: npm install
|
||||||
restore_cache_cmd:
|
restore_cache_cmd:
|
||||||
description: "Custom command for restoring cache with the ability to bust cache. When BUST_CACHE is set to true, jobs will not restore cache"
|
description: "Custom command for restoring cache with the ability to bust cache. When BUST_CACHE is set to true, jobs will not restore cache"
|
||||||
parameters:
|
parameters:
|
||||||
@ -31,7 +31,7 @@ commands:
|
|||||||
type: string
|
type: string
|
||||||
steps:
|
steps:
|
||||||
- when:
|
- when:
|
||||||
condition:
|
condition:
|
||||||
equal: [false, << pipeline.parameters.BUST_CACHE >> ]
|
equal: [false, << pipeline.parameters.BUST_CACHE >> ]
|
||||||
steps:
|
steps:
|
||||||
- restore_cache:
|
- restore_cache:
|
||||||
@ -41,7 +41,7 @@ commands:
|
|||||||
parameters:
|
parameters:
|
||||||
node-version:
|
node-version:
|
||||||
type: string
|
type: string
|
||||||
steps:
|
steps:
|
||||||
- save_cache:
|
- save_cache:
|
||||||
key: deps-{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}-{{ checksum ".circleci/config.yml" }}
|
key: deps-{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}-{{ checksum ".circleci/config.yml" }}
|
||||||
paths:
|
paths:
|
||||||
@ -61,10 +61,10 @@ commands:
|
|||||||
upload_code_covio:
|
upload_code_covio:
|
||||||
description: "Command to upload code coverage reports to codecov.io"
|
description: "Command to upload code coverage reports to codecov.io"
|
||||||
steps:
|
steps:
|
||||||
- run: curl -Os https://uploader.codecov.io/latest/linux/codecov;chmod +x codecov;./codecov
|
- run: curl -Os https://uploader.codecov.io/latest/linux/codecov;chmod +x codecov;./codecov
|
||||||
orbs:
|
orbs:
|
||||||
node: circleci/node@4.9.0
|
node: circleci/node@4.9.0
|
||||||
browser-tools: circleci/browser-tools@1.3.0
|
browser-tools: circleci/browser-tools@1.2.3
|
||||||
jobs:
|
jobs:
|
||||||
npm-audit:
|
npm-audit:
|
||||||
parameters:
|
parameters:
|
||||||
@ -101,7 +101,7 @@ jobs:
|
|||||||
equal: [ "FirefoxESR", <<parameters.browser>> ]
|
equal: [ "FirefoxESR", <<parameters.browser>> ]
|
||||||
steps:
|
steps:
|
||||||
- browser-tools/install-firefox:
|
- 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:
|
- when:
|
||||||
condition:
|
condition:
|
||||||
equal: [ "FirefoxHeadless", <<parameters.browser>> ]
|
equal: [ "FirefoxHeadless", <<parameters.browser>> ]
|
||||||
@ -128,52 +128,36 @@ jobs:
|
|||||||
suite:
|
suite:
|
||||||
type: string
|
type: string
|
||||||
executor: pw-focal-development
|
executor: pw-focal-development
|
||||||
parallelism: 4
|
|
||||||
steps:
|
steps:
|
||||||
- build_and_install:
|
- build_and_install:
|
||||||
node-version: <<parameters.node-version>>
|
node-version: <<parameters.node-version>>
|
||||||
- run: SHARD="$((${CIRCLE_NODE_INDEX}+1))"; npm run test:e2e:<<parameters.suite>> -- --shard=${SHARD}/${CIRCLE_NODE_TOTAL}
|
- run: npx playwright install
|
||||||
|
- run: npm run test:e2e:<<parameters.suite>>
|
||||||
- store_test_results:
|
- store_test_results:
|
||||||
path: test-results/results.xml
|
path: test-results/results.xml
|
||||||
- store_artifacts:
|
- store_artifacts:
|
||||||
path: test-results
|
path: test-results
|
||||||
- generate_and_store_version_and_filesystem_artifacts
|
- generate_and_store_version_and_filesystem_artifacts
|
||||||
perf-test:
|
|
||||||
parameters:
|
|
||||||
node-version:
|
|
||||||
type: string
|
|
||||||
executor: pw-focal-development
|
|
||||||
steps:
|
|
||||||
- build_and_install:
|
|
||||||
node-version: <<parameters.node-version>>
|
|
||||||
- run: npm run test:perf
|
|
||||||
- store_test_results:
|
|
||||||
path: test-results/results.xml
|
|
||||||
- store_artifacts:
|
|
||||||
path: test-results
|
|
||||||
- generate_and_store_version_and_filesystem_artifacts
|
|
||||||
workflows:
|
workflows:
|
||||||
overall-circleci-commit-status: #These jobs run on every commit
|
overall-circleci-commit-status: #These jobs run on every commit
|
||||||
jobs:
|
jobs:
|
||||||
- lint:
|
- lint:
|
||||||
name: node16-lint
|
name: node16-lint
|
||||||
node-version: lts/gallium
|
node-version: lts/gallium
|
||||||
- unit-test:
|
- unit-test:
|
||||||
name: node14-chrome
|
name: node14-chrome
|
||||||
node-version: lts/fermium
|
node-version: lts/fermium
|
||||||
browser: ChromeHeadless
|
browser: ChromeHeadless
|
||||||
post-steps:
|
post-steps:
|
||||||
- upload_code_covio
|
- upload_code_covio
|
||||||
- unit-test:
|
- unit-test:
|
||||||
name: node18-chrome
|
name: node16-chrome
|
||||||
node-version: "18"
|
node-version: lts/gallium
|
||||||
browser: ChromeHeadless
|
browser: ChromeHeadless
|
||||||
- e2e-test:
|
- e2e-test:
|
||||||
name: e2e-ci
|
name: e2e-ci
|
||||||
node-version: lts/gallium
|
node-version: lts/gallium
|
||||||
suite: ci
|
suite: ci
|
||||||
- perf-test:
|
|
||||||
node-version: lts/gallium
|
|
||||||
the-nightly: #These jobs do not run on PRs, but against master at night
|
the-nightly: #These jobs do not run on PRs, but against master at night
|
||||||
jobs:
|
jobs:
|
||||||
- unit-test:
|
- unit-test:
|
||||||
@ -192,10 +176,6 @@ workflows:
|
|||||||
name: node16-chrome-nightly
|
name: node16-chrome-nightly
|
||||||
node-version: lts/gallium
|
node-version: lts/gallium
|
||||||
browser: ChromeHeadless
|
browser: ChromeHeadless
|
||||||
- unit-test:
|
|
||||||
name: node18-chrome
|
|
||||||
node-version: "18"
|
|
||||||
browser: ChromeHeadless
|
|
||||||
- npm-audit:
|
- npm-audit:
|
||||||
node-version: lts/gallium
|
node-version: lts/gallium
|
||||||
- e2e-test:
|
- e2e-test:
|
||||||
|
@ -29,7 +29,6 @@ module.exports = {
|
|||||||
"you-dont-need-lodash-underscore/omit": "off",
|
"you-dont-need-lodash-underscore/omit": "off",
|
||||||
"you-dont-need-lodash-underscore/throttle": "off",
|
"you-dont-need-lodash-underscore/throttle": "off",
|
||||||
"you-dont-need-lodash-underscore/flatten": "off",
|
"you-dont-need-lodash-underscore/flatten": "off",
|
||||||
"you-dont-need-lodash-underscore/get": "off",
|
|
||||||
"no-bitwise": "error",
|
"no-bitwise": "error",
|
||||||
"curly": "error",
|
"curly": "error",
|
||||||
"eqeqeq": "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
|
#### Environment
|
||||||
<!--- If encountered on local machine, execute the following:
|
<!--- 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 -->
|
* Open MCT Version: <!--- date of build, version, or SHA -->
|
||||||
* Deployment Type: <!--- npm dev? VIPER Dev? openmct-yamcs? -->
|
* Deployment Type: <!--- npm dev? VIPER Dev? openmct-yamcs? -->
|
||||||
* OS:
|
* OS:
|
||||||
@ -40,8 +40,6 @@ assignees: ''
|
|||||||
- [ ] Is there a workaround available?
|
- [ ] Is there a workaround available?
|
||||||
- [ ] Does this impact a critical component?
|
- [ ] Does this impact a critical component?
|
||||||
- [ ] Is this just a visual bug with no functional impact?
|
- [ ] 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
|
#### Additional Information
|
||||||
<!--- Include any screenshots, gifs, or logs which will expedite triage -->
|
<!--- 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?
|
* [ ] Unit tests included and/or updated with changes?
|
||||||
* [ ] Command line build passes?
|
* [ ] Command line build passes?
|
||||||
* [ ] Has this been smoke tested?
|
* [ ] 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
|
### Reviewer Checklist
|
||||||
|
|
||||||
|
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
@ -32,12 +32,12 @@ jobs:
|
|||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v2
|
uses: github/codeql-action/init@v1
|
||||||
with:
|
with:
|
||||||
languages: javascript
|
languages: javascript
|
||||||
|
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v2
|
uses: github/codeql-action/autobuild@v1
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v2
|
uses: github/codeql-action/analyze@v1
|
||||||
|
16
.github/workflows/e2e-pr.yml
vendored
16
.github/workflows/e2e-pr.yml
vendored
@ -8,7 +8,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
e2e-full:
|
e2e-full:
|
||||||
if: ${{ github.event.label.name == 'pr:e2e' }}
|
if: ${{ github.event_name == 'pull_request' && github.event.action == 'labeled' && github.event.label.name == 'pr:e2e' }} || ${{ github.event_name == 'pull_request' && github.event.action == 'opened' }}
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
@ -30,11 +30,21 @@ jobs:
|
|||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: '16'
|
node-version: '16'
|
||||||
- run: npx playwright@1.21.1 install
|
check-latest: true
|
||||||
|
- name: Cache node modules
|
||||||
|
uses: actions/cache@v3
|
||||||
|
env:
|
||||||
|
cache-name: cache-node-modules
|
||||||
|
with:
|
||||||
|
path: ~/.npm
|
||||||
|
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
|
||||||
- run: npm install
|
- run: npm install
|
||||||
|
- run: npx playwright install
|
||||||
- run: npm run test:e2e:full
|
- run: npm run test:e2e:full
|
||||||
- name: Archive test results
|
- name: Archive test results
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
path: test-results
|
path: test-results
|
||||||
- name: Test success
|
- name: Test success
|
||||||
|
14
.github/workflows/e2e-visual.yml
vendored
14
.github/workflows/e2e-visual.yml
vendored
@ -10,15 +10,25 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
e2e-visual:
|
e2e-visual:
|
||||||
if: ${{ github.event.label.name == 'pr:visual' }} || ${{ github.event.workflow_dispatch }} || ${{ github.event.schedule }}
|
if: ${{ github.event.label.name == 'pr:visual' }} || ${{ github.event.workflow_dispatch }} || ${{ github.event.schedule }} || ${{ github.event_name == 'pull_request' && github.event.action == 'opened' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: '16'
|
node-version: '16'
|
||||||
- run: npx playwright@1.21.1 install
|
check-latest: true
|
||||||
|
- name: Cache node modules
|
||||||
|
uses: actions/cache@v3
|
||||||
|
env:
|
||||||
|
cache-name: cache-node-modules
|
||||||
|
with:
|
||||||
|
path: ~/.npm
|
||||||
|
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
|
||||||
- run: npm install
|
- run: npm install
|
||||||
|
- run: npx playwright install
|
||||||
- name: Run the e2e visual tests
|
- name: Run the e2e visual tests
|
||||||
run: npm run test:e2e:visual
|
run: npm run test:e2e:visual
|
||||||
env:
|
env:
|
||||||
|
21
.github/workflows/e2e.yml
vendored
21
.github/workflows/e2e.yml
vendored
@ -1,21 +0,0 @@
|
|||||||
name: "e2e"
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
version:
|
|
||||||
description: 'Which branch do you want to test?' # Limited to branch for now
|
|
||||||
required: false
|
|
||||||
default: 'master'
|
|
||||||
jobs:
|
|
||||||
e2e:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
ref: ${{ github.event.inputs.version }}
|
|
||||||
- uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: '16'
|
|
||||||
- run: npm install
|
|
||||||
- name: Run the e2e tests
|
|
||||||
run: npm run test:e2e:ci
|
|
5
.github/workflows/lighthouse.yml
vendored
5
.github/workflows/lighthouse.yml
vendored
@ -6,9 +6,6 @@ on:
|
|||||||
description: 'Which branch do you want to test?' # Limited to branch for now
|
description: 'Which branch do you want to test?' # Limited to branch for now
|
||||||
required: false
|
required: false
|
||||||
default: 'master'
|
default: 'master'
|
||||||
pull_request:
|
|
||||||
types:
|
|
||||||
- labeled
|
|
||||||
jobs:
|
jobs:
|
||||||
lighthouse-pr:
|
lighthouse-pr:
|
||||||
if: ${{ github.event.label.name == 'pr:lighthouse' }}
|
if: ${{ github.event.label.name == 'pr:lighthouse' }}
|
||||||
@ -56,6 +53,7 @@ jobs:
|
|||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: '16'
|
node-version: '16'
|
||||||
|
check-latest: true
|
||||||
- name: Cache node modules
|
- name: Cache node modules
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v2
|
||||||
env:
|
env:
|
||||||
@ -82,6 +80,7 @@ jobs:
|
|||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: '16'
|
node-version: '16'
|
||||||
|
check-latest: true
|
||||||
- name: Cache node modules
|
- name: Cache node modules
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v3
|
||||||
env:
|
env:
|
||||||
|
1
.github/workflows/npm-prerelease.yml
vendored
1
.github/workflows/npm-prerelease.yml
vendored
@ -27,6 +27,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: 16
|
node-version: 16
|
||||||
registry-url: https://registry.npmjs.org/
|
registry-url: https://registry.npmjs.org/
|
||||||
|
check-latest: true
|
||||||
- run: npm install
|
- run: npm install
|
||||||
- run: npm publish --access public --tag unstable
|
- run: npm publish --access public --tag unstable
|
||||||
env:
|
env:
|
||||||
|
15
.github/workflows/pr-platform.yml
vendored
15
.github/workflows/pr-platform.yml
vendored
@ -12,13 +12,14 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os:
|
os:
|
||||||
- ubuntu-latest
|
- ubuntu-20.04 #MMOC Ubuntu version
|
||||||
- macos-latest
|
- macos-latest
|
||||||
|
- macos-10.15
|
||||||
- windows-latest
|
- windows-latest
|
||||||
node_version:
|
node_version:
|
||||||
- 14
|
- 14
|
||||||
- 16
|
- 16
|
||||||
- 18
|
- 17
|
||||||
architecture:
|
architecture:
|
||||||
- x64
|
- x64
|
||||||
name: Node ${{ matrix.node_version }} - ${{ matrix.architecture }} on ${{ matrix.os }}
|
name: Node ${{ matrix.node_version }} - ${{ matrix.architecture }} on ${{ matrix.os }}
|
||||||
@ -29,6 +30,16 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node_version }}
|
node-version: ${{ matrix.node_version }}
|
||||||
architecture: ${{ matrix.architecture }}
|
architecture: ${{ matrix.architecture }}
|
||||||
|
check-latest: true
|
||||||
|
- name: Cache node modules
|
||||||
|
uses: actions/cache@v3
|
||||||
|
env:
|
||||||
|
cache-name: cache-node-modules
|
||||||
|
with:
|
||||||
|
path: ~/.npm
|
||||||
|
key: ${{ runner.os }}-${{ matrix.node_version }}--build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-${{ matrix.node_version }}--build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }}
|
||||||
- run: npm install
|
- run: npm install
|
||||||
- run: npm test
|
- run: npm test
|
||||||
- run: npm run lint -- --quiet
|
- run: npm run lint -- --quiet
|
||||||
|
@ -1,12 +1,4 @@
|
|||||||
/* eslint-disable no-undef */
|
/* eslint-disable no-undef */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
"extends": ["plugin:playwright/playwright-test"],
|
"extends": ["plugin:playwright/playwright-test"]
|
||||||
"overrides": [
|
|
||||||
{
|
|
||||||
"files": ["tests/visual/*.spec.js"],
|
|
||||||
"rules": {
|
|
||||||
"playwright/no-wait-for-timeout": "off"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
};
|
||||||
|
@ -1,41 +0,0 @@
|
|||||||
/* eslint-disable no-undef */
|
|
||||||
|
|
||||||
// This file extends the base functionality of the playwright test framework
|
|
||||||
const base = require('@playwright/test');
|
|
||||||
const { expect } = require('@playwright/test');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.test = base.test.extend({
|
|
||||||
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,41 +2,38 @@
|
|||||||
// playwright.config.js
|
// playwright.config.js
|
||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
const { devices } = require('@playwright/test');
|
const { devices } = require('@playwright/test');
|
||||||
|
|
||||||
/** @type {import('@playwright/test').PlaywrightTestConfig} */
|
/** @type {import('@playwright/test').PlaywrightTestConfig} */
|
||||||
const config = {
|
const config = {
|
||||||
retries: 1,
|
retries: 2,
|
||||||
testDir: 'tests',
|
testDir: 'tests',
|
||||||
testIgnore: '**/*.perf.spec.js', //Ignore performance tests and define in playwright-perfromance.config.js
|
timeout: 90 * 1000,
|
||||||
timeout: 60 * 1000,
|
|
||||||
webServer: {
|
webServer: {
|
||||||
command: 'npm run start',
|
command: 'npm run start',
|
||||||
port: 8080,
|
port: 8080,
|
||||||
timeout: 200 * 1000,
|
timeout: 200 * 1000,
|
||||||
reuseExistingServer: !process.env.CI
|
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
|
workers: 2, //Limit to 2 for CircleCI Agent
|
||||||
use: {
|
use: {
|
||||||
baseURL: 'http://localhost:8080/',
|
baseURL: 'http://localhost:8080/',
|
||||||
headless: true,
|
headless: true,
|
||||||
ignoreHTTPSErrors: true,
|
ignoreHTTPSErrors: true,
|
||||||
screenshot: 'only-on-failure',
|
screenshot: 'on',
|
||||||
trace: 'on-first-retry',
|
trace: 'on',
|
||||||
video: 'on-first-retry'
|
video: 'on'
|
||||||
},
|
},
|
||||||
projects: [
|
projects: [
|
||||||
{
|
{
|
||||||
name: 'chrome',
|
name: 'chrome',
|
||||||
use: {
|
use: {
|
||||||
browserName: 'chromium'
|
browserName: 'chromium',
|
||||||
|
...devices['Desktop Chrome']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'MMOC',
|
name: 'MMOC',
|
||||||
grepInvert: /@snapshot/,
|
|
||||||
use: {
|
use: {
|
||||||
browserName: 'chromium',
|
browserName: 'chromium',
|
||||||
viewport: {
|
viewport: {
|
||||||
@ -55,11 +52,8 @@ const config = {
|
|||||||
],
|
],
|
||||||
reporter: [
|
reporter: [
|
||||||
['list'],
|
['list'],
|
||||||
['html', {
|
|
||||||
open: 'never',
|
|
||||||
outputFolder: '../test-results/html/'
|
|
||||||
}],
|
|
||||||
['junit', { outputFile: 'test-results/results.xml' }],
|
['junit', { outputFile: 'test-results/results.xml' }],
|
||||||
|
['allure-playwright'],
|
||||||
['github']
|
['github']
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
@ -2,14 +2,12 @@
|
|||||||
// playwright.config.js
|
// playwright.config.js
|
||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
const { devices } = require('@playwright/test');
|
const { devices } = require('@playwright/test');
|
||||||
|
|
||||||
/** @type {import('@playwright/test').PlaywrightTestConfig} */
|
/** @type {import('@playwright/test').PlaywrightTestConfig} */
|
||||||
const config = {
|
const config = {
|
||||||
retries: 0,
|
retries: 0,
|
||||||
testDir: 'tests',
|
testDir: 'tests',
|
||||||
testIgnore: '**/*.perf.spec.js',
|
|
||||||
timeout: 30 * 1000,
|
timeout: 30 * 1000,
|
||||||
webServer: {
|
webServer: {
|
||||||
command: 'npm run start',
|
command: 'npm run start',
|
||||||
@ -23,20 +21,20 @@ const config = {
|
|||||||
baseURL: 'http://localhost:8080/',
|
baseURL: 'http://localhost:8080/',
|
||||||
headless: false,
|
headless: false,
|
||||||
ignoreHTTPSErrors: true,
|
ignoreHTTPSErrors: true,
|
||||||
screenshot: 'only-on-failure',
|
screenshot: 'on',
|
||||||
trace: 'retain-on-failure',
|
trace: 'on',
|
||||||
video: 'retain-on-failure'
|
video: 'on'
|
||||||
},
|
},
|
||||||
projects: [
|
projects: [
|
||||||
{
|
{
|
||||||
name: 'chrome',
|
name: 'chrome',
|
||||||
use: {
|
use: {
|
||||||
browserName: 'chromium'
|
browserName: 'chromium',
|
||||||
|
...devices['Desktop Chrome']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'MMOC',
|
name: 'MMOC',
|
||||||
grepInvert: /@snapshot/,
|
|
||||||
use: {
|
use: {
|
||||||
browserName: 'chromium',
|
browserName: 'chromium',
|
||||||
viewport: {
|
viewport: {
|
||||||
@ -55,10 +53,7 @@ const config = {
|
|||||||
],
|
],
|
||||||
reporter: [
|
reporter: [
|
||||||
['list'],
|
['list'],
|
||||||
['html', {
|
['allure-playwright']
|
||||||
open: 'on-failure',
|
|
||||||
outputFolder: '../test-results'
|
|
||||||
}]
|
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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
|
|
||||||
testDir: 'tests/performance/',
|
|
||||||
timeout: 60 * 1000,
|
|
||||||
workers: 1, //Only run in serial with 1 worker
|
|
||||||
webServer: {
|
|
||||||
command: 'npm run start',
|
|
||||||
port: 8080,
|
|
||||||
timeout: 200 * 1000,
|
|
||||||
reuseExistingServer: !process.env.CI
|
|
||||||
},
|
|
||||||
use: {
|
|
||||||
browserName: "chromium",
|
|
||||||
baseURL: 'http://localhost:8080/',
|
|
||||||
headless: Boolean(process.env.CI), //Only if running locally
|
|
||||||
ignoreHTTPSErrors: true,
|
|
||||||
screenshot: 'off',
|
|
||||||
trace: '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,10 +4,10 @@
|
|||||||
|
|
||||||
/** @type {import('@playwright/test').PlaywrightTestConfig} */
|
/** @type {import('@playwright/test').PlaywrightTestConfig} */
|
||||||
const config = {
|
const config = {
|
||||||
retries: 0, // visual tests should never retry due to snapshot comparison errors
|
retries: 0,
|
||||||
testDir: 'tests/visual',
|
testDir: 'tests',
|
||||||
timeout: 90 * 1000,
|
timeout: 90 * 1000,
|
||||||
workers: 1, // visual tests should never run in parallel due to test pollution
|
workers: 1,
|
||||||
webServer: {
|
webServer: {
|
||||||
command: 'npm run start',
|
command: 'npm run start',
|
||||||
port: 8080,
|
port: 8080,
|
||||||
@ -17,7 +17,7 @@ const config = {
|
|||||||
use: {
|
use: {
|
||||||
browserName: "chromium",
|
browserName: "chromium",
|
||||||
baseURL: 'http://localhost:8080/',
|
baseURL: 'http://localhost:8080/',
|
||||||
headless: true, // this needs to remain headless to avoid visual changes due to GPU
|
headless: true,
|
||||||
ignoreHTTPSErrors: true,
|
ignoreHTTPSErrors: true,
|
||||||
screenshot: 'on',
|
screenshot: 'on',
|
||||||
trace: 'off',
|
trace: 'off',
|
||||||
@ -25,7 +25,8 @@ const config = {
|
|||||||
},
|
},
|
||||||
reporter: [
|
reporter: [
|
||||||
['list'],
|
['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,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.
|
This test suite is dedicated to tests which verify branding related components.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { test } = require('../fixtures.js');
|
const { test, expect } = require('@playwright/test');
|
||||||
const { expect } = require('@playwright/test');
|
|
||||||
|
|
||||||
test.describe('Branding tests', () => {
|
test.describe('Branding tests', () => {
|
||||||
test('About Modal launches with basic branding properties', async ({ page }) => {
|
test('About Modal launches with basic branding properties', async ({ page }) => {
|
||||||
@ -58,6 +57,6 @@ test.describe('Branding tests', () => {
|
|||||||
page.waitForEvent('popup'),
|
page.waitForEvent('popup'),
|
||||||
page.locator('text=click here for third party licensing information').click()
|
page.locator('text=click here for third party licensing information').click()
|
||||||
]);
|
]);
|
||||||
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.
|
This test suite is dedicated to tests which verify the basic operations surrounding the example event generator.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { test } = require('../../fixtures.js');
|
const { test, expect } = require('@playwright/test');
|
||||||
const { expect } = require('@playwright/test');
|
|
||||||
|
|
||||||
test.describe('Example Event Generator Operations', () => {
|
test.describe('Example Event Generator Operations', () => {
|
||||||
test('Can create example event generator with a name', async ({ page }) => {
|
test('Can create example event generator with a name', async ({ page }) => {
|
||||||
|
@ -24,8 +24,7 @@
|
|||||||
This test suite is dedicated to tests which verify the basic operations surrounding conditionSets.
|
This test suite is dedicated to tests which verify the basic operations surrounding conditionSets.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { test } = require('../../../fixtures.js');
|
const { test, expect } = require('@playwright/test');
|
||||||
const { expect } = require('@playwright/test');
|
|
||||||
|
|
||||||
test.describe('Sine Wave Generator', () => {
|
test.describe('Sine Wave Generator', () => {
|
||||||
test('Create new Sine Wave Generator Object and validate create Form Logic', async ({ page }) => {
|
test('Create new Sine Wave Generator Object and validate create Form Logic', async ({ page }) => {
|
||||||
|
@ -24,123 +24,19 @@
|
|||||||
This test suite is dedicated to tests which verify the basic operations surrounding moving objects.
|
This test suite is dedicated to tests which verify the basic operations surrounding moving objects.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { test } = require('../fixtures.js');
|
const { test, expect } = require('@playwright/test');
|
||||||
const { expect } = require('@playwright/test');
|
|
||||||
|
|
||||||
test.describe('Move item tests', () => {
|
test.describe('Move item tests', () => {
|
||||||
test('Create a basic object and verify that it can be moved to another folder', async ({ page }) => {
|
test.fixme('Create a basic object and verify that it can be moved to another Folder', async ({ page }) => {
|
||||||
// Go to Open MCT
|
//Create and save Folder
|
||||||
await page.goto('/');
|
//Create and save Domain Object
|
||||||
|
//Verify that the newly created domain object can be moved to Folder from Step 1.
|
||||||
// Create a new folder in the root my items folder
|
//Verify that newly moved object appears in the correct point in Tree
|
||||||
let folder1 = "Folder1";
|
//Verify that newly moved object appears correctly in Inspector panel
|
||||||
await page.locator('button:has-text("Create")').click();
|
|
||||||
await page.locator('li.icon-folder').click();
|
|
||||||
|
|
||||||
await page.locator('text=Properties Title Notes >> input[type="text"]').click();
|
|
||||||
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder1);
|
|
||||||
|
|
||||||
// Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184
|
|
||||||
await page.click('form[name="mctForm"] a:has-text("My Items")');
|
|
||||||
|
|
||||||
await Promise.all([
|
|
||||||
page.waitForNavigation(),
|
|
||||||
page.locator('text=OK').click(),
|
|
||||||
page.waitForSelector('.c-message-banner__message')
|
|
||||||
]);
|
|
||||||
//Wait until Save Banner is gone
|
|
||||||
await page.locator('.c-message-banner__close-button').click();
|
|
||||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
|
||||||
|
|
||||||
// Create another folder with a new name at default location, which is currently inside Folder 1
|
|
||||||
let folder2 = "Folder2";
|
|
||||||
await page.locator('button:has-text("Create")').click();
|
|
||||||
await page.locator('li.icon-folder').click();
|
|
||||||
await page.locator('text=Properties Title Notes >> input[type="text"]').click();
|
|
||||||
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder2);
|
|
||||||
|
|
||||||
// Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184
|
|
||||||
await page.click('form[name="mctForm"] a:has-text("My Items")');
|
|
||||||
|
|
||||||
await Promise.all([
|
|
||||||
page.waitForNavigation(),
|
|
||||||
page.locator('text=OK').click(),
|
|
||||||
page.waitForSelector('.c-message-banner__message')
|
|
||||||
]);
|
|
||||||
//Wait until Save Banner is gone
|
|
||||||
await page.locator('.c-message-banner__close-button').click();
|
|
||||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
|
||||||
|
|
||||||
// Move Folder 2 from Folder 1 to My Items
|
|
||||||
await page.locator('text=Open MCT My Items >> span').nth(3).click();
|
|
||||||
await page.locator('.c-tree__scrollable div div:nth-child(2) .c-tree__item .c-tree__item__view-control').click();
|
|
||||||
|
|
||||||
await page.locator(`a:has-text("${folder2}")`).click({
|
|
||||||
button: 'right'
|
|
||||||
});
|
|
||||||
await page.locator('li.icon-move').click();
|
|
||||||
await page.locator('form[name="mctForm"] >> text=My Items').click();
|
|
||||||
|
|
||||||
await page.locator('text=OK').click();
|
|
||||||
|
|
||||||
// Expect that Folder 2 is in My Items, the root folder
|
|
||||||
expect(page.locator(`text=My Items >> nth=0:has(text=${folder2})`)).toBeTruthy();
|
|
||||||
});
|
});
|
||||||
test('Create a basic object and verify that it cannot be moved to telemetry object without Composition Provider', async ({ page }) => {
|
test.fixme('Create a basic object and verify that it cannot be moved to object without Composition Provider', async ({ page }) => {
|
||||||
// Go to Open MCT
|
//Create and save Telemetry Object
|
||||||
await page.goto('/');
|
//Create and save Domain Object
|
||||||
|
//Verify that the newly created domain object cannot be moved to Telemetry Object from step 1.
|
||||||
// Create Telemetry Table
|
|
||||||
let telemetryTable = 'Test Telemetry Table';
|
|
||||||
await page.locator('button:has-text("Create")').click();
|
|
||||||
await page.locator('li:has-text("Telemetry Table")').click();
|
|
||||||
await page.locator('text=Properties Title Notes >> input[type="text"]').click();
|
|
||||||
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(telemetryTable);
|
|
||||||
|
|
||||||
// Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184
|
|
||||||
await page.click('form[name="mctForm"] a:has-text("My Items")');
|
|
||||||
|
|
||||||
await page.locator('text=OK').click();
|
|
||||||
|
|
||||||
// Finish editing and save Telemetry Table
|
|
||||||
await page.locator('.c-button--menu.c-button--major.icon-save').click();
|
|
||||||
await page.locator('text=Save and Finish Editing').click();
|
|
||||||
|
|
||||||
// Create New Folder Basic Domain Object
|
|
||||||
let folder = 'Test Folder';
|
|
||||||
await page.locator('button:has-text("Create")').click();
|
|
||||||
await page.locator('li:has-text("Folder")').click();
|
|
||||||
await page.locator('text=Properties Title Notes >> input[type="text"]').click();
|
|
||||||
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder);
|
|
||||||
|
|
||||||
// See if it's possible to put the folder in the Telemetry object during creation (Soft Assert)
|
|
||||||
await page.locator(`form[name="mctForm"] >> text=${telemetryTable}`).click();
|
|
||||||
let okButton = await page.locator('button.c-button.c-button--major:has-text("OK")');
|
|
||||||
let okButtonStateDisabled = await okButton.isDisabled();
|
|
||||||
expect.soft(okButtonStateDisabled).toBeTruthy();
|
|
||||||
|
|
||||||
// Continue test regardless of assertion and create it in My Items
|
|
||||||
await page.locator('form[name="mctForm"] >> text=My Items').click();
|
|
||||||
await page.locator('text=OK').click();
|
|
||||||
|
|
||||||
// Open My Items
|
|
||||||
await page.locator('text=Open MCT My Items >> span').nth(3).click();
|
|
||||||
|
|
||||||
// Select Folder Object and select Move from context menu
|
|
||||||
await Promise.all([
|
|
||||||
page.waitForNavigation(),
|
|
||||||
page.locator(`a:has-text("${folder}")`).click()
|
|
||||||
]);
|
|
||||||
await page.locator('.c-tree__item.is-navigated-object .c-tree__item__label .c-tree__item__type-icon').click({
|
|
||||||
button: 'right'
|
|
||||||
});
|
|
||||||
await page.locator('li.icon-move').click();
|
|
||||||
|
|
||||||
// See if it's possible to put the folder in the Telemetry object after creation
|
|
||||||
await page.locator('text=Location Open MCT My Items >> span').nth(3).click();
|
|
||||||
await page.locator(`form[name="mctForm"] >> text=${telemetryTable}`).click();
|
|
||||||
let okButton2 = await page.locator('button.c-button.c-button--major:has-text("OK")');
|
|
||||||
let okButtonStateDisabled2 = await okButton2.isDisabled();
|
|
||||||
expect(okButtonStateDisabled2).toBeTruthy();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,177 +0,0 @@
|
|||||||
/*****************************************************************************
|
|
||||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
|
||||||
* as represented by the Administrator of the National Aeronautics and Space
|
|
||||||
* Administration. All rights reserved.
|
|
||||||
*
|
|
||||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
* License for the specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*
|
|
||||||
* Open MCT includes source code licensed under additional open source
|
|
||||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
|
||||||
* this source code distribution or the Licensing information page available
|
|
||||||
* at runtime from the About dialog for additional information.
|
|
||||||
*****************************************************************************/
|
|
||||||
|
|
||||||
/*
|
|
||||||
This test suite is dedicated to performance tests to ensure that testability of performance
|
|
||||||
is not broken upstream on Open MCT. Any assumptions made downstream will be tested here
|
|
||||||
|
|
||||||
TODO:
|
|
||||||
- Update resolution of performance config
|
|
||||||
- Add Performance Observer on init to push all performance marks
|
|
||||||
- Move client CDP connection to before or to a fixture
|
|
||||||
-
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
const { test, expect } = require('@playwright/test');
|
|
||||||
|
|
||||||
const filePath = 'e2e/test-data/PerformanceDisplayLayout.json';
|
|
||||||
|
|
||||||
test.describe('Performance tests', () => {
|
|
||||||
test.beforeEach(async ({ page, browser }, testInfo) => {
|
|
||||||
// Go to baseURL
|
|
||||||
await page.goto('/', { waitUntil: 'networkidle' });
|
|
||||||
|
|
||||||
// Click a:has-text("My Items")
|
|
||||||
await page.locator('a:has-text("My Items")').click({
|
|
||||||
button: 'right'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Click text=Import from JSON
|
|
||||||
await page.locator('text=Import from JSON').click();
|
|
||||||
|
|
||||||
// Upload Performance Display Layout.json
|
|
||||||
await page.setInputFiles('#fileElem', filePath);
|
|
||||||
|
|
||||||
// Click text=OK
|
|
||||||
await page.locator('text=OK').click();
|
|
||||||
|
|
||||||
await expect(page.locator('a:has-text("Performance Display Layout Display Layout")')).toBeVisible();
|
|
||||||
|
|
||||||
//Create a Chrome Performance Timeline trace to store as a test artifact
|
|
||||||
console.log("\n==== Devtools: startTracing ====\n");
|
|
||||||
await browser.startTracing(page, {
|
|
||||||
path: `${testInfo.outputPath()}-trace.json`,
|
|
||||||
screenshots: true
|
|
||||||
});
|
|
||||||
});
|
|
||||||
test.afterEach(async ({ page, browser}) => {
|
|
||||||
console.log("\n==== Devtools: stopTracing ====\n");
|
|
||||||
await browser.stopTracing();
|
|
||||||
|
|
||||||
/* Measurement Section
|
|
||||||
/ The following section includes a block of performance measurements.
|
|
||||||
*/
|
|
||||||
//Get time difference between viewlarge actionability and evaluate time
|
|
||||||
await page.evaluate(() => (window.performance.measure("machine-time-difference", "viewlarge.start", "viewLarge.start.test")));
|
|
||||||
|
|
||||||
//Get StartTime
|
|
||||||
const startTime = await page.evaluate(() => window.performance.timing.navigationStart);
|
|
||||||
console.log('window.performance.timing.navigationStart', startTime);
|
|
||||||
|
|
||||||
//Get All Performance Marks
|
|
||||||
const getAllMarksJson = await page.evaluate(() => JSON.stringify(window.performance.getEntriesByType("mark")));
|
|
||||||
const getAllMarks = JSON.parse(getAllMarksJson);
|
|
||||||
console.log('window.performance.getEntriesByType("mark")', getAllMarks);
|
|
||||||
|
|
||||||
//Get All Performance Measures
|
|
||||||
const getAllMeasuresJson = await page.evaluate(() => JSON.stringify(window.performance.getEntriesByType("measure")));
|
|
||||||
const getAllMeasures = JSON.parse(getAllMeasuresJson);
|
|
||||||
console.log('window.performance.getEntriesByType("measure")', getAllMeasures);
|
|
||||||
|
|
||||||
});
|
|
||||||
/* The following test will navigate to a previously created Performance Display Layout and measure the
|
|
||||||
/ following metrics:
|
|
||||||
/ - ElementResourceTiming
|
|
||||||
/ - Interaction Timing
|
|
||||||
*/
|
|
||||||
test('Embedded View Large for Imagery is performant in Fixed Time', async ({ page, browser }) => {
|
|
||||||
const client = await page.context().newCDPSession(page);
|
|
||||||
// Tell the DevTools session to record performance metrics
|
|
||||||
// https://chromedevtools.github.io/devtools-protocol/tot/Performance/#method-getMetrics
|
|
||||||
await client.send('Performance.enable');
|
|
||||||
// Go to baseURL
|
|
||||||
await page.goto('/');
|
|
||||||
|
|
||||||
// Search Available after Launch
|
|
||||||
await page.locator('[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,8 +24,7 @@
|
|||||||
This test suite is dedicated to tests which verify the basic operations surrounding conditionSets.
|
This test suite is dedicated to tests which verify the basic operations surrounding conditionSets.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { test } = require('../../fixtures.js');
|
const { test, expect } = require('@playwright/test');
|
||||||
const { expect } = require('@playwright/test');
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
// https://github.com/nasa/openmct/issues/4323#issuecomment-1067282651
|
// https://github.com/nasa/openmct/issues/4323#issuecomment-1067282651
|
||||||
|
@ -24,10 +24,7 @@
|
|||||||
This test suite is dedicated to tests which verify the basic operations surrounding exportAsJSON.
|
This test suite is dedicated to tests which verify the basic operations surrounding exportAsJSON.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { test } = require('../../../fixtures.js');
|
const { test, expect } = require('@playwright/test');
|
||||||
// FIXME: Remove this eslint exception once tests are implemented
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
const { expect } = require('@playwright/test');
|
|
||||||
|
|
||||||
test.describe('ExportAsJSON', () => {
|
test.describe('ExportAsJSON', () => {
|
||||||
test.fixme('Create a basic object and verify that it can be exported as JSON from Tree', async ({ page }) => {
|
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.
|
This test suite is dedicated to tests which verify the basic operations surrounding importAsJSON.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { test } = require('../../../fixtures.js');
|
const { test, expect } = require('@playwright/test');
|
||||||
// FIXME: Remove this eslint exception once tests are implemented
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
const { expect } = require('@playwright/test');
|
|
||||||
|
|
||||||
test.describe('ExportAsJSON', () => {
|
test.describe('ExportAsJSON', () => {
|
||||||
test.fixme('Verify that domain object can be importAsJSON from Tree', async ({ page }) => {
|
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.
|
This test suite is dedicated to tests which verify the basic operations surrounding Clock.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { test } = require('../../../fixtures.js');
|
const { test, expect } = require('@playwright/test');
|
||||||
const { expect } = require('@playwright/test');
|
|
||||||
|
|
||||||
test.describe('Clock Generator', () => {
|
test.describe('Clock Generator', () => {
|
||||||
|
|
||||||
@ -46,22 +45,22 @@ test.describe('Clock Generator', () => {
|
|||||||
// Click .icon-arrow-down
|
// Click .icon-arrow-down
|
||||||
await page.locator('.icon-arrow-down').click();
|
await page.locator('.icon-arrow-down').click();
|
||||||
//verify if the autocomplete dropdown is visible
|
//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
|
// Click .icon-arrow-down
|
||||||
await page.locator('.icon-arrow-down').click();
|
await page.locator('.icon-arrow-down').click();
|
||||||
|
|
||||||
// Verify clicking on the autocomplete arrow collapses the dropdown
|
// 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
|
// 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
|
//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
|
// Verify clicking outside the autocomplete dropdown collapses it
|
||||||
await page.locator('text=Timezone').click();
|
await page.locator('text=Timezone').click();
|
||||||
// Verify clicking on the autocomplete arrow collapses the dropdown
|
// 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();
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -21,21 +21,13 @@
|
|||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
/*
|
/*
|
||||||
This test suite is dedicated to tests which verify the basic operations surrounding conditionSets. Note: this
|
This test suite is dedicated to tests which verify the basic operations surrounding conditionSets.
|
||||||
suite is sharing state between tests which is considered an anti-pattern. Implimenting in this way to
|
|
||||||
demonstrate some playwright for test developers. This pattern should not be re-used in other CRUD suites.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { test } = require('../../../fixtures.js');
|
const { test, expect } = require('@playwright/test');
|
||||||
const { expect } = require('@playwright/test');
|
|
||||||
|
|
||||||
let conditionSetUrl;
|
test.describe('Condition Set Operations', () => {
|
||||||
let getConditionSetIdentifierFromUrl;
|
test('Create new button `condition set` creates new condition object', async ({ page }) => {
|
||||||
|
|
||||||
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
|
//Go to baseURL
|
||||||
await page.goto('/', { waitUntil: 'networkidle' });
|
await page.goto('/', { waitUntil: 'networkidle' });
|
||||||
|
|
||||||
@ -43,139 +35,31 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
|
|||||||
await page.click('button:has-text("Create")');
|
await page.click('button:has-text("Create")');
|
||||||
|
|
||||||
// Click text=Condition Set
|
// Click text=Condition Set
|
||||||
await page.locator('li:has-text("Condition Set")').click();
|
await page.click('text=Condition Set');
|
||||||
|
|
||||||
// Click text=OK
|
// Click text=OK
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
page.waitForNavigation(),
|
page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine/dab945d4-5a84-480e-8180-222b4aa730fa?tc.mode=fixed&tc.startBound=1639696164435&tc.endBound=1639697964435&tc.timeSystem=utc&view=conditionSet.view' }*/),
|
||||||
page.click('text=OK')
|
page.click('text=OK')
|
||||||
]);
|
]);
|
||||||
|
|
||||||
//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.debug('getConditionSetIdentifierFromUrl ' + getConditionSetIdentifierFromUrl);
|
|
||||||
});
|
|
||||||
test.afterAll(async ({ browser }) => {
|
|
||||||
await browser.close();
|
|
||||||
});
|
|
||||||
//Load localStorage for subsequent tests
|
|
||||||
test.use({ storageState: './e2e/tests/recycled_storage.json' });
|
|
||||||
//Begin suite of tests again localStorage
|
|
||||||
test('Condition set object properties persist in main view and inspector', async ({ page }) => {
|
|
||||||
//Navigate to baseURL with injected localStorage
|
|
||||||
await page.goto(conditionSetUrl, { waitUntil: 'networkidle' });
|
|
||||||
|
|
||||||
//Assertions on loaded Condition Set in main view
|
|
||||||
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
|
|
||||||
|
|
||||||
//Assertions on loaded Condition Set in Inspector
|
|
||||||
await expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy;
|
|
||||||
|
|
||||||
//Reload Page
|
|
||||||
await Promise.all([
|
|
||||||
page.reload(),
|
|
||||||
page.waitForLoadState('networkidle')
|
|
||||||
]);
|
|
||||||
|
|
||||||
//Re-verify after reload
|
|
||||||
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
|
|
||||||
//Assertions on loaded Condition Set in Inspector
|
|
||||||
await expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy;
|
|
||||||
|
|
||||||
});
|
|
||||||
test('condition set object can be modified on @localStorage', async ({ page }) => {
|
|
||||||
await page.goto(conditionSetUrl, { waitUntil: 'networkidle' });
|
|
||||||
|
|
||||||
//Assertions on loaded Condition Set in main view
|
|
||||||
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
|
|
||||||
|
|
||||||
//Update the Condition Set properties
|
|
||||||
// Click Edit Button
|
|
||||||
await page.locator('text=Conditions View Snapshot >> button').nth(3).click();
|
|
||||||
|
|
||||||
//Edit Condition Set Name from main view
|
|
||||||
await page.locator('text=Unnamed Condition Set').first().fill('Renamed Condition Set');
|
|
||||||
await page.locator('text=Renamed Condition Set').first().press('Enter');
|
|
||||||
// Click Save Button
|
|
||||||
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
|
|
||||||
// Click Save and Finish Editing Option
|
|
||||||
await page.locator('text=Save and Finish Editing').click();
|
|
||||||
|
|
||||||
//Verify Main section reflects updated Name Property
|
|
||||||
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Renamed Condition Set');
|
|
||||||
|
|
||||||
// Verify Inspector properties
|
|
||||||
// Verify Inspector has updated Name property
|
|
||||||
await expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy();
|
|
||||||
// Verify Inspector Details has updated Name property
|
|
||||||
await expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy();
|
|
||||||
|
|
||||||
// Verify Tree reflects updated Name proprety
|
|
||||||
// Expand Tree
|
|
||||||
await page.locator('text=Open MCT My Items >> span >> nth=3').click();
|
|
||||||
// Verify Condition Set Object is renamed in Tree
|
|
||||||
await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
|
|
||||||
// Verify Search Tree reflects renamed Name property
|
|
||||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Renamed');
|
|
||||||
await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
|
|
||||||
|
|
||||||
//Reload Page
|
|
||||||
await Promise.all([
|
|
||||||
page.reload(),
|
|
||||||
page.waitForLoadState('networkidle')
|
|
||||||
]);
|
|
||||||
|
|
||||||
//Verify Main section reflects updated Name Property
|
|
||||||
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Renamed Condition Set');
|
|
||||||
|
|
||||||
// Verify Inspector properties
|
|
||||||
// Verify Inspector has updated Name property
|
|
||||||
await expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy();
|
|
||||||
// Verify Inspector Details has updated Name property
|
|
||||||
await expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy();
|
|
||||||
|
|
||||||
// Verify Tree reflects updated Name proprety
|
|
||||||
// Expand Tree
|
|
||||||
await page.locator('text=Open MCT My Items >> span >> nth=3').click();
|
|
||||||
// Verify Condition Set Object is renamed in Tree
|
|
||||||
await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
|
|
||||||
// Verify Search Tree reflects renamed Name property
|
|
||||||
await page.locator('[aria-label="OpenMCT Search"] 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' });
|
|
||||||
|
|
||||||
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") >> nth=0')).toBeVisible();
|
|
||||||
|
|
||||||
// Search for Unnamed Condition Set
|
|
||||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Unnamed Condition Set');
|
|
||||||
// Click Search Result
|
|
||||||
await page.locator('[aria-label="OpenMCT Search"] >> text=Unnamed Condition Set').first().click();
|
|
||||||
// Click hamburger button
|
|
||||||
await page.locator('[title="More options"]').click();
|
|
||||||
// Click text=Remove
|
|
||||||
await page.locator('text=Remove').click();
|
|
||||||
|
|
||||||
await page.locator('text=OK').click();
|
|
||||||
|
|
||||||
//Expect Unnamed Condition Set to be removed in Main View
|
|
||||||
const numberOfConditionSetsAtEnd = await page.locator('a:has-text("Unnamed Condition Set Condition Set")').count();
|
|
||||||
expect(numberOfConditionSetsAtEnd).toEqual(numberOfConditionSetsToStart - 1);
|
|
||||||
|
|
||||||
//Feature?
|
|
||||||
//Domain Object is still available by direct URL after delete
|
|
||||||
await page.goto(conditionSetUrl, { waitUntil: 'networkidle' });
|
|
||||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
|
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
|
||||||
|
});
|
||||||
|
test.fixme('condition set object properties exist', async ({ page }) => {
|
||||||
|
//Go to object created in step one
|
||||||
|
//Verify the Condition Set properties persist on Save
|
||||||
|
//Verify the Condition Set properties persist on page.reload()
|
||||||
|
});
|
||||||
|
test.fixme('condition set object can be modified', async ({ page }) => {
|
||||||
|
//Go to object created in step one
|
||||||
|
//Update the Condition Set properties
|
||||||
|
//Verify the Condition Set properties persist on Save
|
||||||
|
//Verify the Condition Set properties persist on page.reload()
|
||||||
|
});
|
||||||
|
test.fixme('condition set object can be deleted', async ({ page }) => {
|
||||||
|
//Go to object created in step one
|
||||||
|
//Verify that Condition Set object can be deleted
|
||||||
|
//Verify the Condition Set object does not exist in Tree
|
||||||
|
//Verify the Condition Set object does not exist with direct navigation to object's URL
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -24,14 +24,13 @@
|
|||||||
This test suite is dedicated to tests which verify the basic operations surrounding imagery,
|
This test suite is dedicated to tests which verify the basic operations surrounding imagery,
|
||||||
but only assume that example imagery is present.
|
but only assume that example imagery is present.
|
||||||
*/
|
*/
|
||||||
/* globals process */
|
|
||||||
|
|
||||||
const { test } = require('../../../fixtures.js');
|
const { test, expect } = require('@playwright/test');
|
||||||
const { expect } = require('@playwright/test');
|
|
||||||
|
|
||||||
test.describe('Example Imagery', () => {
|
test.describe('Example Imagery', () => {
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
|
page.on('console', msg => console.log(msg.text()))
|
||||||
//Go to baseURL
|
//Go to baseURL
|
||||||
await page.goto('/', { waitUntil: 'networkidle' });
|
await page.goto('/', { waitUntil: 'networkidle' });
|
||||||
|
|
||||||
@ -41,38 +40,32 @@ test.describe('Example Imagery', () => {
|
|||||||
// Click text=Example Imagery
|
// Click text=Example Imagery
|
||||||
await page.click('text=Example Imagery');
|
await page.click('text=Example Imagery');
|
||||||
|
|
||||||
// Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184
|
|
||||||
await page.click('form[name="mctForm"] a:has-text("My Items")');
|
|
||||||
|
|
||||||
// Click text=OK
|
// Click text=OK
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
page.waitForNavigation({waitUntil: 'networkidle'}),
|
page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine/dab945d4-5a84-480e-8180-222b4aa730fa?tc.mode=fixed&tc.startBound=1639696164435&tc.endBound=1639697964435&tc.timeSystem=utc&view=conditionSet.view' }*/),
|
||||||
page.click('text=OK'),
|
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 expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');
|
||||||
});
|
});
|
||||||
|
|
||||||
const backgroundImageSelector = '.c-imagery__main-image__background-image';
|
const backgroundImageSelector = '.c-imagery__main-image__background-image';
|
||||||
test('Can use Mouse Wheel to zoom in and out of latest image', async ({ page }) => {
|
test('Can use Mouse Wheel to zoom in and out of latest image', async ({ page }) => {
|
||||||
const bgImageLocator = page.locator(backgroundImageSelector);
|
const bgImageLocator = await page.locator(backgroundImageSelector);
|
||||||
const deltaYStep = 100; //equivalent to 1x zoom
|
const deltaYStep = 100; //equivalent to 1x zoom
|
||||||
await bgImageLocator.hover({trial: true});
|
await bgImageLocator.hover();
|
||||||
const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox();
|
const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox();
|
||||||
// zoom in
|
// zoom in
|
||||||
await bgImageLocator.hover({trial: true});
|
await bgImageLocator.hover();
|
||||||
await page.mouse.wheel(0, deltaYStep * 2);
|
await page.mouse.wheel(0, deltaYStep * 2);
|
||||||
// wait for zoom animation to finish
|
// wait for zoom animation to finish
|
||||||
await bgImageLocator.hover({trial: true});
|
await bgImageLocator.hover();
|
||||||
const imageMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox();
|
const imageMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox();
|
||||||
// zoom out
|
// zoom out
|
||||||
await bgImageLocator.hover({trial: true});
|
await bgImageLocator.hover();
|
||||||
await page.mouse.wheel(0, -deltaYStep);
|
await page.mouse.wheel(0, -deltaYStep);
|
||||||
// wait for zoom animation to finish
|
// wait for zoom animation to finish
|
||||||
await bgImageLocator.hover({trial: true});
|
await bgImageLocator.hover();
|
||||||
const imageMouseZoomedOut = await page.locator(backgroundImageSelector).boundingBox();
|
const imageMouseZoomedOut = await page.locator(backgroundImageSelector).boundingBox();
|
||||||
|
|
||||||
expect(imageMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height);
|
expect(imageMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height);
|
||||||
@ -84,14 +77,12 @@ test.describe('Example Imagery', () => {
|
|||||||
|
|
||||||
test('Can use alt+drag to move around image once zoomed in', async ({ page }) => {
|
test('Can use alt+drag to move around image once zoomed in', async ({ page }) => {
|
||||||
const deltaYStep = 100; //equivalent to 1x zoom
|
const deltaYStep = 100; //equivalent to 1x zoom
|
||||||
const panHotkey = process.platform === 'linux' ? ['Control', 'Alt'] : ['Alt'];
|
|
||||||
|
|
||||||
const bgImageLocator = page.locator(backgroundImageSelector);
|
|
||||||
await bgImageLocator.hover({trial: true});
|
|
||||||
|
|
||||||
|
const bgImageLocator = await page.locator(backgroundImageSelector);
|
||||||
|
await bgImageLocator.hover();
|
||||||
// zoom in
|
// zoom in
|
||||||
await page.mouse.wheel(0, deltaYStep * 2);
|
await page.mouse.wheel(0, deltaYStep * 2);
|
||||||
await bgImageLocator.hover({trial: true});
|
await bgImageLocator.hover();
|
||||||
const zoomedBoundingBox = await bgImageLocator.boundingBox();
|
const zoomedBoundingBox = await bgImageLocator.boundingBox();
|
||||||
const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;
|
const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;
|
||||||
const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
|
const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
|
||||||
@ -100,72 +91,63 @@ test.describe('Example Imagery', () => {
|
|||||||
// center the mouse pointer
|
// center the mouse pointer
|
||||||
await page.mouse.move(imageCenterX, imageCenterY);
|
await page.mouse.move(imageCenterX, imageCenterY);
|
||||||
|
|
||||||
//Get Diagnostic info about process environment
|
|
||||||
console.log('process.platform is ' + process.platform);
|
|
||||||
const getUA = await page.evaluate(() => navigator.userAgent);
|
|
||||||
console.log('navigator.userAgent ' + getUA);
|
|
||||||
// Pan Imagery Hints
|
|
||||||
const expectedAltText = process.platform === 'linux' ? 'Ctrl+Alt drag to pan' : 'Alt drag to pan';
|
|
||||||
const imageryHintsText = await page.locator('.c-imagery__hints').innerText();
|
|
||||||
expect(expectedAltText).toEqual(imageryHintsText);
|
|
||||||
|
|
||||||
// pan right
|
// pan right
|
||||||
await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
|
await page.keyboard.down('Alt');
|
||||||
await page.mouse.down();
|
await page.mouse.down();
|
||||||
await page.mouse.move(imageCenterX - 200, imageCenterY, 10);
|
await page.mouse.move(imageCenterX - 200, imageCenterY, 10);
|
||||||
await page.mouse.up();
|
await page.mouse.up();
|
||||||
await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
|
await page.keyboard.up('Alt');
|
||||||
const afterRightPanBoundingBox = await bgImageLocator.boundingBox();
|
const afterRightPanBoundingBox = await bgImageLocator.boundingBox();
|
||||||
expect(zoomedBoundingBox.x).toBeGreaterThan(afterRightPanBoundingBox.x);
|
expect(zoomedBoundingBox.x).toBeGreaterThan(afterRightPanBoundingBox.x);
|
||||||
|
|
||||||
// pan left
|
// pan left
|
||||||
await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
|
await page.keyboard.down('Alt');
|
||||||
await page.mouse.down();
|
await page.mouse.down();
|
||||||
await page.mouse.move(imageCenterX, imageCenterY, 10);
|
await page.mouse.move(imageCenterX, imageCenterY, 10);
|
||||||
await page.mouse.up();
|
await page.mouse.up();
|
||||||
await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
|
await page.keyboard.up('Alt');
|
||||||
const afterLeftPanBoundingBox = await bgImageLocator.boundingBox();
|
const afterLeftPanBoundingBox = await bgImageLocator.boundingBox();
|
||||||
expect(afterRightPanBoundingBox.x).toBeLessThan(afterLeftPanBoundingBox.x);
|
expect(afterRightPanBoundingBox.x).toBeLessThan(afterLeftPanBoundingBox.x);
|
||||||
|
|
||||||
// pan up
|
// pan up
|
||||||
await page.mouse.move(imageCenterX, imageCenterY);
|
await page.mouse.move(imageCenterX, imageCenterY);
|
||||||
await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
|
await page.keyboard.down('Alt');
|
||||||
await page.mouse.down();
|
await page.mouse.down();
|
||||||
await page.mouse.move(imageCenterX, imageCenterY + 200, 10);
|
await page.mouse.move(imageCenterX, imageCenterY + 200, 10);
|
||||||
await page.mouse.up();
|
await page.mouse.up();
|
||||||
await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
|
await page.keyboard.up('Alt');
|
||||||
const afterUpPanBoundingBox = await bgImageLocator.boundingBox();
|
const afterUpPanBoundingBox = await bgImageLocator.boundingBox();
|
||||||
expect(afterUpPanBoundingBox.y).toBeGreaterThan(afterLeftPanBoundingBox.y);
|
expect(afterUpPanBoundingBox.y).toBeGreaterThan(afterLeftPanBoundingBox.y);
|
||||||
|
|
||||||
// pan down
|
// pan down
|
||||||
await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
|
await page.keyboard.down('Alt');
|
||||||
await page.mouse.down();
|
await page.mouse.down();
|
||||||
await page.mouse.move(imageCenterX, imageCenterY - 200, 10);
|
await page.mouse.move(imageCenterX, imageCenterY - 200, 10);
|
||||||
await page.mouse.up();
|
await page.mouse.up();
|
||||||
await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
|
await page.keyboard.up('Alt');
|
||||||
const afterDownPanBoundingBox = await bgImageLocator.boundingBox();
|
const afterDownPanBoundingBox = await bgImageLocator.boundingBox();
|
||||||
expect(afterDownPanBoundingBox.y).toBeLessThan(afterUpPanBoundingBox.y);
|
expect(afterDownPanBoundingBox.y).toBeLessThan(afterUpPanBoundingBox.y);
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Can use + - buttons to zoom on the image', async ({ page }) => {
|
test('Can use + - buttons to zoom on the image', async ({ page }) => {
|
||||||
const bgImageLocator = page.locator(backgroundImageSelector);
|
const bgImageLocator = await page.locator(backgroundImageSelector);
|
||||||
await bgImageLocator.hover({trial: true});
|
await bgImageLocator.hover();
|
||||||
const zoomInBtn = page.locator('.t-btn-zoom-in').nth(0);
|
const zoomInBtn = await page.locator('.t-btn-zoom-in');
|
||||||
const zoomOutBtn = page.locator('.t-btn-zoom-out').nth(0);
|
const zoomOutBtn = await page.locator('.t-btn-zoom-out');
|
||||||
const initialBoundingBox = await bgImageLocator.boundingBox();
|
const initialBoundingBox = await bgImageLocator.boundingBox();
|
||||||
|
|
||||||
await zoomInBtn.click();
|
await zoomInBtn.click();
|
||||||
await zoomInBtn.click();
|
await zoomInBtn.click();
|
||||||
// wait for zoom animation to finish
|
// wait for zoom animation to finish
|
||||||
await bgImageLocator.hover({trial: true});
|
await bgImageLocator.hover();
|
||||||
const zoomedInBoundingBox = await bgImageLocator.boundingBox();
|
const zoomedInBoundingBox = await bgImageLocator.boundingBox();
|
||||||
expect(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
|
expect(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
|
||||||
expect(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);
|
expect(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);
|
||||||
|
|
||||||
await zoomOutBtn.click();
|
await zoomOutBtn.click();
|
||||||
// wait for zoom animation to finish
|
// wait for zoom animation to finish
|
||||||
await bgImageLocator.hover({trial: true});
|
await bgImageLocator.hover();
|
||||||
const zoomedOutBoundingBox = await bgImageLocator.boundingBox();
|
const zoomedOutBoundingBox = await bgImageLocator.boundingBox();
|
||||||
expect(zoomedOutBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height);
|
expect(zoomedOutBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height);
|
||||||
expect(zoomedOutBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width);
|
expect(zoomedOutBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width);
|
||||||
@ -173,28 +155,22 @@ test.describe('Example Imagery', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Can use the reset button to reset the image', async ({ page }) => {
|
test('Can use the reset button to reset the image', async ({ page }) => {
|
||||||
const bgImageLocator = page.locator(backgroundImageSelector);
|
const bgImageLocator = await page.locator(backgroundImageSelector);
|
||||||
// wait for zoom animation to finish
|
await bgImageLocator.hover();
|
||||||
await bgImageLocator.hover({trial: true});
|
const zoomInBtn = await page.locator('.t-btn-zoom-in');
|
||||||
|
const zoomResetBtn = await page.locator('.t-btn-zoom-reset');
|
||||||
const zoomInBtn = page.locator('.t-btn-zoom-in').nth(0);
|
|
||||||
const zoomResetBtn = page.locator('.t-btn-zoom-reset').nth(0);
|
|
||||||
const initialBoundingBox = await bgImageLocator.boundingBox();
|
const initialBoundingBox = await bgImageLocator.boundingBox();
|
||||||
|
|
||||||
await zoomInBtn.click();
|
await zoomInBtn.click();
|
||||||
// wait for zoom animation to finish
|
|
||||||
await bgImageLocator.hover({trial: true});
|
|
||||||
await zoomInBtn.click();
|
await zoomInBtn.click();
|
||||||
// wait for zoom animation to finish
|
// wait for zoom animation to finish
|
||||||
await bgImageLocator.hover({trial: true});
|
await bgImageLocator.hover();
|
||||||
|
|
||||||
const zoomedInBoundingBox = await bgImageLocator.boundingBox();
|
const zoomedInBoundingBox = await bgImageLocator.boundingBox();
|
||||||
expect.soft(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
|
expect.soft(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
|
||||||
expect.soft(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);
|
expect.soft(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);
|
||||||
|
|
||||||
await zoomResetBtn.click();
|
await zoomResetBtn.click();
|
||||||
// wait for zoom animation to finish
|
await bgImageLocator.hover();
|
||||||
await bgImageLocator.hover({trial: true});
|
|
||||||
|
|
||||||
const resetBoundingBox = await bgImageLocator.boundingBox();
|
const resetBoundingBox = await bgImageLocator.boundingBox();
|
||||||
expect.soft(resetBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height);
|
expect.soft(resetBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height);
|
||||||
@ -204,271 +180,38 @@ test.describe('Example Imagery', () => {
|
|||||||
expect(resetBoundingBox.width).toEqual(initialBoundingBox.width);
|
expect(resetBoundingBox.width).toEqual(initialBoundingBox.width);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Using the zoom features does not pause telemetry', async ({ page }) => {
|
//test('Can use Mouse Wheel to zoom in and out of previous image');
|
||||||
const bgImageLocator = page.locator(backgroundImageSelector);
|
//test('Can zoom into the latest image and the real-time/fixed-time imagery will pause');
|
||||||
const pausePlayButton = page.locator('.c-button.pause-play');
|
//test.skip('Can zoom into a previous image from thumbstrip in real-time or fixed-time');
|
||||||
// wait for zoom animation to finish
|
//test.skip('Clicking on the left arrow should pause the imagery and go to previous image');
|
||||||
await bgImageLocator.hover({trial: true});
|
//test.skip('If the imagery view is in pause mode, it should not be updated when new images come in');
|
||||||
|
//test.skip('If the imagery view is not in pause mode, it should be updated when new images come in');
|
||||||
// open the time conductor drop down
|
|
||||||
await page.locator('button:has-text("Fixed Timespan")').click();
|
|
||||||
// Click local clock
|
|
||||||
await page.locator('[data-testid="conductor-modeOption-realtime"]').click();
|
|
||||||
|
|
||||||
await expect.soft(pausePlayButton).not.toHaveClass(/is-paused/);
|
|
||||||
const zoomInBtn = page.locator('.t-btn-zoom-in').nth(0);
|
|
||||||
await zoomInBtn.click();
|
|
||||||
// wait for zoom animation to finish
|
|
||||||
await bgImageLocator.hover({trial: true});
|
|
||||||
|
|
||||||
return expect(pausePlayButton).not.toHaveClass(/is-paused/);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// The following test case will cover these scenarios
|
test.describe('Example Imagery in Display layout', () => {
|
||||||
// ('Can use Mouse Wheel to zoom in and out of previous image');
|
test.skip('Can use Mouse Wheel to zoom in and out of previous image');
|
||||||
// ('Can use alt+drag to move around image once zoomed in');
|
test.skip('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');
|
test.skip('Can zoom into the latest image and the real-time/fixed-time imagery will pause');
|
||||||
// ('If the imagery view is in pause mode, it should not be updated when new images come in');
|
test.skip('Clicking on the left arrow should pause the imagery and go to previous image');
|
||||||
// ('If the imagery view is not in pause mode, it should be updated when new images come in');
|
test.skip('If the imagery view is in pause mode, it should not be updated when new images come in');
|
||||||
const backgroundImageSelector = '.c-imagery__main-image__background-image';
|
test.skip('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 }) => {
|
|
||||||
test.info().annotations.push({
|
|
||||||
type: 'issue',
|
|
||||||
description: 'https://github.com/nasa/openmct/issues/5265'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Go to baseURL
|
|
||||||
await page.goto('/', { waitUntil: 'networkidle' });
|
|
||||||
|
|
||||||
// Click the Create button
|
|
||||||
await page.click('button:has-text("Create")');
|
|
||||||
|
|
||||||
// Click text=Example Imagery
|
|
||||||
await page.click('text=Example Imagery');
|
|
||||||
|
|
||||||
// Clear and set Image load delay to minimum value
|
|
||||||
// FIXME: Update the value to 5000 ms when this bug is fixed.
|
|
||||||
// See: https://github.com/nasa/openmct/issues/5265
|
|
||||||
await page.locator('input[type="number"]').fill('');
|
|
||||||
await page.locator('input[type="number"]').fill('0');
|
|
||||||
|
|
||||||
// Click text=OK
|
|
||||||
await Promise.all([
|
|
||||||
page.waitForNavigation({waitUntil: 'networkidle'}),
|
|
||||||
page.click('text=OK'),
|
|
||||||
//Wait for Save Banner to appear
|
|
||||||
page.waitForSelector('.c-message-banner__message')
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Wait until Save Banner is gone
|
|
||||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
|
||||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');
|
|
||||||
const bgImageLocator = page.locator(backgroundImageSelector);
|
|
||||||
await bgImageLocator.hover({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 bgImageLocator.hover({trial: true});
|
|
||||||
const deltaYStep = 100; // equivalent to 1x zoom
|
|
||||||
await page.mouse.wheel(0, deltaYStep * 2);
|
|
||||||
const zoomedBoundingBox = await bgImageLocator.boundingBox();
|
|
||||||
const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;
|
|
||||||
const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
|
|
||||||
|
|
||||||
// Wait for zoom animation to finish
|
|
||||||
await bgImageLocator.hover({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 bgImageLocator.hover({trial: true});
|
|
||||||
await page.mouse.wheel(0, deltaYStep * 2);
|
|
||||||
|
|
||||||
// Wait for zoom animation to finish
|
|
||||||
await bgImageLocator.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 new images still stream in",
|
|
||||||
timeout: 6 * 1000
|
|
||||||
}).toBeGreaterThan(imageCount);
|
|
||||||
|
|
||||||
// Verify selected image is still displayed
|
|
||||||
await expect(selectedImage).toBeVisible();
|
|
||||||
|
|
||||||
// Unpause imagery
|
|
||||||
await page.locator('.pause-play').click();
|
|
||||||
|
|
||||||
//Get background-image url from background-image css prop
|
|
||||||
const backgroundImage = page.locator('.c-imagery__main-image__background-image');
|
|
||||||
let backgroundImageUrl = await backgroundImage.evaluate((el) => {
|
|
||||||
return window.getComputedStyle(el).getPropertyValue('background-image').match(/url\(([^)]+)\)/)[1];
|
|
||||||
});
|
|
||||||
let backgroundImageUrl1 = backgroundImageUrl.slice(1, -1); //forgive me, padre
|
|
||||||
console.log('backgroundImageUrl1 ' + backgroundImageUrl1);
|
|
||||||
|
|
||||||
let backgroundImageUrl2;
|
|
||||||
await expect.poll(async () => {
|
|
||||||
// Verify next image has updated
|
|
||||||
let backgroundImageUrlNext = await backgroundImage.evaluate((el) => {
|
|
||||||
return window.getComputedStyle(el).getPropertyValue('background-image').match(/url\(([^)]+)\)/)[1];
|
|
||||||
});
|
|
||||||
backgroundImageUrl2 = backgroundImageUrlNext.slice(1, -1); //forgive me, padre
|
|
||||||
|
|
||||||
return backgroundImageUrl2;
|
|
||||||
}, {
|
|
||||||
message: "verify next image has updated",
|
|
||||||
timeout: 6 * 1000
|
|
||||||
}).not.toBe(backgroundImageUrl1);
|
|
||||||
console.log('backgroundImageUrl2 ' + backgroundImageUrl2);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Example imagery thumbnails resize in display layouts', () => {
|
|
||||||
|
|
||||||
test('Resizing the layout changes thumbnail visibility and size', async ({ page }) => {
|
|
||||||
await page.goto('/', { waitUntil: 'networkidle' });
|
|
||||||
|
|
||||||
const thumbsWrapperLocator = page.locator('.c-imagery__thumbs-wrapper');
|
|
||||||
// Click button:has-text("Create")
|
|
||||||
await page.locator('button:has-text("Create")').click();
|
|
||||||
|
|
||||||
// Click li:has-text("Display Layout")
|
|
||||||
await page.locator('li:has-text("Display Layout")').click();
|
|
||||||
const displayLayoutTitleField = page.locator('text=Properties Title Notes Horizontal grid (px) Vertical grid (px) Horizontal size ( >> input[type="text"]');
|
|
||||||
await displayLayoutTitleField.click();
|
|
||||||
|
|
||||||
await displayLayoutTitleField.fill('Thumbnail Display Layout');
|
|
||||||
|
|
||||||
// Click text=OK
|
|
||||||
await Promise.all([
|
|
||||||
page.waitForNavigation(),
|
|
||||||
page.locator('text=OK').click()
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Click text=Snapshot Save and Finish Editing Save and Continue Editing >> button >> nth=1
|
|
||||||
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
|
|
||||||
|
|
||||||
// Click text=Save and Finish Editing
|
|
||||||
await page.locator('text=Save and Finish Editing').click();
|
|
||||||
|
|
||||||
// Click button:has-text("Create")
|
|
||||||
await page.locator('button:has-text("Create")').click();
|
|
||||||
|
|
||||||
// Click li:has-text("Example Imagery")
|
|
||||||
await page.locator('li:has-text("Example Imagery")').click();
|
|
||||||
|
|
||||||
const imageryTitleField = page.locator('text=Properties Title Notes Images url list (comma separated) Image load delay (milli >> input[type="text"]');
|
|
||||||
// Click text=Properties Title Notes Images url list (comma separated) Image load delay (milli >> input[type="text"]
|
|
||||||
await imageryTitleField.click();
|
|
||||||
|
|
||||||
// Fill text=Properties Title Notes Images url list (comma separated) Image load delay (milli >> input[type="text"]
|
|
||||||
await imageryTitleField.fill('Thumbnail Example Imagery');
|
|
||||||
|
|
||||||
// Click text=OK
|
|
||||||
await Promise.all([
|
|
||||||
page.waitForNavigation(),
|
|
||||||
page.locator('text=OK').click()
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Click text=Thumbnail Example Imagery Imagery Layout Snapshot >> button >> nth=0
|
|
||||||
await Promise.all([
|
|
||||||
page.waitForNavigation(),
|
|
||||||
page.locator('text=Thumbnail Example Imagery Imagery Layout Snapshot >> button').first().click()
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Edit mode
|
|
||||||
await page.locator('text=Thumbnail Display Layout Snapshot >> button').nth(3).click();
|
|
||||||
|
|
||||||
// Click on example imagery to expose toolbar
|
|
||||||
await page.locator('text=Thumbnail Example Imagery Snapshot Large View').click();
|
|
||||||
|
|
||||||
// expect thumbnails not be visible when first added
|
|
||||||
expect.soft(thumbsWrapperLocator.isHidden()).toBeTruthy();
|
|
||||||
|
|
||||||
// Resize the example imagery vertically to change the thumbnail visibility
|
|
||||||
/*
|
|
||||||
The following arbitrary values are added to observe the separate visual
|
|
||||||
conditions of the thumbnails (hidden, small thumbnails, regular thumbnails).
|
|
||||||
Specifically, height is set to 50px for small thumbs and 100px for regular
|
|
||||||
*/
|
|
||||||
// Click #mct-input-id-103
|
|
||||||
await page.locator('#mct-input-id-103').click();
|
|
||||||
|
|
||||||
// Fill #mct-input-id-103
|
|
||||||
await page.locator('#mct-input-id-103').fill('50');
|
|
||||||
|
|
||||||
expect(thumbsWrapperLocator.isVisible()).toBeTruthy();
|
|
||||||
await expect(thumbsWrapperLocator).toHaveClass(/is-small-thumbs/);
|
|
||||||
|
|
||||||
// Resize the example imagery vertically to change the thumbnail visibility
|
|
||||||
// Click #mct-input-id-103
|
|
||||||
await page.locator('#mct-input-id-103').click();
|
|
||||||
|
|
||||||
// Fill #mct-input-id-103
|
|
||||||
await page.locator('#mct-input-id-103').fill('100');
|
|
||||||
|
|
||||||
expect(thumbsWrapperLocator.isVisible()).toBeTruthy();
|
|
||||||
await expect(thumbsWrapperLocator).not.toHaveClass(/is-small-thumbs/);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe('Example Imagery in Flexible layout', () => {
|
test.describe('Example Imagery in Flexible layout', () => {
|
||||||
test.fixme('Can use Mouse Wheel to zoom in and out of previous image');
|
test.skip('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.skip('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.skip('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.skip('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.skip('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.skip('If the imagery view is not in pause mode, it should be updated when new images come in');
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe('Example Imagery in Tabs view', () => {
|
test.describe('Example Imagery in Tabs view', () => {
|
||||||
test.fixme('Can use Mouse Wheel to zoom in and out of previous image');
|
test.skip('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.skip('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.skip('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.skip('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.skip('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.skip('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.skip('If the imagery view is not in pause mode, it should be updated when new images come in');
|
||||||
});
|
});
|
||||||
|
@ -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,264 +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 COMMIT_BUTTON_TEXT = 'button:has-text("Commit Entries")';
|
|
||||||
const SINE_WAVE_GENERATOR = 'text=Unnamed Sine Wave Generator';
|
|
||||||
const NOTEBOOK_DROP_AREA = '.c-notebook__drag-area';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {import('@playwright/test').Page} page
|
|
||||||
*/
|
|
||||||
async function startAndAddNotebookObject(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')
|
|
||||||
]);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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');
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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(SINE_WAVE_GENERATOR, NOTEBOOK_DROP_AREA);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {import('@playwright/test').Page} page
|
|
||||||
*/
|
|
||||||
async function lockPage(page) {
|
|
||||||
const commitButton = page.locator(COMMIT_BUTTON_TEXT);
|
|
||||||
await commitButton.click();
|
|
||||||
|
|
||||||
// confirmation dialog click
|
|
||||||
await page.locator('text=Lock Page').click();
|
|
||||||
|
|
||||||
// waiting for mutation of locked page
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
setTimeout(resolve, 1000);
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {import('@playwright/test').Page} page
|
|
||||||
*/
|
|
||||||
async function openContextMenuRestrictedNotebook(page) {
|
|
||||||
// Click text=Open MCT My Items (This expands the My Items folder to show it's chilren in the tree)
|
|
||||||
await page.locator('text=Open MCT My Items >> span').nth(3).click();
|
|
||||||
|
|
||||||
// Click a:has-text("Unnamed CUSTOM_NAME")
|
|
||||||
await page.locator(`a:has-text("Unnamed ${CUSTOM_NAME}")`).click({
|
|
||||||
button: 'right'
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
test.describe('Restricted Notebook', () => {
|
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
|
||||||
await startAndAddNotebookObject(page);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Can be renamed', async ({ page }) => {
|
|
||||||
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText(`Unnamed ${CUSTOM_NAME}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Can be deleted if there are no locked pages', async ({ page }) => {
|
|
||||||
await openContextMenuRestrictedNotebook(page);
|
|
||||||
|
|
||||||
const menuOptions = page.locator('.c-menu ul');
|
|
||||||
await expect.soft(menuOptions).toContainText('Remove');
|
|
||||||
|
|
||||||
const restrictedNotebookTreeObject = page.locator(`a:has-text("Unnamed ${CUSTOM_NAME}")`);
|
|
||||||
|
|
||||||
// notbook tree object exists
|
|
||||||
expect.soft(await restrictedNotebookTreeObject.count()).toEqual(1);
|
|
||||||
|
|
||||||
// Click text=Remove
|
|
||||||
await page.locator('text=Remove').click();
|
|
||||||
// Click text=OK
|
|
||||||
await Promise.all([
|
|
||||||
page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine?tc.mode=fixed&tc.startBound=1653671067340&tc.endBound=1653672867340&tc.timeSystem=utc&view=grid' }*/),
|
|
||||||
page.locator('text=OK').click()
|
|
||||||
]);
|
|
||||||
|
|
||||||
// has been deleted
|
|
||||||
expect.soft(await restrictedNotebookTreeObject.count()).toEqual(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Can be locked if at least one page has one entry', async ({ page }) => {
|
|
||||||
|
|
||||||
await enterTextEntry(page);
|
|
||||||
|
|
||||||
const commitButton = page.locator(COMMIT_BUTTON_TEXT);
|
|
||||||
expect.soft(await commitButton.count()).toEqual(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Restricted Notebook with at least one entry and with the page locked', () => {
|
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
|
||||||
await startAndAddNotebookObject(page);
|
|
||||||
await enterTextEntry(page);
|
|
||||||
await lockPage(page);
|
|
||||||
|
|
||||||
// open sidebar
|
|
||||||
await page.locator('button.c-notebook__toggle-nav-button').click();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Locked page should now be in a locked state', async ({ page }) => {
|
|
||||||
// main lock message on page
|
|
||||||
const lockMessage = page.locator('text=This page has been committed and cannot be modified or removed');
|
|
||||||
expect.soft(await lockMessage.count()).toEqual(1);
|
|
||||||
|
|
||||||
// lock icon on page in sidebar
|
|
||||||
const pageLockIcon = page.locator('ul.c-notebook__pages li div.icon-lock');
|
|
||||||
expect.soft(await pageLockIcon.count()).toEqual(1);
|
|
||||||
|
|
||||||
// no way to remove a restricted notebook with a locked page
|
|
||||||
await openContextMenuRestrictedNotebook(page);
|
|
||||||
|
|
||||||
const menuOptions = page.locator('.c-menu ul');
|
|
||||||
|
|
||||||
await expect.soft(menuOptions).not.toContainText('Remove');
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Can still: add page, rename, add entry, delete unlocked pages', async ({ page }) => {
|
|
||||||
// Click text=Page Add >> button
|
|
||||||
await Promise.all([
|
|
||||||
page.waitForNavigation(),
|
|
||||||
page.locator('text=Page Add >> button').click()
|
|
||||||
]);
|
|
||||||
// Click text=Unnamed Page >> nth=1
|
|
||||||
await page.locator('text=Unnamed Page').nth(1).click();
|
|
||||||
// Press a with modifiers
|
|
||||||
await page.locator('text=Unnamed Page').nth(1).fill(TEST_TEXT_NAME);
|
|
||||||
|
|
||||||
// expect to be able to rename unlocked pages
|
|
||||||
const newPageElement = page.locator(`text=${TEST_TEXT_NAME}`);
|
|
||||||
const newPageCount = await newPageElement.count();
|
|
||||||
await newPageElement.press('Enter'); // exit contenteditable state
|
|
||||||
expect.soft(newPageCount).toEqual(1);
|
|
||||||
|
|
||||||
// enter test text
|
|
||||||
await enterTextEntry(page);
|
|
||||||
|
|
||||||
// expect new page to be lockable
|
|
||||||
const commitButton = page.locator(COMMIT_BUTTON_TEXT);
|
|
||||||
expect.soft(await commitButton.count()).toEqual(1);
|
|
||||||
|
|
||||||
// Click text=Unnamed PageTest Page >> button
|
|
||||||
await page.locator('text=Unnamed PageTest Page >> button').click();
|
|
||||||
// Click text=Delete Page
|
|
||||||
await page.locator('text=Delete Page').click();
|
|
||||||
// Click text=Ok
|
|
||||||
await Promise.all([
|
|
||||||
page.waitForNavigation(),
|
|
||||||
page.locator('text=Ok').click()
|
|
||||||
]);
|
|
||||||
|
|
||||||
// deleted page, should no longer exist
|
|
||||||
const deletedPageElement = page.locator(`text=${TEST_TEXT_NAME}`);
|
|
||||||
expect.soft(await deletedPageElement.count()).toEqual(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Restricted Notebook with a page locked and with an embed', () => {
|
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
|
||||||
await startAndAddNotebookObject(page);
|
|
||||||
await dragAndDropEmbed(page);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Allows embeds to be deleted if page unlocked', async ({ page }) => {
|
|
||||||
// Click .c-ne__embed__name .c-popup-menu-button
|
|
||||||
await page.locator('.c-ne__embed__name .c-popup-menu-button').click(); // embed popup menu
|
|
||||||
|
|
||||||
const embedMenu = page.locator('body >> .c-menu');
|
|
||||||
await expect.soft(embedMenu).toContainText('Remove This Embed');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Disallows embeds to be deleted if page locked', async ({ page }) => {
|
|
||||||
await lockPage(page);
|
|
||||||
// Click .c-ne__embed__name .c-popup-menu-button
|
|
||||||
await page.locator('.c-ne__embed__name .c-popup-menu-button').click(); // embed popup menu
|
|
||||||
|
|
||||||
const embedMenu = page.locator('body >> .c-menu');
|
|
||||||
await expect.soft(embedMenu).not.toContainText('Remove This Embed');
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
@ -1,198 +0,0 @@
|
|||||||
/*****************************************************************************
|
|
||||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
|
||||||
* as represented by the Administrator of the National Aeronautics and Space
|
|
||||||
* Administration. All rights reserved.
|
|
||||||
*
|
|
||||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
* License for the specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*
|
|
||||||
* Open MCT includes source code licensed under additional open source
|
|
||||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
|
||||||
* this source code distribution or the Licensing information page available
|
|
||||||
* at runtime from the About dialog for additional information.
|
|
||||||
*****************************************************************************/
|
|
||||||
|
|
||||||
/*
|
|
||||||
Testsuite for plot autoscale.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const { test: _test } = require('../../../fixtures.js');
|
|
||||||
const { expect } = require('@playwright/test');
|
|
||||||
|
|
||||||
// create a new `test` API that will not append platform details to snapshot
|
|
||||||
// file names, only for the tests in this file, so that the same snapshots will
|
|
||||||
// be used for all platforms.
|
|
||||||
const test = _test.extend({
|
|
||||||
_autoSnapshotSuffix: [
|
|
||||||
async ({}, use, testInfo) => {
|
|
||||||
testInfo.snapshotSuffix = '';
|
|
||||||
await use();
|
|
||||||
},
|
|
||||||
{ auto: true }
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
test.use({
|
|
||||||
viewport: {
|
|
||||||
width: 1280,
|
|
||||||
height: 720
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('ExportAsJSON', () => {
|
|
||||||
test('User can set autoscale with a valid range @snapshot', async ({ page }) => {
|
|
||||||
//This is necessary due to the size of the test suite.
|
|
||||||
await test.setTimeout(120 * 1000);
|
|
||||||
|
|
||||||
await page.goto('/', { waitUntil: 'networkidle' });
|
|
||||||
|
|
||||||
await setTimeRange(page);
|
|
||||||
|
|
||||||
await createSinewaveOverlayPlot(page);
|
|
||||||
|
|
||||||
await testYTicks(page, ['-1.00', '-0.50', '0.00', '0.50', '1.00']);
|
|
||||||
|
|
||||||
await turnOffAutoscale(page);
|
|
||||||
|
|
||||||
const canvas = page.locator('canvas').nth(1);
|
|
||||||
|
|
||||||
// Make sure that after turning off autoscale, the user selected range values start at the same values the plot had prior.
|
|
||||||
await Promise.all([
|
|
||||||
testYTicks(page, ['-1.00', '-0.50', '0.00', '0.50', '1.00']),
|
|
||||||
new Promise(r => setTimeout(r, 100))
|
|
||||||
.then(() => canvas.screenshot())
|
|
||||||
.then(shot => expect(shot).toMatchSnapshot('autoscale-canvas-prepan.png', { maxDiffPixels: 40 }))
|
|
||||||
]);
|
|
||||||
|
|
||||||
await page.keyboard.down('Alt');
|
|
||||||
|
|
||||||
await canvas.dragTo(canvas, {
|
|
||||||
sourcePosition: {
|
|
||||||
x: 200,
|
|
||||||
y: 200
|
|
||||||
},
|
|
||||||
targetPosition: {
|
|
||||||
x: 400,
|
|
||||||
y: 400
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.keyboard.up('Alt');
|
|
||||||
|
|
||||||
// Ensure the drag worked.
|
|
||||||
await Promise.all([
|
|
||||||
testYTicks(page, ['0.00', '0.50', '1.00', '1.50', '2.00']),
|
|
||||||
new Promise(r => setTimeout(r, 100))
|
|
||||||
.then(() => canvas.screenshot())
|
|
||||||
.then(shot => expect(shot).toMatchSnapshot('autoscale-canvas-panned.png', { maxDiffPixels: 40 }))
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {import('@playwright/test').Page} page
|
|
||||||
* @param {string} start
|
|
||||||
* @param {string} end
|
|
||||||
*/
|
|
||||||
async function setTimeRange(page, start = '2022-03-29 22:00:00.000Z', end = '2022-03-29 22:00:30.000Z') {
|
|
||||||
// Set a specific time range for consistency, otherwise it will change
|
|
||||||
// on every test to a range based on the current time.
|
|
||||||
|
|
||||||
const timeInputs = page.locator('input.c-input--datetime');
|
|
||||||
await timeInputs.first().click();
|
|
||||||
await timeInputs.first().fill(start);
|
|
||||||
|
|
||||||
await timeInputs.nth(1).click();
|
|
||||||
await timeInputs.nth(1).fill(end);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {import('@playwright/test').Page} page
|
|
||||||
*/
|
|
||||||
async function createSinewaveOverlayPlot(page) {
|
|
||||||
// click create button
|
|
||||||
await page.locator('button:has-text("Create")').click();
|
|
||||||
|
|
||||||
// add overlay plot with defaults
|
|
||||||
await page.locator('li:has-text("Overlay Plot")').click();
|
|
||||||
await Promise.all([
|
|
||||||
page.waitForNavigation(),
|
|
||||||
page.locator('text=OK').click(),
|
|
||||||
//Wait for Save Banner to appear1
|
|
||||||
page.waitForSelector('.c-message-banner__message')
|
|
||||||
]);
|
|
||||||
//Wait until Save Banner is gone
|
|
||||||
await page.locator('.c-message-banner__close-button').click();
|
|
||||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
|
||||||
|
|
||||||
// save (exit edit mode)
|
|
||||||
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
|
|
||||||
await page.locator('text=Save and Finish Editing').click();
|
|
||||||
|
|
||||||
// click create button
|
|
||||||
await page.locator('button:has-text("Create")').click();
|
|
||||||
|
|
||||||
// add sine wave generator with defaults
|
|
||||||
await page.locator('li:has-text("Sine Wave Generator")').click();
|
|
||||||
await Promise.all([
|
|
||||||
page.waitForNavigation(),
|
|
||||||
page.locator('text=OK').click(),
|
|
||||||
//Wait for Save Banner to appear1
|
|
||||||
page.waitForSelector('.c-message-banner__message')
|
|
||||||
]);
|
|
||||||
//Wait until Save Banner is gone
|
|
||||||
await page.locator('.c-message-banner__close-button').click();
|
|
||||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
|
||||||
|
|
||||||
// focus the overlay plot
|
|
||||||
await page.locator('text=Open MCT My Items >> span').nth(3).click();
|
|
||||||
await Promise.all([
|
|
||||||
page.waitForNavigation(),
|
|
||||||
page.locator('text=Unnamed Overlay Plot').first().click()
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {import('@playwright/test').Page} page
|
|
||||||
*/
|
|
||||||
async function turnOffAutoscale(page) {
|
|
||||||
// enter edit mode
|
|
||||||
await page.locator('text=Unnamed Overlay Plot Snapshot >> button').nth(3).click();
|
|
||||||
|
|
||||||
// uncheck autoscale
|
|
||||||
await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"] >> nth=1').uncheck();
|
|
||||||
|
|
||||||
// save
|
|
||||||
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
|
|
||||||
await Promise.all([
|
|
||||||
page.locator('text=Save and Finish Editing').click(),
|
|
||||||
//Wait for Save Banner to appear
|
|
||||||
page.waitForSelector('.c-message-banner__message')
|
|
||||||
]);
|
|
||||||
//Wait until Save Banner is gone
|
|
||||||
await page.locator('.c-message-banner__close-button').click();
|
|
||||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {import('@playwright/test').Page} page
|
|
||||||
*/
|
|
||||||
async function testYTicks(page, values) {
|
|
||||||
const yTicks = page.locator('.gl-plot-y-tick-label');
|
|
||||||
await page.locator('canvas >> nth=1').hover();
|
|
||||||
let promises = [yTicks.count().then(c => expect(c).toBe(values.length))];
|
|
||||||
|
|
||||||
for (let i = 0, l = values.length; i < l; i += 1) {
|
|
||||||
promises.push(expect(yTicks.nth(i)).toHaveText(values[i])); // eslint-disable-line
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.all(promises);
|
|
||||||
}
|
|
Binary file not shown.
Before Width: | Height: | Size: 18 KiB |
Binary file not shown.
Before Width: | Height: | Size: 19 KiB |
@ -1,312 +0,0 @@
|
|||||||
/*****************************************************************************
|
|
||||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
|
||||||
* as represented by the Administrator of the National Aeronautics and Space
|
|
||||||
* Administration. All rights reserved.
|
|
||||||
*
|
|
||||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
* License for the specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*
|
|
||||||
* Open MCT includes source code licensed under additional open source
|
|
||||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
|
||||||
* this source code distribution or the Licensing information page available
|
|
||||||
* at runtime from the About dialog for additional information.
|
|
||||||
*****************************************************************************/
|
|
||||||
|
|
||||||
/*
|
|
||||||
Tests to verify log plot functionality. Note this test suite if very much under active development and should not
|
|
||||||
necessarily be used for reference when writing new tests in this area.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const { test } = require('../../../fixtures.js');
|
|
||||||
const { expect } = require('@playwright/test');
|
|
||||||
|
|
||||||
test.describe('Log plot tests', () => {
|
|
||||||
test('Log Plot ticks are functionally correct in regular and log mode and after refresh', async ({ page }) => {
|
|
||||||
//This is necessary due to the size of the test suite.
|
|
||||||
await test.setTimeout(120 * 1000);
|
|
||||||
|
|
||||||
await makeOverlayPlot(page);
|
|
||||||
await testRegularTicks(page);
|
|
||||||
await enableEditMode(page);
|
|
||||||
await enableLogMode(page);
|
|
||||||
await testLogTicks(page);
|
|
||||||
await disableLogMode(page);
|
|
||||||
await testRegularTicks(page);
|
|
||||||
await enableLogMode(page);
|
|
||||||
await testLogTicks(page);
|
|
||||||
await saveOverlayPlot(page);
|
|
||||||
await testLogTicks(page);
|
|
||||||
//await testLogPlotPixels(page);
|
|
||||||
|
|
||||||
// FIXME: Get rid of the waitForTimeout() and lint warning exception.
|
|
||||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
|
||||||
await page.waitForTimeout(1 * 1000);
|
|
||||||
|
|
||||||
// refresh page and wait for charts and ticks to load
|
|
||||||
await page.reload({ waitUntil: 'networkidle'});
|
|
||||||
await page.waitForSelector('.gl-plot-chart-area');
|
|
||||||
await page.waitForSelector('.gl-plot-y-tick-label');
|
|
||||||
|
|
||||||
// test log ticks hold up after refresh
|
|
||||||
await testLogTicks(page);
|
|
||||||
//await testLogPlotPixels(page);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Leaving test as 'TODO' for now.
|
|
||||||
// NOTE: Not eligible for community contributions.
|
|
||||||
test.fixme('Verify that log mode option is reflected in import/export JSON', async ({ page }) => {
|
|
||||||
await makeOverlayPlot(page);
|
|
||||||
await enableEditMode(page);
|
|
||||||
await enableLogMode(page);
|
|
||||||
await saveOverlayPlot(page);
|
|
||||||
|
|
||||||
// TODO ...export, delete the overlay, then import it...
|
|
||||||
|
|
||||||
//await testLogTicks(page);
|
|
||||||
|
|
||||||
// TODO, the plot is slightly at different position that in the other test, so this fails.
|
|
||||||
// ...We can fix it by copying all steps from the first test...
|
|
||||||
// await testLogPlotPixels(page);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Makes an overlay plot with a sine wave generator and clicks on the overlay plot in the sidebar so it is the active thing displayed.
|
|
||||||
* @param {import('@playwright/test').Page} page
|
|
||||||
*/
|
|
||||||
async function makeOverlayPlot(page) {
|
|
||||||
// fresh page with time range from 2022-03-29 22:00:00.000Z to 2022-03-29 22:00:30.000Z
|
|
||||||
await page.goto('/', { waitUntil: 'networkidle' });
|
|
||||||
|
|
||||||
// Set a specific time range for consistency, otherwise it will change
|
|
||||||
// on every test to a range based on the current time.
|
|
||||||
|
|
||||||
const timeInputs = page.locator('input.c-input--datetime');
|
|
||||||
await timeInputs.first().click();
|
|
||||||
await timeInputs.first().fill('2022-03-29 22:00:00.000Z');
|
|
||||||
|
|
||||||
await timeInputs.nth(1).click();
|
|
||||||
await timeInputs.nth(1).fill('2022-03-29 22:00:30.000Z');
|
|
||||||
|
|
||||||
// create overlay plot
|
|
||||||
|
|
||||||
await page.locator('button.c-create-button').click();
|
|
||||||
await page.locator('li:has-text("Overlay Plot")').click();
|
|
||||||
await Promise.all([
|
|
||||||
page.waitForNavigation({ waitUntil: 'networkidle'}),
|
|
||||||
page.locator('text=OK').click(),
|
|
||||||
//Wait for Save Banner to appear
|
|
||||||
page.waitForSelector('.c-message-banner__message')
|
|
||||||
]);
|
|
||||||
//Wait until Save Banner is gone
|
|
||||||
await page.locator('.c-message-banner__close-button').click();
|
|
||||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
|
||||||
|
|
||||||
// save the overlay plot
|
|
||||||
|
|
||||||
await saveOverlayPlot(page);
|
|
||||||
|
|
||||||
// create a sinewave generator
|
|
||||||
|
|
||||||
await page.locator('button.c-create-button').click();
|
|
||||||
await page.locator('li:has-text("Sine Wave Generator")').click();
|
|
||||||
|
|
||||||
// set amplitude to 6, offset 4, period 2
|
|
||||||
|
|
||||||
await page.locator('div:nth-child(5) .form-row .c-form-row__controls .form-control .field input').click();
|
|
||||||
await page.locator('div:nth-child(5) .form-row .c-form-row__controls .form-control .field input').fill('6');
|
|
||||||
|
|
||||||
await page.locator('div:nth-child(6) .form-row .c-form-row__controls .form-control .field input').click();
|
|
||||||
await page.locator('div:nth-child(6) .form-row .c-form-row__controls .form-control .field input').fill('4');
|
|
||||||
|
|
||||||
await page.locator('div:nth-child(7) .form-row .c-form-row__controls .form-control .field input').click();
|
|
||||||
await page.locator('div:nth-child(7) .form-row .c-form-row__controls .form-control .field input').fill('2');
|
|
||||||
|
|
||||||
// Click OK to make generator
|
|
||||||
|
|
||||||
await Promise.all([
|
|
||||||
page.waitForNavigation({ waitUntil: 'networkidle'}),
|
|
||||||
page.locator('text=OK').click(),
|
|
||||||
//Wait for Save Banner to appear
|
|
||||||
page.waitForSelector('.c-message-banner__message')
|
|
||||||
]);
|
|
||||||
//Wait until Save Banner is gone
|
|
||||||
await page.locator('.c-message-banner__close-button').click();
|
|
||||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
|
||||||
|
|
||||||
// click on overlay plot
|
|
||||||
|
|
||||||
await page.locator('text=Open MCT My Items >> span').nth(3).click();
|
|
||||||
await Promise.all([
|
|
||||||
page.waitForNavigation(),
|
|
||||||
page.locator('text=Unnamed Overlay Plot').first().click()
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {import('@playwright/test').Page} page
|
|
||||||
*/
|
|
||||||
async function testRegularTicks(page) {
|
|
||||||
const yTicks = await page.locator('.gl-plot-y-tick-label');
|
|
||||||
expect(await yTicks.count()).toBe(7);
|
|
||||||
await expect(yTicks.nth(0)).toHaveText('-2');
|
|
||||||
await expect(yTicks.nth(1)).toHaveText('0');
|
|
||||||
await expect(yTicks.nth(2)).toHaveText('2');
|
|
||||||
await expect(yTicks.nth(3)).toHaveText('4');
|
|
||||||
await expect(yTicks.nth(4)).toHaveText('6');
|
|
||||||
await expect(yTicks.nth(5)).toHaveText('8');
|
|
||||||
await expect(yTicks.nth(6)).toHaveText('10');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {import('@playwright/test').Page} page
|
|
||||||
*/
|
|
||||||
async function testLogTicks(page) {
|
|
||||||
const yTicks = await page.locator('.gl-plot-y-tick-label');
|
|
||||||
expect(await yTicks.count()).toBe(28);
|
|
||||||
await expect(yTicks.nth(0)).toHaveText('-2.98');
|
|
||||||
await expect(yTicks.nth(1)).toHaveText('-2.50');
|
|
||||||
await expect(yTicks.nth(2)).toHaveText('-2.00');
|
|
||||||
await expect(yTicks.nth(3)).toHaveText('-1.51');
|
|
||||||
await expect(yTicks.nth(4)).toHaveText('-1.20');
|
|
||||||
await expect(yTicks.nth(5)).toHaveText('-1.00');
|
|
||||||
await expect(yTicks.nth(6)).toHaveText('-0.80');
|
|
||||||
await expect(yTicks.nth(7)).toHaveText('-0.58');
|
|
||||||
await expect(yTicks.nth(8)).toHaveText('-0.40');
|
|
||||||
await expect(yTicks.nth(9)).toHaveText('-0.20');
|
|
||||||
await expect(yTicks.nth(10)).toHaveText('-0.00');
|
|
||||||
await expect(yTicks.nth(11)).toHaveText('0.20');
|
|
||||||
await expect(yTicks.nth(12)).toHaveText('0.40');
|
|
||||||
await expect(yTicks.nth(13)).toHaveText('0.58');
|
|
||||||
await expect(yTicks.nth(14)).toHaveText('0.80');
|
|
||||||
await expect(yTicks.nth(15)).toHaveText('1.00');
|
|
||||||
await expect(yTicks.nth(16)).toHaveText('1.20');
|
|
||||||
await expect(yTicks.nth(17)).toHaveText('1.51');
|
|
||||||
await expect(yTicks.nth(18)).toHaveText('2.00');
|
|
||||||
await expect(yTicks.nth(19)).toHaveText('2.50');
|
|
||||||
await expect(yTicks.nth(20)).toHaveText('2.98');
|
|
||||||
await expect(yTicks.nth(21)).toHaveText('3.50');
|
|
||||||
await expect(yTicks.nth(22)).toHaveText('4.00');
|
|
||||||
await expect(yTicks.nth(23)).toHaveText('4.50');
|
|
||||||
await expect(yTicks.nth(24)).toHaveText('5.31');
|
|
||||||
await expect(yTicks.nth(25)).toHaveText('7.00');
|
|
||||||
await expect(yTicks.nth(26)).toHaveText('8.00');
|
|
||||||
await expect(yTicks.nth(27)).toHaveText('9.00');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {import('@playwright/test').Page} page
|
|
||||||
*/
|
|
||||||
async function enableEditMode(page) {
|
|
||||||
// turn on edit mode
|
|
||||||
await page.locator('text=Unnamed Overlay Plot Snapshot >> button').nth(3).click();
|
|
||||||
await expect(await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1)).toBeVisible();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {import('@playwright/test').Page} page
|
|
||||||
*/
|
|
||||||
async function enableLogMode(page) {
|
|
||||||
// turn on log mode
|
|
||||||
await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"]').first().check();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {import('@playwright/test').Page} page
|
|
||||||
*/
|
|
||||||
async function disableLogMode(page) {
|
|
||||||
// turn off log mode
|
|
||||||
await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"]').first().uncheck();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {import('@playwright/test').Page} page
|
|
||||||
*/
|
|
||||||
async function saveOverlayPlot(page) {
|
|
||||||
// save overlay plot
|
|
||||||
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
|
|
||||||
|
|
||||||
await Promise.all([
|
|
||||||
page.locator('text=Save and Finish Editing').click(),
|
|
||||||
//Wait for Save Banner to appear
|
|
||||||
page.waitForSelector('.c-message-banner__message')
|
|
||||||
]);
|
|
||||||
//Wait until Save Banner is gone
|
|
||||||
await page.locator('.c-message-banner__close-button').click();
|
|
||||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached' });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {import('@playwright/test').Page} page
|
|
||||||
*/
|
|
||||||
// FIXME: Remove this eslint exception once implemented
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
async function testLogPlotPixels(page) {
|
|
||||||
const pixelsMatch = await page.evaluate(async () => {
|
|
||||||
// TODO get canvas pixels at a few locations to make sure they're the correct color, to test that the plot comes out as expected.
|
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 5 * 1000));
|
|
||||||
|
|
||||||
// These are some pixels that should be blue points in the log plot.
|
|
||||||
// If the plot changes shape to an unexpected shape, this will
|
|
||||||
// likely fail, which is what we want.
|
|
||||||
//
|
|
||||||
// I found these pixels by pausing playwright in debug mode at this
|
|
||||||
// point, and using similar code as below to output the pixel data, then
|
|
||||||
// I logged those pixels here.
|
|
||||||
const expectedBluePixels = [
|
|
||||||
// TODO these pixel sets only work with the first test, but not the second test.
|
|
||||||
|
|
||||||
// [60, 35],
|
|
||||||
// [121, 125],
|
|
||||||
// [156, 377],
|
|
||||||
// [264, 73],
|
|
||||||
// [372, 186],
|
|
||||||
// [576, 73],
|
|
||||||
// [659, 439],
|
|
||||||
// [675, 423]
|
|
||||||
|
|
||||||
[60, 35],
|
|
||||||
[120, 125],
|
|
||||||
[156, 375],
|
|
||||||
[264, 73],
|
|
||||||
[372, 185],
|
|
||||||
[575, 72],
|
|
||||||
[659, 437],
|
|
||||||
[675, 421]
|
|
||||||
];
|
|
||||||
|
|
||||||
// The first canvas in the DOM is the one that has the plot point
|
|
||||||
// icons (canvas 2d), which is the one we are testing. The second
|
|
||||||
// one in the DOM is the WebGL canvas with the line. (Why aren't
|
|
||||||
// they both WebGL?)
|
|
||||||
const canvas = document.querySelector('canvas');
|
|
||||||
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
|
|
||||||
for (const pixel of expectedBluePixels) {
|
|
||||||
// XXX Possible optimization: call getImageData only once with
|
|
||||||
// area including all pixels to be tested.
|
|
||||||
const data = ctx.getImageData(pixel[0], pixel[1], 1, 1).data;
|
|
||||||
|
|
||||||
// #43b0ffff <-- openmct cyanish-blue with 100% opacity
|
|
||||||
// if (data[0] !== 0x43 || data[1] !== 0xb0 || data[2] !== 0xff || data[3] !== 0xff) {
|
|
||||||
if (data[0] === 0 && data[1] === 0 && data[2] === 0 && data[3] === 0) {
|
|
||||||
// If any pixel is empty, it means we didn't hit a plot point.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(pixelsMatch).toBe(true);
|
|
||||||
}
|
|
@ -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.
|
|
||||||
*****************************************************************************/
|
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
// 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 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();
|
|
||||||
|
|
||||||
// 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 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.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
const { test } = require('../../../fixtures.js');
|
const { test, expect } = require('@playwright/test');
|
||||||
const { 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 }) => {
|
test('validate start time does not exceeds end time', async ({ page }) => {
|
||||||
// Go to baseURL
|
//Go to baseURL
|
||||||
await page.goto('/', { waitUntil: 'networkidle' });
|
await page.goto('/', { waitUntil: 'networkidle' });
|
||||||
const year = new Date().getFullYear();
|
const year = new Date().getFullYear();
|
||||||
|
|
||||||
@ -68,168 +67,3 @@ test.describe('Time conductor operations', () => {
|
|||||||
expect(endDateValidityStatus).not.toBeTruthy();
|
expect(endDateValidityStatus).not.toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Testing instructions:
|
|
||||||
// Try to change the realtime offsets when in realtime (local clock) mode.
|
|
||||||
test.describe('Time conductor input fields real-time mode', () => {
|
|
||||||
test('validate input fields in real-time mode', async ({ page }) => {
|
|
||||||
const startOffset = {
|
|
||||||
secs: '23'
|
|
||||||
};
|
|
||||||
|
|
||||||
const endOffset = {
|
|
||||||
secs: '31'
|
|
||||||
};
|
|
||||||
|
|
||||||
// Go to baseURL
|
|
||||||
await page.goto('/', { waitUntil: 'networkidle' });
|
|
||||||
|
|
||||||
// Switch to real-time mode
|
|
||||||
await setRealTimeMode(page);
|
|
||||||
|
|
||||||
// Set start time offset
|
|
||||||
await setStartOffset(page, startOffset);
|
|
||||||
|
|
||||||
// Verify time was updated on time offset button
|
|
||||||
await expect(page.locator('data-testid=conductor-start-offset-button')).toContainText('00:30:23');
|
|
||||||
|
|
||||||
// Set end time offset
|
|
||||||
await setEndOffset(page, endOffset);
|
|
||||||
|
|
||||||
// Verify time was updated on preceding time offset button
|
|
||||||
await expect(page.locator('data-testid=conductor-end-offset-button')).toContainText('00:00:31');
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verify that offsets and url params are preserved when switching
|
|
||||||
* between fixed timespan and real-time mode.
|
|
||||||
*/
|
|
||||||
test('preserve offsets and url params when switching between fixed and real-time mode', async ({ page }) => {
|
|
||||||
const startOffset = {
|
|
||||||
mins: '30',
|
|
||||||
secs: '23'
|
|
||||||
};
|
|
||||||
|
|
||||||
const endOffset = {
|
|
||||||
secs: '01'
|
|
||||||
};
|
|
||||||
|
|
||||||
// Convert offsets to milliseconds
|
|
||||||
const startDelta = (30 * 60 * 1000) + (23 * 1000);
|
|
||||||
const endDelta = (1 * 1000);
|
|
||||||
|
|
||||||
// Go to baseURL
|
|
||||||
await page.goto('/', { waitUntil: 'networkidle' });
|
|
||||||
|
|
||||||
// Switch to real-time mode
|
|
||||||
await setRealTimeMode(page);
|
|
||||||
|
|
||||||
// Set start time offset
|
|
||||||
await setStartOffset(page, startOffset);
|
|
||||||
|
|
||||||
// Set end time offset
|
|
||||||
await setEndOffset(page, endOffset);
|
|
||||||
|
|
||||||
// Switch to fixed timespan mode
|
|
||||||
await setFixedTimeMode(page);
|
|
||||||
|
|
||||||
// Switch back to real-time mode
|
|
||||||
await setRealTimeMode(page);
|
|
||||||
|
|
||||||
// Verify updated start time offset persists after mode switch
|
|
||||||
await expect(page.locator('data-testid=conductor-start-offset-button')).toContainText('00:30:23');
|
|
||||||
|
|
||||||
// Verify updated end time offset persists after mode switch
|
|
||||||
await expect(page.locator('data-testid=conductor-end-offset-button')).toContainText('00:00:01');
|
|
||||||
|
|
||||||
// Verify url parameters persist after mode switch
|
|
||||||
await page.waitForNavigation();
|
|
||||||
expect(page.url()).toContain(`startDelta=${startDelta}`);
|
|
||||||
expect(page.url()).toContain(`endDelta=${endDelta}`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {Object} OffsetValues
|
|
||||||
* @property {string | undefined} hours
|
|
||||||
* @property {string | undefined} mins
|
|
||||||
* @property {string | undefined} secs
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the values (hours, mins, secs) for the start time offset when in realtime mode
|
|
||||||
* @param {import('@playwright/test').Page} page
|
|
||||||
* @param {OffsetValues} offset
|
|
||||||
*/
|
|
||||||
async function setStartOffset(page, offset) {
|
|
||||||
const startOffsetButton = page.locator('data-testid=conductor-start-offset-button');
|
|
||||||
await setTimeConductorOffset(page, offset, startOffsetButton);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the values (hours, mins, secs) for the end time offset when in realtime mode
|
|
||||||
* @param {import('@playwright/test').Page} page
|
|
||||||
* @param {OffsetValues} offset
|
|
||||||
*/
|
|
||||||
async function setEndOffset(page, offset) {
|
|
||||||
const endOffsetButton = page.locator('data-testid=conductor-end-offset-button');
|
|
||||||
await setTimeConductorOffset(page, offset, endOffsetButton);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the time conductor to fixed timespan mode
|
|
||||||
* @param {import('@playwright/test').Page} page
|
|
||||||
*/
|
|
||||||
async function setFixedTimeMode(page) {
|
|
||||||
await setTimeConductorMode(page, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the time conductor to realtime mode
|
|
||||||
* @param {import('@playwright/test').Page} page
|
|
||||||
*/
|
|
||||||
async function setRealTimeMode(page) {
|
|
||||||
await setTimeConductorMode(page, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the values (hours, mins, secs) for the TimeConductor offsets when in realtime mode
|
|
||||||
* @param {import('@playwright/test').Page} page
|
|
||||||
* @param {OffsetValues} offset
|
|
||||||
* @param {import('@playwright/test').Locator} offsetButton
|
|
||||||
*/
|
|
||||||
async function setTimeConductorOffset(page, {hours, mins, secs}, offsetButton) {
|
|
||||||
await offsetButton.click();
|
|
||||||
|
|
||||||
if (hours) {
|
|
||||||
await page.fill('.pr-time-controls__hrs', hours);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mins) {
|
|
||||||
await page.fill('.pr-time-controls__mins', mins);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (secs) {
|
|
||||||
await page.fill('.pr-time-controls__secs', secs);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Click the check button
|
|
||||||
await page.locator('.icon-check').click();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the time conductor mode to either fixed timespan or realtime mode.
|
|
||||||
* @param {import('@playwright/test').Page} page
|
|
||||||
* @param {boolean} [isFixedTimespan=true] true for fixed timespan mode, false for realtime mode; default is true
|
|
||||||
*/
|
|
||||||
async function setTimeConductorMode(page, isFixedTimespan = true) {
|
|
||||||
// Click 'mode' button
|
|
||||||
await page.locator('.c-mode-button').click();
|
|
||||||
|
|
||||||
// Switch time conductor mode
|
|
||||||
if (isFixedTimespan) {
|
|
||||||
await page.locator('data-testid=conductor-modeOption-fixed').click();
|
|
||||||
} else {
|
|
||||||
await page.locator('data-testid=conductor-modeOption-realtime').click();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
"cookies": [],
|
|
||||||
"origins": [
|
|
||||||
{
|
|
||||||
"origin": "http://localhost:8080",
|
|
||||||
"localStorage": [
|
|
||||||
{
|
|
||||||
"name": "tcHistory",
|
|
||||||
"value": "{\"utc\":[{\"start\":1652301954635,\"end\":1652303754635}]}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "mct",
|
|
||||||
"value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1652303756008,\"modified\":1652303756007},\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"73f2d9ae-d1f3-4561-b7fc-ecd5df557249\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1652303755999,\"location\":\"mine\",\"persisted\":1652303756002}}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "mct-tree-expanded",
|
|
||||||
"value": "[]"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@ -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.
|
Make no assumptions about the order that elements appear in the DOM.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { test } = require('../fixtures.js');
|
const { test, expect } = require('@playwright/test');
|
||||||
const { expect } = require('@playwright/test');
|
|
||||||
|
|
||||||
test('Verify that the create button appears and that the Folder Domain Object is available for selection', async ({ page }) => {
|
test('Verify that the create button appears and that the Folder Domain Object is available for selection', async ({ page }) => {
|
||||||
|
|
@ -47,10 +47,7 @@ test.beforeEach(async ({ context }) => {
|
|||||||
path: path.join(__dirname, '../../..', './node_modules/sinon/pkg/sinon.js')
|
path: path.join(__dirname, '../../..', './node_modules/sinon/pkg/sinon.js')
|
||||||
});
|
});
|
||||||
await context.addInitScript(() => {
|
await context.addInitScript(() => {
|
||||||
window.__clock = sinon.useFakeTimers({
|
window.__clock = sinon.useFakeTimers(); //Set browser clock to UNIX Epoch
|
||||||
now: 0,
|
|
||||||
shouldAdvanceTime: true
|
|
||||||
}); //Set browser clock to UNIX Epoch
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -59,7 +56,8 @@ test('Visual - Root and About', async ({ page }) => {
|
|||||||
await page.goto('/', { waitUntil: 'networkidle' });
|
await page.goto('/', { waitUntil: 'networkidle' });
|
||||||
|
|
||||||
// Verify that Create button is actionable
|
// 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
|
// Take a snapshot of the Dashboard
|
||||||
await page.waitForTimeout(VISUAL_GRACE_PERIOD);
|
await page.waitForTimeout(VISUAL_GRACE_PERIOD);
|
||||||
@ -173,36 +171,3 @@ test('Visual - Sine Wave Generator Form', async ({ page }) => {
|
|||||||
await page.waitForTimeout(VISUAL_GRACE_PERIOD);
|
await page.waitForTimeout(VISUAL_GRACE_PERIOD);
|
||||||
await percySnapshot(page, 'removed amplitude property value');
|
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');
|
|
||||||
|
|
||||||
});
|
|
@ -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 EventEmitter from 'EventEmitter';
|
||||||
import { v4 as uuid } from 'uuid';
|
import uuid from 'uuid';
|
||||||
import createExampleUser from './exampleUserCreator';
|
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 {
|
export default class ExampleUserProvider extends EventEmitter {
|
||||||
constructor(openmct, {defaultStatusRole} = {defaultStatusRole: undefined}) {
|
constructor(openmct) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.openmct = openmct;
|
this.openmct = openmct;
|
||||||
this.user = undefined;
|
this.user = undefined;
|
||||||
this.loggedIn = false;
|
this.loggedIn = false;
|
||||||
this.autoLoginUser = undefined;
|
this.autoLoginUser = undefined;
|
||||||
this.status = STATUSES[1];
|
|
||||||
this.pollQuestion = undefined;
|
|
||||||
this.defaultStatusRole = defaultStatusRole;
|
|
||||||
|
|
||||||
this.ExampleUser = createExampleUser(this.openmct.user.User);
|
this.ExampleUser = createExampleUser(this.openmct.user.User);
|
||||||
this.loginPromise = undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoggedIn() {
|
isLoggedIn() {
|
||||||
@ -82,19 +45,11 @@ export default class ExampleUserProvider extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getCurrentUser() {
|
getCurrentUser() {
|
||||||
if (!this.loginPromise) {
|
if (this.loggedIn) {
|
||||||
this.loginPromise = this._login().then(() => this.user);
|
return Promise.resolve(this.user);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.loginPromise;
|
return this._login().then(() => this.user);
|
||||||
}
|
|
||||||
|
|
||||||
canProvideStatusForRole() {
|
|
||||||
return Promise.resolve(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
canSetPollQuestion() {
|
|
||||||
return Promise.resolve(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hasRole(roleId) {
|
hasRole(roleId) {
|
||||||
@ -105,55 +60,6 @@ export default class ExampleUserProvider extends EventEmitter {
|
|||||||
return Promise.resolve(this.user.getRoles().includes(roleId));
|
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() {
|
_login() {
|
||||||
const id = uuid();
|
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';
|
import ExampleUserProvider from './ExampleUserProvider';
|
||||||
|
|
||||||
export default function ExampleUserPlugin({autoLoginUser, defaultStatusRole} = {
|
export default function ExampleUserPlugin() {
|
||||||
autoLoginUser: 'guest',
|
|
||||||
defaultStatusRole: 'test-role'
|
|
||||||
}) {
|
|
||||||
return function install(openmct) {
|
return function install(openmct) {
|
||||||
const userProvider = new ExampleUserProvider(openmct, {
|
openmct.user.setProvider(new ExampleUserProvider(openmct));
|
||||||
defaultStatusRole
|
|
||||||
});
|
|
||||||
|
|
||||||
if (autoLoginUser !== undefined) {
|
|
||||||
userProvider.autoLogin(autoLoginUser);
|
|
||||||
}
|
|
||||||
|
|
||||||
openmct.user.setProvider(userProvider);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ import {
|
|||||||
} from '../../src/utils/testing';
|
} from '../../src/utils/testing';
|
||||||
import ExampleUserProvider from './ExampleUserProvider';
|
import ExampleUserProvider from './ExampleUserProvider';
|
||||||
|
|
||||||
describe("The Example User Plugin", () => {
|
xdescribe("The Example User Plugin", () => {
|
||||||
let openmct;
|
let openmct;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@ -47,4 +47,9 @@ describe("The Example User Plugin", () => {
|
|||||||
});
|
});
|
||||||
openmct.install(openmct.plugins.example.ExampleUser());
|
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",
|
key: "cos",
|
||||||
name: "Wavelength",
|
name: "Cosine",
|
||||||
unit: "nm",
|
unit: "deg",
|
||||||
format: 'string[]',
|
formatString: '%0.2f',
|
||||||
hints: {
|
hints: {
|
||||||
range: 4
|
domain: 3
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Need to enable "LocalTimeSystem" plugin to make use of this
|
// Need to enable "LocalTimeSystem" plugin to make use of this
|
||||||
@ -64,14 +64,6 @@ define([
|
|||||||
hints: {
|
hints: {
|
||||||
range: 2
|
range: 2
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "intensities",
|
|
||||||
name: "Intensities",
|
|
||||||
format: 'number[]',
|
|
||||||
hints: {
|
|
||||||
range: 3
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
define([
|
define([
|
||||||
'uuid'
|
'uuid'
|
||||||
], function (
|
], function (
|
||||||
{ v4: uuid }
|
uuid
|
||||||
) {
|
) {
|
||||||
function WorkerInterface(openmct) {
|
function WorkerInterface(openmct) {
|
||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
|
@ -77,8 +77,7 @@
|
|||||||
utc: nextStep,
|
utc: nextStep,
|
||||||
yesterday: nextStep - 60 * 60 * 24 * 1000,
|
yesterday: nextStep - 60 * 60 * 24 * 1000,
|
||||||
sin: sin(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness),
|
sin: sin(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness),
|
||||||
wavelengths: wavelengths(),
|
wavelength: wavelength(start, nextStep),
|
||||||
intensities: intensities(),
|
|
||||||
cos: cos(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness)
|
cos: cos(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -127,8 +126,7 @@
|
|||||||
utc: nextStep,
|
utc: nextStep,
|
||||||
yesterday: nextStep - 60 * 60 * 24 * 1000,
|
yesterday: nextStep - 60 * 60 * 24 * 1000,
|
||||||
sin: sin(nextStep, period, amplitude, offset, phase, randomness),
|
sin: sin(nextStep, period, amplitude, offset, phase, randomness),
|
||||||
wavelengths: wavelengths(),
|
wavelength: wavelength(start, nextStep),
|
||||||
intensities: intensities(),
|
|
||||||
cos: cos(nextStep, period, amplitude, offset, phase, randomness)
|
cos: cos(nextStep, period, amplitude, offset, phase, randomness)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -156,28 +154,8 @@
|
|||||||
* Math.sin(phase + (timestamp / period / 1000 * Math.PI * 2)) + (amplitude * Math.random() * randomness) + offset;
|
* Math.sin(phase + (timestamp / period / 1000 * Math.PI * 2)) + (amplitude * Math.random() * randomness) + offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
function wavelengths() {
|
function wavelength(start, nextStep) {
|
||||||
let values = [];
|
return (nextStep - start) / 10;
|
||||||
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 sendError(error, message) {
|
function sendError(error, message) {
|
||||||
|
@ -59,8 +59,7 @@ export default function () {
|
|||||||
object.configuration = {
|
object.configuration = {
|
||||||
imageLocation: '',
|
imageLocation: '',
|
||||||
imageLoadDelayInMilliSeconds: DEFAULT_IMAGE_LOAD_DELAY_IN_MILISECONDS,
|
imageLoadDelayInMilliSeconds: DEFAULT_IMAGE_LOAD_DELAY_IN_MILISECONDS,
|
||||||
imageSamples: [],
|
imageSamples: []
|
||||||
layers: []
|
|
||||||
};
|
};
|
||||||
|
|
||||||
object.telemetry = {
|
object.telemetry = {
|
||||||
@ -91,21 +90,7 @@ export default function () {
|
|||||||
format: 'image',
|
format: 'image',
|
||||||
hints: {
|
hints: {
|
||||||
image: 1
|
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',
|
name: 'Image Download Name',
|
||||||
|
@ -75,12 +75,12 @@
|
|||||||
const TWO_HOURS = ONE_HOUR * 2;
|
const TWO_HOURS = ONE_HOUR * 2;
|
||||||
const ONE_DAY = ONE_HOUR * 24;
|
const ONE_DAY = ONE_HOUR * 24;
|
||||||
|
|
||||||
openmct.install(openmct.plugins.LocalStorage());
|
|
||||||
|
|
||||||
|
openmct.install(openmct.plugins.LocalStorage());
|
||||||
|
|
||||||
openmct.install(openmct.plugins.example.Generator());
|
openmct.install(openmct.plugins.example.Generator());
|
||||||
openmct.install(openmct.plugins.example.EventGeneratorPlugin());
|
openmct.install(openmct.plugins.example.EventGeneratorPlugin());
|
||||||
openmct.install(openmct.plugins.example.ExampleImagery());
|
openmct.install(openmct.plugins.example.ExampleImagery());
|
||||||
openmct.install(openmct.plugins.example.ExampleTags());
|
|
||||||
|
|
||||||
openmct.install(openmct.plugins.Espresso());
|
openmct.install(openmct.plugins.Espresso());
|
||||||
openmct.install(openmct.plugins.MyItems());
|
openmct.install(openmct.plugins.MyItems());
|
||||||
@ -191,13 +191,10 @@
|
|||||||
openmct.install(openmct.plugins.ObjectMigration());
|
openmct.install(openmct.plugins.ObjectMigration());
|
||||||
openmct.install(openmct.plugins.ClearData(
|
openmct.install(openmct.plugins.ClearData(
|
||||||
['table', 'telemetry.plot.overlay', 'telemetry.plot.stacked', 'example.imagery'],
|
['table', 'telemetry.plot.overlay', 'telemetry.plot.stacked', 'example.imagery'],
|
||||||
{ indicator: true }
|
{indicator: true}
|
||||||
));
|
));
|
||||||
openmct.install(openmct.plugins.Clock({ enableClockIndicator: true }));
|
openmct.install(openmct.plugins.Clock({ enableClockIndicator: true }));
|
||||||
openmct.install(openmct.plugins.Timer());
|
openmct.install(openmct.plugins.Timer());
|
||||||
openmct.install(openmct.plugins.Timelist());
|
|
||||||
openmct.install(openmct.plugins.BarChart());
|
|
||||||
openmct.install(openmct.plugins.ScatterPlot());
|
|
||||||
openmct.start();
|
openmct.start();
|
||||||
</script>
|
</script>
|
||||||
</html>
|
</html>
|
||||||
|
73
package.json
73
package.json
@ -1,86 +1,90 @@
|
|||||||
{
|
{
|
||||||
"name": "openmct",
|
"name": "openmct",
|
||||||
"version": "2.0.5",
|
"version": "2.0.2-SNAPSHOT",
|
||||||
"description": "The Open MCT core platform",
|
"description": "The Open MCT core platform",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/eslint-parser": "7.18.2",
|
"@babel/eslint-parser": "7.16.3",
|
||||||
"@braintree/sanitize-url": "6.0.0",
|
"@braintree/sanitize-url": "6.0.0",
|
||||||
"@percy/cli": "1.2.1",
|
"@percy/cli": "1.0.0-beta.76",
|
||||||
"@percy/playwright": "1.0.4",
|
"@percy/playwright": "1.0.1",
|
||||||
"@playwright/test": "1.21.1",
|
"@playwright/test": "1.19.2",
|
||||||
"@types/eventemitter3": "^1.0.0",
|
"@types/eventemitter3": "^1.0.0",
|
||||||
"@types/jasmine": "^4.0.1",
|
"@types/jasmine": "^4.0.1",
|
||||||
"@types/karma": "^6.3.2",
|
"@types/karma": "^6.3.2",
|
||||||
"@types/lodash": "^4.14.178",
|
"@types/lodash": "^4.14.178",
|
||||||
"@types/mocha": "^9.1.0",
|
"@types/mocha": "^9.1.0",
|
||||||
|
"allure-playwright": "2.0.0-beta.15",
|
||||||
"babel-loader": "8.2.3",
|
"babel-loader": "8.2.3",
|
||||||
"babel-plugin-istanbul": "6.1.1",
|
"babel-plugin-istanbul": "6.1.1",
|
||||||
"comma-separated-values": "3.6.4",
|
"comma-separated-values": "3.6.4",
|
||||||
"copy-webpack-plugin": "11.0.0",
|
"copy-webpack-plugin": "10.2.0",
|
||||||
|
"core-js": "3.21.1",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"css-loader": "4.0.0",
|
"css-loader": "4.0.0",
|
||||||
"d3-axis": "3.0.0",
|
"d3-axis": "1.0.x",
|
||||||
"d3-scale": "3.3.0",
|
"d3-scale": "1.0.x",
|
||||||
"d3-selection": "3.0.0",
|
"d3-selection": "1.3.x",
|
||||||
"eslint": "8.13.0",
|
"eslint": "8.11.0",
|
||||||
"eslint-plugin-compat": "4.0.2",
|
"eslint-plugin-compat": "4.0.2",
|
||||||
"eslint-plugin-playwright": "0.9.0",
|
"eslint-plugin-playwright": "0.8.0",
|
||||||
"eslint-plugin-vue": "9.1.0",
|
"eslint-plugin-vue": "8.5.0",
|
||||||
"eslint-plugin-you-dont-need-lodash-underscore": "6.12.0",
|
"eslint-plugin-you-dont-need-lodash-underscore": "6.12.0",
|
||||||
"eventemitter3": "1.2.0",
|
"eventemitter3": "1.2.0",
|
||||||
|
"exports-loader": "0.7.0",
|
||||||
"express": "4.13.1",
|
"express": "4.13.1",
|
||||||
"file-saver": "2.0.5",
|
"file-saver": "2.0.5",
|
||||||
"git-rev-sync": "3.0.2",
|
"git-rev-sync": "3.0.2",
|
||||||
"html2canvas": "1.4.1",
|
"html2canvas": "1.4.1",
|
||||||
"imports-loader": "0.8.0",
|
"imports-loader": "0.8.0",
|
||||||
"jasmine-core": "4.1.1",
|
"jasmine-core": "4.0.1",
|
||||||
"jsdoc": "3.5.5",
|
"jsdoc": "3.5.5",
|
||||||
"karma": "6.3.20",
|
"karma": "6.3.15",
|
||||||
"karma-chrome-launcher": "3.1.1",
|
"karma-chrome-launcher": "3.1.1",
|
||||||
"karma-cli": "2.0.0",
|
"karma-cli": "2.0.0",
|
||||||
"karma-coverage": "2.2.0",
|
"karma-coverage": "2.1.1",
|
||||||
"karma-coverage-istanbul-reporter": "3.0.3",
|
"karma-coverage-istanbul-reporter": "3.0.3",
|
||||||
"karma-firefox-launcher": "2.1.2",
|
"karma-firefox-launcher": "2.1.2",
|
||||||
"karma-jasmine": "4.0.1",
|
"karma-jasmine": "4.0.1",
|
||||||
"karma-junit-reporter": "2.0.1",
|
"karma-junit-reporter": "2.0.1",
|
||||||
"karma-sourcemap-loader": "0.3.8",
|
"karma-sourcemap-loader": "0.3.8",
|
||||||
"karma-spec-reporter": "0.0.34",
|
"karma-spec-reporter": "0.0.33",
|
||||||
"karma-webpack": "5.0.0",
|
"karma-webpack": "5.0.0",
|
||||||
"lighthouse": "9.6.1",
|
"lighthouse": "9.5.0",
|
||||||
"location-bar": "3.0.1",
|
"location-bar": "3.0.1",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"mini-css-extract-plugin": "2.6.0",
|
"mini-css-extract-plugin": "2.6.0",
|
||||||
"moment": "2.29.3",
|
"moment": "2.29.1",
|
||||||
"moment-duration-format": "2.3.2",
|
"moment-duration-format": "2.2.2",
|
||||||
"moment-timezone": "0.5.34",
|
"moment-timezone": "0.5.34",
|
||||||
"node-bourbon": "4.2.3",
|
"node-bourbon": "4.2.3",
|
||||||
"painterro": "1.2.56",
|
"painterro": "1.2.56",
|
||||||
"plotly.js-basic-dist": "2.12.0",
|
"plotly.js-basic-dist": "2.5.0",
|
||||||
"plotly.js-gl2d-dist": "2.12.0",
|
"plotly.js-gl2d-dist": "2.5.0",
|
||||||
"printj": "1.3.1",
|
"printj": "1.3.1",
|
||||||
"request": "2.88.2",
|
"request": "2.88.2",
|
||||||
"resolve-url-loader": "5.0.0",
|
"resolve-url-loader": "4.0.0",
|
||||||
"sass": "1.52.2",
|
"sass": "1.49.9",
|
||||||
"sass-loader": "12.6.0",
|
"sass-loader": "12.6.0",
|
||||||
"sinon": "14.0.0",
|
"sinon": "13.0.1",
|
||||||
"style-loader": "^1.0.1",
|
"style-loader": "^1.0.1",
|
||||||
"uuid": "8.3.2",
|
"uuid": "3.3.3",
|
||||||
"vue": "2.6.14",
|
"vue": "2.6.14",
|
||||||
"vue-eslint-parser": "9.0.2",
|
"vue-eslint-parser": "8.2.0",
|
||||||
"vue-loader": "15.9.8",
|
"vue-loader": "15.9.8",
|
||||||
"vue-template-compiler": "2.6.14",
|
"vue-template-compiler": "2.6.14",
|
||||||
"webpack": "5.68.0",
|
"webpack": "5.68.0",
|
||||||
"webpack-cli": "4.9.2",
|
"webpack-cli": "4.9.2",
|
||||||
"webpack-dev-middleware": "5.3.3",
|
"webpack-dev-middleware": "5.3.1",
|
||||||
"webpack-hot-middleware": "2.25.1",
|
"webpack-hot-middleware": "2.25.1",
|
||||||
"webpack-merge": "5.8.0"
|
"webpack-merge": "5.8.0",
|
||||||
|
"zepto": "1.2.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rm -rf ./dist ./node_modules ./package-lock.json",
|
"clean": "rm -rf ./dist ./node_modules ./package-lock.json",
|
||||||
"clean-test-lint": "npm run clean; npm install; npm run test; npm run lint",
|
"clean-test-lint": "npm run clean; npm install; npm run test; npm run lint",
|
||||||
"start": "node app.js",
|
"start": "node app.js",
|
||||||
"lint": "eslint example src e2e --ext .js,.vue openmct.js --max-warnings=0",
|
"lint": "eslint example src --ext .js,.vue openmct.js",
|
||||||
"lint:fix": "eslint example src e2e --ext .js,.vue openmct.js --fix",
|
"lint:fix": "eslint example src --ext .js,.vue openmct.js --fix",
|
||||||
"build:prod": "cross-env webpack --config webpack.prod.js",
|
"build:prod": "cross-env webpack --config webpack.prod.js",
|
||||||
"build:dev": "webpack --config webpack.dev.js",
|
"build:dev": "webpack --config webpack.dev.js",
|
||||||
"build:coverage": "webpack --config webpack.coverage.js",
|
"build:coverage": "webpack --config webpack.coverage.js",
|
||||||
@ -89,12 +93,10 @@
|
|||||||
"test": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run",
|
"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: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:debug": "cross-env NODE_ENV=debug karma start --no-single-run",
|
||||||
"test:e2e:ci": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome smoke branding default condition timeConductor clock exampleImagery persistence performance",
|
"test:e2e:ci": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome smoke default condition timeConductor",
|
||||||
"test:e2e:local": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome",
|
"test:e2e:local": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome",
|
||||||
"test:e2e:updatesnapshots": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome --grep @snapshot --update-snapshots",
|
"test:e2e:visual": "percy exec --config ./e2e/.percy.yml -- npx playwright test --config=e2e/playwright-visual.config.js default",
|
||||||
"test:e2e:visual": "percy exec --config ./e2e/.percy.yml -- npx playwright test --config=e2e/playwright-visual.config.js",
|
|
||||||
"test:e2e:full": "npx playwright test --config=e2e/playwright-ci.config.js",
|
"test:e2e:full": "npx playwright test --config=e2e/playwright-ci.config.js",
|
||||||
"test:perf": "npx playwright test --config=e2e/playwright-performance.config.js",
|
|
||||||
"test:watch": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --no-single-run",
|
"test:watch": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --no-single-run",
|
||||||
"jsdoc": "jsdoc -c jsdoc.json -R API.md -r -d dist/docs/api",
|
"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-about-dialog-copyright": "perl -pi -e 's/20\\d\\d\\-202\\d/2014\\-2022/gm' ./src/ui/layout/AboutDialog.vue",
|
||||||
@ -110,9 +112,6 @@
|
|||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.19.1"
|
"node": ">=14.19.1"
|
||||||
},
|
},
|
||||||
"overrides": {
|
|
||||||
"core-js": "3.21.1"
|
|
||||||
},
|
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"Firefox ESR",
|
"Firefox ESR",
|
||||||
"not IE 11",
|
"not IE 11",
|
||||||
|
15
src/MCT.js
15
src/MCT.js
@ -42,7 +42,6 @@ define([
|
|||||||
'./plugins/duplicate/plugin',
|
'./plugins/duplicate/plugin',
|
||||||
'./plugins/importFromJSONAction/plugin',
|
'./plugins/importFromJSONAction/plugin',
|
||||||
'./plugins/exportAsJSONAction/plugin',
|
'./plugins/exportAsJSONAction/plugin',
|
||||||
'./ui/components/components',
|
|
||||||
'vue'
|
'vue'
|
||||||
], function (
|
], function (
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
@ -66,7 +65,6 @@ define([
|
|||||||
DuplicateActionPlugin,
|
DuplicateActionPlugin,
|
||||||
ImportFromJSONAction,
|
ImportFromJSONAction,
|
||||||
ExportAsJSONAction,
|
ExportAsJSONAction,
|
||||||
components,
|
|
||||||
Vue
|
Vue
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
@ -238,22 +236,14 @@ define([
|
|||||||
this.priority = api.PriorityAPI;
|
this.priority = api.PriorityAPI;
|
||||||
|
|
||||||
this.router = new ApplicationRouter(this);
|
this.router = new ApplicationRouter(this);
|
||||||
this.faults = new api.FaultManagementAPI.default(this);
|
|
||||||
this.forms = new api.FormsAPI.default(this);
|
this.forms = new api.FormsAPI.default(this);
|
||||||
|
|
||||||
this.branding = BrandingAPI.default;
|
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
|
// Plugins that are installed by default
|
||||||
|
|
||||||
this.install(this.plugins.Plot());
|
this.install(this.plugins.Plot());
|
||||||
|
this.install(this.plugins.Chart());
|
||||||
this.install(this.plugins.TelemetryTable.default());
|
this.install(this.plugins.TelemetryTable.default());
|
||||||
this.install(PreviewPlugin.default());
|
this.install(PreviewPlugin.default());
|
||||||
this.install(LicensesPlugin.default());
|
this.install(LicensesPlugin.default());
|
||||||
@ -389,7 +379,6 @@ define([
|
|||||||
};
|
};
|
||||||
|
|
||||||
MCT.prototype.plugins = plugins;
|
MCT.prototype.plugins = plugins;
|
||||||
MCT.prototype.components = components.default;
|
|
||||||
|
|
||||||
return MCT;
|
return MCT;
|
||||||
});
|
});
|
||||||
|
@ -1,275 +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: [],
|
|
||||||
contentText,
|
|
||||||
targets
|
|
||||||
};
|
|
||||||
existingAnnotation = await this.create(annotationCreationArguments);
|
|
||||||
}
|
|
||||||
|
|
||||||
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',
|
'./actions/ActionsAPI',
|
||||||
'./composition/CompositionAPI',
|
'./composition/CompositionAPI',
|
||||||
'./Editor',
|
'./Editor',
|
||||||
'./faultmanagement/FaultManagementAPI',
|
|
||||||
'./forms/FormsAPI',
|
'./forms/FormsAPI',
|
||||||
'./indicators/IndicatorAPI',
|
'./indicators/IndicatorAPI',
|
||||||
'./menu/MenuAPI',
|
'./menu/MenuAPI',
|
||||||
@ -35,13 +34,11 @@ define([
|
|||||||
'./telemetry/TelemetryAPI',
|
'./telemetry/TelemetryAPI',
|
||||||
'./time/TimeAPI',
|
'./time/TimeAPI',
|
||||||
'./types/TypeRegistry',
|
'./types/TypeRegistry',
|
||||||
'./user/UserAPI',
|
'./user/UserAPI'
|
||||||
'./annotation/AnnotationAPI'
|
|
||||||
], function (
|
], function (
|
||||||
ActionsAPI,
|
ActionsAPI,
|
||||||
CompositionAPI,
|
CompositionAPI,
|
||||||
EditorAPI,
|
EditorAPI,
|
||||||
FaultManagementAPI,
|
|
||||||
FormsAPI,
|
FormsAPI,
|
||||||
IndicatorAPI,
|
IndicatorAPI,
|
||||||
MenuAPI,
|
MenuAPI,
|
||||||
@ -52,16 +49,14 @@ define([
|
|||||||
TelemetryAPI,
|
TelemetryAPI,
|
||||||
TimeAPI,
|
TimeAPI,
|
||||||
TypeRegistry,
|
TypeRegistry,
|
||||||
UserAPI,
|
UserAPI
|
||||||
AnnotationAPI
|
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
ActionsAPI: ActionsAPI.default,
|
ActionsAPI: ActionsAPI.default,
|
||||||
CompositionAPI: CompositionAPI,
|
CompositionAPI: CompositionAPI,
|
||||||
EditorAPI: EditorAPI,
|
EditorAPI: EditorAPI,
|
||||||
FaultManagementAPI: FaultManagementAPI,
|
|
||||||
FormsAPI: FormsAPI,
|
FormsAPI: FormsAPI,
|
||||||
IndicatorAPI: IndicatorAPI.default,
|
IndicatorAPI: IndicatorAPI,
|
||||||
MenuAPI: MenuAPI.default,
|
MenuAPI: MenuAPI.default,
|
||||||
NotificationAPI: NotificationAPI.default,
|
NotificationAPI: NotificationAPI.default,
|
||||||
ObjectAPI: ObjectAPI,
|
ObjectAPI: ObjectAPI,
|
||||||
@ -70,7 +65,6 @@ define([
|
|||||||
TelemetryAPI: TelemetryAPI,
|
TelemetryAPI: TelemetryAPI,
|
||||||
TimeAPI: TimeAPI.default,
|
TimeAPI: TimeAPI.default,
|
||||||
TypeRegistry: TypeRegistry,
|
TypeRegistry: TypeRegistry,
|
||||||
UserAPI: UserAPI.default,
|
UserAPI: UserAPI.default
|
||||||
AnnotationAPI: AnnotationAPI.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();
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
@ -1,6 +1,5 @@
|
|||||||
import AutoCompleteField from './components/controls/AutoCompleteField.vue';
|
import AutoCompleteField from './components/controls/AutoCompleteField.vue';
|
||||||
import ClockDisplayFormatField from './components/controls/ClockDisplayFormatField.vue';
|
import ClockDisplayFormatField from './components/controls/ClockDisplayFormatField.vue';
|
||||||
import CheckBoxField from './components/controls/CheckBoxField.vue';
|
|
||||||
import Datetime from './components/controls/Datetime.vue';
|
import Datetime from './components/controls/Datetime.vue';
|
||||||
import FileInput from './components/controls/FileInput.vue';
|
import FileInput from './components/controls/FileInput.vue';
|
||||||
import Locator from './components/controls/Locator.vue';
|
import Locator from './components/controls/Locator.vue';
|
||||||
@ -8,13 +7,11 @@ import NumberField from './components/controls/NumberField.vue';
|
|||||||
import SelectField from './components/controls/SelectField.vue';
|
import SelectField from './components/controls/SelectField.vue';
|
||||||
import TextAreaField from './components/controls/TextAreaField.vue';
|
import TextAreaField from './components/controls/TextAreaField.vue';
|
||||||
import TextField from './components/controls/TextField.vue';
|
import TextField from './components/controls/TextField.vue';
|
||||||
import ToggleSwitchField from './components/controls/ToggleSwitchField.vue';
|
|
||||||
|
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
|
||||||
export const DEFAULT_CONTROLS_MAP = {
|
export const DEFAULT_CONTROLS_MAP = {
|
||||||
'autocomplete': AutoCompleteField,
|
'autocomplete': AutoCompleteField,
|
||||||
'checkbox': CheckBoxField,
|
|
||||||
'composite': ClockDisplayFormatField,
|
'composite': ClockDisplayFormatField,
|
||||||
'datetime': Datetime,
|
'datetime': Datetime,
|
||||||
'file-input': FileInput,
|
'file-input': FileInput,
|
||||||
@ -22,8 +19,7 @@ export const DEFAULT_CONTROLS_MAP = {
|
|||||||
'numberfield': NumberField,
|
'numberfield': NumberField,
|
||||||
'select': SelectField,
|
'select': SelectField,
|
||||||
'textarea': TextAreaField,
|
'textarea': TextAreaField,
|
||||||
'textfield': TextField,
|
'textfield': TextField
|
||||||
'toggleSwitch': ToggleSwitchField
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class FormControl {
|
export default class FormControl {
|
||||||
@ -98,3 +94,4 @@ export default class FormControl {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,13 +23,10 @@
|
|||||||
import FormController from './FormController';
|
import FormController from './FormController';
|
||||||
import FormProperties from './components/FormProperties.vue';
|
import FormProperties from './components/FormProperties.vue';
|
||||||
|
|
||||||
import EventEmitter from 'EventEmitter';
|
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
|
||||||
export default class FormsAPI extends EventEmitter {
|
export default class FormsAPI {
|
||||||
constructor(openmct) {
|
constructor(openmct) {
|
||||||
super();
|
|
||||||
|
|
||||||
this.openmct = openmct;
|
this.openmct = openmct;
|
||||||
this.formController = new FormController(openmct);
|
this.formController = new FormController(openmct);
|
||||||
}
|
}
|
||||||
@ -110,8 +107,6 @@ export default class FormsAPI extends EventEmitter {
|
|||||||
let onDismiss;
|
let onDismiss;
|
||||||
let onSave;
|
let onSave;
|
||||||
|
|
||||||
const self = this;
|
|
||||||
|
|
||||||
const promise = new Promise((resolve, reject) => {
|
const promise = new Promise((resolve, reject) => {
|
||||||
onSave = onFormSave(resolve);
|
onSave = onFormSave(resolve);
|
||||||
onDismiss = onFormDismiss(reject);
|
onDismiss = onFormDismiss(reject);
|
||||||
@ -120,7 +115,7 @@ export default class FormsAPI extends EventEmitter {
|
|||||||
const vm = new Vue({
|
const vm = new Vue({
|
||||||
components: { FormProperties },
|
components: { FormProperties },
|
||||||
provide: {
|
provide: {
|
||||||
openmct: self.openmct
|
openmct: this.openmct
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -137,7 +132,7 @@ export default class FormsAPI extends EventEmitter {
|
|||||||
if (element) {
|
if (element) {
|
||||||
element.append(formElement);
|
element.append(formElement);
|
||||||
} else {
|
} else {
|
||||||
overlay = self.openmct.overlays.overlay({
|
overlay = this.openmct.overlays.overlay({
|
||||||
element: vm.$el,
|
element: vm.$el,
|
||||||
size: 'small',
|
size: 'small',
|
||||||
onDestroy: () => vm.$destroy()
|
onDestroy: () => vm.$destroy()
|
||||||
@ -145,7 +140,6 @@ export default class FormsAPI extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onFormPropertyChange(data) {
|
function onFormPropertyChange(data) {
|
||||||
self.emit('onFormPropertyChange', data);
|
|
||||||
if (onChange) {
|
if (onChange) {
|
||||||
onChange(data);
|
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>
|
<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__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 class="c-overlay__dialog-hint hint">All fields marked <span class="req icon-asterisk"></span> are required.</div>
|
||||||
</div>
|
</div>
|
||||||
<form
|
<form
|
||||||
@ -44,14 +44,18 @@
|
|||||||
>
|
>
|
||||||
{{ section.name }}
|
{{ section.name }}
|
||||||
</h2>
|
</h2>
|
||||||
<FormRow
|
<div
|
||||||
v-for="(row, index) in section.rows"
|
v-for="(row, index) in section.rows"
|
||||||
:key="row.id"
|
:key="row.id"
|
||||||
:css-class="row.cssClass"
|
class="u-contents"
|
||||||
:first="index < 1"
|
>
|
||||||
:row="row"
|
<FormRow
|
||||||
@onChange="onChange"
|
:css-class="section.cssClass"
|
||||||
/>
|
:first="index < 1"
|
||||||
|
:row="row"
|
||||||
|
@onChange="onChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@ -66,7 +70,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
class="c-button js-cancel-button"
|
class="c-button"
|
||||||
@click="onDismiss"
|
@click="onDismiss"
|
||||||
>
|
>
|
||||||
{{ cancelLabel }}
|
{{ cancelLabel }}
|
||||||
@ -77,7 +81,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import FormRow from "@/api/forms/components/FormRow.vue";
|
import FormRow from "@/api/forms/components/FormRow.vue";
|
||||||
import { v4 as uuid } from 'uuid';
|
import uuid from 'uuid';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -23,10 +23,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="form-row c-form__row"
|
class="form-row c-form__row"
|
||||||
:class="[
|
:class="[{ 'first': first }]"
|
||||||
{ 'first': first },
|
|
||||||
cssClass
|
|
||||||
]"
|
|
||||||
@onChange="onChange"
|
@onChange="onChange"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@ -37,7 +34,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="c-form-row__state-indicator"
|
class="c-form-row__state-indicator"
|
||||||
:class="reqClass"
|
:class="rowClass"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@ -79,22 +76,22 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
reqClass() {
|
rowClass() {
|
||||||
let reqClass = 'req';
|
let cssClass = this.cssClass;
|
||||||
|
|
||||||
if (!this.row.required) {
|
if (this.row.required) {
|
||||||
return;
|
cssClass = `${cssClass} req`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.visited && this.valid !== undefined) {
|
if (this.visited && this.valid !== undefined) {
|
||||||
if (this.valid === true) {
|
if (this.valid === true) {
|
||||||
reqClass = 'valid';
|
cssClass = `${cssClass} valid`;
|
||||||
} else {
|
} else {
|
||||||
reqClass = 'invalid';
|
cssClass = `${cssClass} invalid`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return reqClass;
|
return cssClass;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
@ -19,46 +19,35 @@
|
|||||||
* this source code distribution or the Licensing information page available
|
* this source code distribution or the Licensing information page available
|
||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div class="form-control autocomplete">
|
||||||
ref="autoCompleteForm"
|
<span class="autocompleteInputAndArrow">
|
||||||
class="form-control c-input--autocomplete js-autocomplete"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="c-input--autocomplete__wrapper"
|
|
||||||
>
|
|
||||||
<input
|
<input
|
||||||
ref="autoCompleteInput"
|
|
||||||
v-model="field"
|
v-model="field"
|
||||||
class="c-input--autocomplete__input js-autocomplete__input"
|
class="autocompleteInput"
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="placeHolderText"
|
|
||||||
@click="inputClicked()"
|
@click="inputClicked()"
|
||||||
@keydown="keyDown($event)"
|
@keydown="keyDown($event)"
|
||||||
>
|
>
|
||||||
<div
|
<span
|
||||||
class="icon-arrow-down c-icon-button c-input--autocomplete__afford-arrow js-autocomplete__afford-arrow"
|
class="icon-arrow-down"
|
||||||
@click="arrowClicked()"
|
@click="arrowClicked()"
|
||||||
></div>
|
></span>
|
||||||
</div>
|
</span>
|
||||||
<div
|
<div
|
||||||
v-if="!hideOptions"
|
class="autocompleteOptions"
|
||||||
class="c-menu c-input--autocomplete__options"
|
|
||||||
@blur="hideOptions = true"
|
@blur="hideOptions = true"
|
||||||
>
|
>
|
||||||
<ul>
|
<ul v-if="!hideOptions">
|
||||||
<li
|
<li
|
||||||
v-for="opt in filteredOptions"
|
v-for="opt in filteredOptions"
|
||||||
:key="opt.optionId"
|
:key="opt.optionId"
|
||||||
:class="[
|
:class="{'optionPreSelected': optionIndex === opt.optionId}"
|
||||||
{'optionPreSelected': optionIndex === opt.optionId},
|
|
||||||
itemCssClass
|
|
||||||
]"
|
|
||||||
:style="itemStyle(opt)"
|
|
||||||
@click="fillInputWithString(opt.name)"
|
@click="fillInputWithString(opt.name)"
|
||||||
@mouseover="optionMouseover(opt.optionId)"
|
@mouseover="optionMouseover(opt.optionId)"
|
||||||
>
|
>
|
||||||
{{ opt.name }}
|
<span class="optionText">{{ opt.name }}</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@ -76,23 +65,7 @@ export default {
|
|||||||
props: {
|
props: {
|
||||||
model: {
|
model: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true
|
||||||
default() {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
placeHolderText: {
|
|
||||||
type: String,
|
|
||||||
default() {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
},
|
|
||||||
itemCssClass: {
|
|
||||||
type: String,
|
|
||||||
required: false,
|
|
||||||
default() {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@ -105,40 +78,31 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
filteredOptions() {
|
filteredOptions() {
|
||||||
const fullOptions = this.options || [];
|
const options = this.optionNames || [];
|
||||||
if (this.showFilteredOptions) {
|
if (this.showFilteredOptions) {
|
||||||
const optionsFiltered = fullOptions
|
return options
|
||||||
.filter(option => {
|
.filter(option => {
|
||||||
if (option.name && this.field) {
|
return option.toLowerCase().indexOf(this.field.toLowerCase()) >= 0;
|
||||||
return option.name.toLowerCase().indexOf(this.field.toLowerCase()) >= 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}).map((option, index) => {
|
}).map((option, index) => {
|
||||||
return {
|
return {
|
||||||
optionId: index,
|
optionId: index,
|
||||||
name: option.name,
|
name: option
|
||||||
color: option.color
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return optionsFiltered;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const optionsFiltered = fullOptions.map((option, index) => {
|
return options.map((option, index) => {
|
||||||
return {
|
return {
|
||||||
optionId: index,
|
optionId: index,
|
||||||
name: option.name,
|
name: option
|
||||||
color: option.color
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return optionsFiltered;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
field(newValue, oldValue) {
|
field(newValue, oldValue) {
|
||||||
if (newValue !== oldValue) {
|
if (newValue !== oldValue) {
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
model: this.model,
|
model: this.model,
|
||||||
value: newValue
|
value: newValue
|
||||||
@ -159,17 +123,17 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.autocompleteInputAndArrow = this.$refs.autoCompleteForm;
|
this.options = this.model.options;
|
||||||
this.autocompleteInputElement = this.$refs.autoCompleteInput;
|
this.autocompleteInputAndArrow = this.$el.getElementsByClassName('autocompleteInputAndArrow')[0];
|
||||||
if (this.model.options && this.model.options.length && !this.model.options[0].name) {
|
this.autocompleteInputElement = this.$el.getElementsByClassName('autocompleteInput')[0];
|
||||||
// If options is only an array of string.
|
if (this.options[0].name) {
|
||||||
this.options = this.model.options.map((option) => {
|
// If "options" include name, value pair
|
||||||
return {
|
this.optionNames = this.options.map((opt) => {
|
||||||
name: option
|
return opt.name;
|
||||||
};
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.options = this.model.options;
|
// If options is only an array of string.
|
||||||
|
this.optionNames = this.options;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
destroyed() {
|
destroyed() {
|
||||||
@ -258,12 +222,6 @@ export default {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
|
||||||
itemStyle(option) {
|
|
||||||
if (option.color) {
|
|
||||||
|
|
||||||
return { '--optionIconColor': option.color };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -1,55 +0,0 @@
|
|||||||
/*****************************************************************************
|
|
||||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
|
||||||
* as represented by the Administrator of the National Aeronautics and Space
|
|
||||||
* Administration. All rights reserved.
|
|
||||||
*
|
|
||||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
* License for the specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*
|
|
||||||
* Open MCT includes source code licensed under additional open source
|
|
||||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
|
||||||
* this source code distribution or the Licensing information page available
|
|
||||||
* at runtime from the About dialog for additional information.
|
|
||||||
*****************************************************************************/
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<span class="form-control shell">
|
|
||||||
<span
|
|
||||||
class="field control"
|
|
||||||
:class="model.cssClass"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
:checked="isChecked"
|
|
||||||
@input="toggleCheckBox"
|
|
||||||
>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import toggleMixin from '../../toggle-check-box-mixin';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
mixins: [toggleMixin],
|
|
||||||
props: {
|
|
||||||
model: {
|
|
||||||
type: Object,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
isChecked: this.model.value
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
@ -40,12 +40,6 @@
|
|||||||
>
|
>
|
||||||
{{ name }}
|
{{ name }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
v-if="removable"
|
|
||||||
class="c-button icon-trash"
|
|
||||||
title="Remove file"
|
|
||||||
@click="removeFile"
|
|
||||||
></button>
|
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
@ -69,9 +63,6 @@ export default {
|
|||||||
const fileInfo = this.fileInfo || this.model.value;
|
const fileInfo = this.fileInfo || this.model.value;
|
||||||
|
|
||||||
return fileInfo && fileInfo.name || this.model.text;
|
return fileInfo && fileInfo.name || this.model.text;
|
||||||
},
|
|
||||||
removable() {
|
|
||||||
return (this.fileInfo || this.model.value) && this.model.removable;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
@ -106,15 +97,6 @@ export default {
|
|||||||
},
|
},
|
||||||
selectFile() {
|
selectFile() {
|
||||||
this.$refs.fileInput.click();
|
this.$refs.fileInput.click();
|
||||||
},
|
|
||||||
removeFile() {
|
|
||||||
this.model.value = undefined;
|
|
||||||
this.fileInfo = undefined;
|
|
||||||
const data = {
|
|
||||||
model: this.model,
|
|
||||||
value: undefined
|
|
||||||
};
|
|
||||||
this.$emit('onChange', data);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -58,6 +58,7 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
updateText() {
|
updateText() {
|
||||||
|
console.log('updateText', this.field);
|
||||||
const data = {
|
const data = {
|
||||||
model: this.model,
|
model: this.model,
|
||||||
value: this.field
|
value: this.field
|
||||||
|
@ -1,62 +0,0 @@
|
|||||||
/*****************************************************************************
|
|
||||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
|
||||||
* as represented by the Administrator of the National Aeronautics and Space
|
|
||||||
* Administration. All rights reserved.
|
|
||||||
*
|
|
||||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
* License for the specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*
|
|
||||||
* Open MCT includes source code licensed under additional open source
|
|
||||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
|
||||||
* this source code distribution or the Licensing information page available
|
|
||||||
* at runtime from the About dialog for additional information.
|
|
||||||
*****************************************************************************/
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<span class="form-control shell">
|
|
||||||
<span
|
|
||||||
class="field control"
|
|
||||||
:class="model.cssClass"
|
|
||||||
>
|
|
||||||
<ToggleSwitch
|
|
||||||
id="switchId"
|
|
||||||
:checked="isChecked"
|
|
||||||
@change="toggleCheckBox"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import toggleMixin from '../../toggle-check-box-mixin';
|
|
||||||
import ToggleSwitch from '@/ui/components/ToggleSwitch.vue';
|
|
||||||
|
|
||||||
import { v4 as uuid } from 'uuid';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
ToggleSwitch
|
|
||||||
},
|
|
||||||
mixins: [toggleMixin],
|
|
||||||
props: {
|
|
||||||
model: {
|
|
||||||
type: Object,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
switchId: `toggleSwitch-${uuid}`,
|
|
||||||
isChecked: this.model.value
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
@ -1,19 +0,0 @@
|
|||||||
export default {
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
isChecked: false
|
|
||||||
};
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
toggleCheckBox(event) {
|
|
||||||
this.isChecked = !this.isChecked;
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
model: this.model,
|
|
||||||
value: this.isChecked
|
|
||||||
};
|
|
||||||
|
|
||||||
this.$emit('onChange', data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
@ -19,27 +19,27 @@
|
|||||||
* this source code distribution or the Licensing information page available
|
* this source code distribution or the Licensing information page available
|
||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
define([
|
||||||
import EventEmitter from "EventEmitter";
|
'./SimpleIndicator',
|
||||||
import SimpleIndicator from "./SimpleIndicator";
|
'lodash'
|
||||||
|
], function (
|
||||||
class IndicatorAPI extends EventEmitter {
|
SimpleIndicator,
|
||||||
constructor(openmct) {
|
_
|
||||||
super();
|
) {
|
||||||
|
function IndicatorAPI(openmct) {
|
||||||
this.openmct = openmct;
|
this.openmct = openmct;
|
||||||
this.indicatorObjects = [];
|
this.indicatorObjects = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
getIndicatorObjectsByPriority() {
|
IndicatorAPI.prototype.getIndicatorObjectsByPriority = function () {
|
||||||
const sortedIndicators = this.indicatorObjects.sort((a, b) => b.priority - a.priority);
|
const sortedIndicators = this.indicatorObjects.sort((a, b) => b.priority - a.priority);
|
||||||
|
|
||||||
return sortedIndicators;
|
return sortedIndicators;
|
||||||
}
|
};
|
||||||
|
|
||||||
simpleIndicator() {
|
IndicatorAPI.prototype.simpleIndicator = function () {
|
||||||
return new SimpleIndicator(this.openmct);
|
return new SimpleIndicator(this.openmct);
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Accepts an indicator object, which is a simple object
|
* Accepts an indicator object, which is a simple object
|
||||||
@ -62,16 +62,14 @@ class IndicatorAPI extends EventEmitter {
|
|||||||
* myIndicator.iconClass("icon-info");
|
* myIndicator.iconClass("icon-info");
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
add(indicator) {
|
IndicatorAPI.prototype.add = function (indicator) {
|
||||||
if (!indicator.priority) {
|
if (!indicator.priority) {
|
||||||
indicator.priority = this.openmct.priority.DEFAULT;
|
indicator.priority = this.openmct.priority.DEFAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.indicatorObjects.push(indicator);
|
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.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
import EventEmitter from 'EventEmitter';
|
define(['zepto', './res/indicator-template.html'],
|
||||||
import indicatorTemplate from './res/indicator-template.html';
|
function ($, indicatorTemplate) {
|
||||||
import { convertTemplateToHTML } from '@/utils/template/templateHelpers';
|
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 {
|
this.textElement = this.element.querySelector('.js-indicator-text');
|
||||||
constructor(openmct) {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.openmct = openmct;
|
//Set defaults
|
||||||
this.element = convertTemplateToHTML(indicatorTemplate)[0];
|
this.text('New Indicator');
|
||||||
this.priority = openmct.priority.DEFAULT;
|
this.description('');
|
||||||
|
this.iconClass(DEFAULT_ICON_CLASS);
|
||||||
this.textElement = this.element.querySelector('.js-indicator-text');
|
this.statusClass('');
|
||||||
|
|
||||||
//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');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.textValue;
|
SimpleIndicator.prototype.text = function (text) {
|
||||||
}
|
if (text !== undefined && text !== this.textValue) {
|
||||||
|
this.textValue = text;
|
||||||
|
this.textElement.innerText = text;
|
||||||
|
|
||||||
description(description) {
|
if (!text) {
|
||||||
if (description !== undefined && description !== this.descriptionValue) {
|
this.element.classList.add('hidden');
|
||||||
this.descriptionValue = description;
|
} else {
|
||||||
this.element.title = description;
|
this.element.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
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 (iconClass) {
|
return this.textValue;
|
||||||
this.element.classList.add(iconClass);
|
};
|
||||||
|
|
||||||
|
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 (iconClass) {
|
||||||
if (arguments.length === 1 && statusClass !== this.statusClassValue) {
|
this.element.classList.add(iconClass);
|
||||||
if (this.statusClassValue) {
|
}
|
||||||
this.element.classList.remove(this.statusClassValue);
|
|
||||||
|
this.iconClassValue = iconClass;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (statusClass !== undefined) {
|
return this.iconClassValue;
|
||||||
this.element.classList.add(statusClass);
|
};
|
||||||
|
|
||||||
|
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', () => {
|
describe ('The Menu API', () => {
|
||||||
let openmct;
|
let openmct;
|
||||||
let appHolder;
|
let element;
|
||||||
let menuAPI;
|
let menuAPI;
|
||||||
let actionsArray;
|
let actionsArray;
|
||||||
|
let x;
|
||||||
|
let y;
|
||||||
let result;
|
let result;
|
||||||
let menuElement;
|
let onDestroy;
|
||||||
|
|
||||||
const x = 8;
|
|
||||||
const y = 16;
|
|
||||||
|
|
||||||
const menuOptions = {
|
|
||||||
onDestroy: () => {
|
|
||||||
console.log('default onDestroy');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach((done) => {
|
beforeEach((done) => {
|
||||||
appHolder = document.createElement('div');
|
const appHolder = document.createElement('div');
|
||||||
appHolder.style.display = 'block';
|
appHolder.style.display = 'block';
|
||||||
appHolder.style.width = '1920px';
|
appHolder.style.width = '1920px';
|
||||||
appHolder.style.height = '1080px';
|
appHolder.style.height = '1080px';
|
||||||
|
|
||||||
openmct = createOpenMct();
|
openmct = createOpenMct();
|
||||||
|
|
||||||
|
element = document.createElement('div');
|
||||||
|
element.style.display = 'block';
|
||||||
|
element.style.width = '1920px';
|
||||||
|
element.style.height = '1080px';
|
||||||
|
|
||||||
openmct.on('start', done);
|
openmct.on('start', done);
|
||||||
openmct.startHeadless();
|
openmct.startHeadless(appHolder);
|
||||||
|
|
||||||
menuAPI = new MenuAPI(openmct);
|
menuAPI = new MenuAPI(openmct);
|
||||||
actionsArray = [
|
actionsArray = [
|
||||||
@ -58,7 +56,7 @@ describe ('The Menu API', () => {
|
|||||||
key: 'test-css-class-1',
|
key: 'test-css-class-1',
|
||||||
name: 'Test Action 1',
|
name: 'Test Action 1',
|
||||||
cssClass: 'icon-clock',
|
cssClass: 'icon-clock',
|
||||||
description: 'This is a test action 1',
|
description: 'This is a test action',
|
||||||
onItemClicked: () => {
|
onItemClicked: () => {
|
||||||
result = 'Test Action 1 Invoked';
|
result = 'Test Action 1 Invoked';
|
||||||
}
|
}
|
||||||
@ -67,165 +65,149 @@ describe ('The Menu API', () => {
|
|||||||
key: 'test-css-class-2',
|
key: 'test-css-class-2',
|
||||||
name: 'Test Action 2',
|
name: 'Test Action 2',
|
||||||
cssClass: 'icon-clock',
|
cssClass: 'icon-clock',
|
||||||
description: 'This is a test action 2',
|
description: 'This is a test action',
|
||||||
onItemClicked: () => {
|
onItemClicked: () => {
|
||||||
result = 'Test Action 2 Invoked';
|
result = 'Test Action 2 Invoked';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
x = 8;
|
||||||
|
y = 16;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
return resetApplicationState(openmct);
|
return resetApplicationState(openmct);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('showMenu method', () => {
|
describe("showMenu method", () => {
|
||||||
beforeAll(() => {
|
it("creates an instance of Menu when invoked", () => {
|
||||||
spyOn(menuOptions, 'onDestroy').and.callThrough();
|
menuAPI.showMenu(x, y, actionsArray);
|
||||||
});
|
|
||||||
|
|
||||||
it('creates an instance of Menu when invoked', (done) => {
|
|
||||||
menuOptions.onDestroy = done;
|
|
||||||
|
|
||||||
menuAPI.showMenu(x, y, actionsArray, menuOptions);
|
|
||||||
|
|
||||||
expect(menuAPI.menuComponent).toBeInstanceOf(Menu);
|
expect(menuAPI.menuComponent).toBeInstanceOf(Menu);
|
||||||
document.body.click();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('creates a menu component', () => {
|
describe("creates a menu component", () => {
|
||||||
it('with all the actions passed in', (done) => {
|
let menuComponent;
|
||||||
menuOptions.onDestroy = done;
|
let vueComponent;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
onDestroy = jasmine.createSpy('onDestroy');
|
||||||
|
|
||||||
|
const menuOptions = {
|
||||||
|
onDestroy
|
||||||
|
};
|
||||||
|
|
||||||
menuAPI.showMenu(x, y, actionsArray, menuOptions);
|
menuAPI.showMenu(x, y, actionsArray, menuOptions);
|
||||||
menuElement = document.querySelector('.c-menu');
|
vueComponent = menuAPI.menuComponent.component;
|
||||||
expect(menuElement).toBeDefined();
|
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);
|
expect(listItems.length).toEqual(actionsArray.length);
|
||||||
document.body.click();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('with click-able menu items, that will invoke the correct callBack', (done) => {
|
it("with click-able menu items, that will invoke the correct callBacks", () => {
|
||||||
menuOptions.onDestroy = done;
|
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();
|
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) => {
|
it("dismisses the menu when action is clicked on", () => {
|
||||||
menuOptions.onDestroy = done;
|
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();
|
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) => {
|
it("invokes the destroy method when menu is dismissed", () => {
|
||||||
menuOptions.onDestroy = done;
|
|
||||||
|
|
||||||
menuAPI.showMenu(x, y, actionsArray, menuOptions);
|
|
||||||
|
|
||||||
const vueComponent = menuAPI.menuComponent.component;
|
|
||||||
spyOn(vueComponent, '$destroy');
|
|
||||||
|
|
||||||
document.body.click();
|
document.body.click();
|
||||||
|
|
||||||
expect(vueComponent.$destroy).toHaveBeenCalled();
|
expect(vueComponent.$destroy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('invokes the onDestroy callback if passed in', (done) => {
|
it("invokes the onDestroy callback if passed in", () => {
|
||||||
let count = 0;
|
|
||||||
menuOptions.onDestroy = () => {
|
|
||||||
count++;
|
|
||||||
expect(count).toEqual(1);
|
|
||||||
done();
|
|
||||||
};
|
|
||||||
|
|
||||||
menuAPI.showMenu(x, y, actionsArray, menuOptions);
|
|
||||||
|
|
||||||
document.body.click();
|
document.body.click();
|
||||||
|
|
||||||
|
expect(onDestroy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('superMenu method', () => {
|
describe("superMenu method", () => {
|
||||||
it('creates a superMenu', (done) => {
|
it("creates a superMenu", () => {
|
||||||
menuOptions.onDestroy = done;
|
menuAPI.showSuperMenu(x, y, actionsArray);
|
||||||
|
|
||||||
menuAPI.showSuperMenu(x, y, actionsArray, menuOptions);
|
const superMenu = document.querySelector('.c-super-menu__menu');
|
||||||
menuElement = document.querySelector('.c-super-menu__menu');
|
|
||||||
|
|
||||||
expect(menuElement).not.toBeNull();
|
expect(superMenu).not.toBeNull();
|
||||||
document.body.click();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Mouse over a superMenu shows correct description', (done) => {
|
it("Mouse over a superMenu shows correct description", (done) => {
|
||||||
menuOptions.onDestroy = done;
|
menuAPI.showSuperMenu(x, y, actionsArray);
|
||||||
|
|
||||||
menuAPI.showSuperMenu(x, y, actionsArray, menuOptions);
|
const superMenu = document.querySelector('.c-super-menu__menu');
|
||||||
menuElement = document.querySelector('.c-super-menu__menu');
|
const superMenuItem = superMenu.querySelector('li');
|
||||||
|
|
||||||
const superMenuItem = menuElement.querySelector('li');
|
|
||||||
const mouseOverEvent = createMouseEvent('mouseover');
|
const mouseOverEvent = createMouseEvent('mouseover');
|
||||||
|
|
||||||
superMenuItem.dispatchEvent(mouseOverEvent);
|
superMenuItem.dispatchEvent(mouseOverEvent);
|
||||||
const itemDescription = document.querySelector('.l-item-description__description');
|
const itemDescription = document.querySelector('.l-item-description__description');
|
||||||
|
|
||||||
menuAPI.menuComponent.component.$nextTick(() => {
|
setTimeout(() => {
|
||||||
expect(menuElement).not.toBeNull();
|
|
||||||
expect(itemDescription.innerText).toEqual(actionsArray[0].description);
|
expect(itemDescription.innerText).toEqual(actionsArray[0].description);
|
||||||
|
expect(superMenu).not.toBeNull();
|
||||||
document.body.click();
|
done();
|
||||||
});
|
}, 300);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Menu Placements', () => {
|
describe("Menu Placements", () => {
|
||||||
it('default menu position BOTTOM_RIGHT', (done) => {
|
it("default menu position BOTTOM_RIGHT", () => {
|
||||||
menuOptions.onDestroy = done;
|
menuAPI.showMenu(x, y, actionsArray);
|
||||||
|
|
||||||
menuAPI.showMenu(x, y, actionsArray, menuOptions);
|
const menu = document.querySelector('.c-menu');
|
||||||
menuElement = document.querySelector('.c-menu');
|
|
||||||
|
|
||||||
const boundingClientRect = menuElement.getBoundingClientRect();
|
const boundingClientRect = menu.getBoundingClientRect();
|
||||||
const left = boundingClientRect.left;
|
const left = boundingClientRect.left;
|
||||||
const top = boundingClientRect.top;
|
const top = boundingClientRect.top;
|
||||||
|
|
||||||
expect(left).toEqual(x);
|
expect(left).toEqual(x);
|
||||||
expect(top).toEqual(y);
|
expect(top).toEqual(y);
|
||||||
|
|
||||||
document.body.click();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('menu position BOTTOM_RIGHT', (done) => {
|
it("menu position BOTTOM_RIGHT", () => {
|
||||||
menuOptions.onDestroy = done;
|
const menuOptions = {
|
||||||
menuOptions.placement = openmct.menus.menuPlacement.BOTTOM_RIGHT;
|
placement: openmct.menus.menuPlacement.BOTTOM_RIGHT
|
||||||
|
};
|
||||||
|
|
||||||
menuAPI.showMenu(x, y, actionsArray, menuOptions);
|
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 left = boundingClientRect.left;
|
||||||
const top = boundingClientRect.top;
|
const top = boundingClientRect.top;
|
||||||
|
|
||||||
expect(left).toEqual(x);
|
expect(left).toEqual(x);
|
||||||
expect(top).toEqual(y);
|
expect(top).toEqual(y);
|
||||||
|
|
||||||
document.body.click();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -12,7 +12,6 @@
|
|||||||
:key="action.name"
|
:key="action.name"
|
||||||
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
|
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
|
||||||
:title="action.description"
|
:title="action.description"
|
||||||
:data-testid="action.testId || false"
|
|
||||||
@click="action.onItemClicked"
|
@click="action.onItemClicked"
|
||||||
>
|
>
|
||||||
{{ action.name }}
|
{{ action.name }}
|
||||||
@ -36,9 +35,8 @@
|
|||||||
<li
|
<li
|
||||||
v-for="action in options.actions"
|
v-for="action in options.actions"
|
||||||
:key="action.name"
|
:key="action.name"
|
||||||
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
|
:class="action.cssClass"
|
||||||
:title="action.description"
|
:title="action.description"
|
||||||
:data-testid="action.testId || false"
|
|
||||||
@click="action.onItemClicked"
|
@click="action.onItemClicked"
|
||||||
>
|
>
|
||||||
{{ action.name }}
|
{{ action.name }}
|
||||||
|
@ -15,7 +15,6 @@
|
|||||||
:key="action.name"
|
:key="action.name"
|
||||||
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
|
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
|
||||||
:title="action.description"
|
:title="action.description"
|
||||||
:data-testid="action.testId || false"
|
|
||||||
@click="action.onItemClicked"
|
@click="action.onItemClicked"
|
||||||
@mouseover="toggleItemDescription(action)"
|
@mouseover="toggleItemDescription(action)"
|
||||||
@mouseleave="toggleItemDescription()"
|
@mouseleave="toggleItemDescription()"
|
||||||
@ -46,7 +45,6 @@
|
|||||||
:key="action.name"
|
:key="action.name"
|
||||||
:class="action.cssClass"
|
:class="action.cssClass"
|
||||||
:title="action.description"
|
:title="action.description"
|
||||||
:data-testid="action.testId || false"
|
|
||||||
@click="action.onItemClicked"
|
@click="action.onItemClicked"
|
||||||
@mouseover="toggleItemDescription(action)"
|
@mouseover="toggleItemDescription(action)"
|
||||||
@mouseleave="toggleItemDescription()"
|
@mouseleave="toggleItemDescription()"
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
* at runtime from the About dialog for additional information.
|
* at runtime from the About dialog for additional information.
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
|
|
||||||
import { v4 as uuid } from 'uuid';
|
import uuid from 'uuid';
|
||||||
|
|
||||||
class InMemorySearchProvider {
|
class InMemorySearchProvider {
|
||||||
/**
|
/**
|
||||||
@ -36,13 +36,13 @@ class InMemorySearchProvider {
|
|||||||
*/
|
*/
|
||||||
this.MAX_CONCURRENT_REQUESTS = 100;
|
this.MAX_CONCURRENT_REQUESTS = 100;
|
||||||
/**
|
/**
|
||||||
* If max results is not specified in query, use this as default.
|
* If max results is not specified in query, use this as default.
|
||||||
*/
|
*/
|
||||||
this.DEFAULT_MAX_RESULTS = 100;
|
this.DEFAULT_MAX_RESULTS = 100;
|
||||||
|
|
||||||
this.openmct = openmct;
|
this.openmct = openmct;
|
||||||
|
|
||||||
this.indexedIds = {};
|
this.indexedIds = {};
|
||||||
this.indexedCompositions = {};
|
|
||||||
this.indexedTags = {};
|
|
||||||
this.idsToIndex = [];
|
this.idsToIndex = [];
|
||||||
this.pendingIndex = {};
|
this.pendingIndex = {};
|
||||||
this.pendingRequests = 0;
|
this.pendingRequests = 0;
|
||||||
@ -51,20 +51,14 @@ class InMemorySearchProvider {
|
|||||||
/**
|
/**
|
||||||
* If we don't have SharedWorkers available (e.g., iOS)
|
* If we don't have SharedWorkers available (e.g., iOS)
|
||||||
*/
|
*/
|
||||||
this.localIndexedDomainObjects = {};
|
this.localIndexedItems = {};
|
||||||
this.localIndexedAnnotationsByDomainObject = {};
|
|
||||||
this.localIndexedAnnotationsByTag = {};
|
|
||||||
|
|
||||||
this.pendingQueries = {};
|
this.pendingQueries = {};
|
||||||
this.onWorkerMessage = this.onWorkerMessage.bind(this);
|
this.onWorkerMessage = this.onWorkerMessage.bind(this);
|
||||||
this.onWorkerMessageError = this.onWorkerMessageError.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.onerror = this.onWorkerError.bind(this);
|
||||||
this.startIndexing = this.startIndexing.bind(this);
|
this.startIndexing = this.startIndexing.bind(this);
|
||||||
|
this.onMutationOfIndexedObject = this.onMutationOfIndexedObject.bind(this);
|
||||||
|
|
||||||
this.openmct.on('start', this.startIndexing);
|
this.openmct.on('start', this.startIndexing);
|
||||||
this.openmct.on('destroy', () => {
|
this.openmct.on('destroy', () => {
|
||||||
@ -74,47 +68,18 @@ class InMemorySearchProvider {
|
|||||||
this.worker.port.onmessageerror = null;
|
this.worker.port.onmessageerror = null;
|
||||||
this.worker.port.close();
|
this.worker.port.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.destroyObservers(this.indexedIds);
|
|
||||||
this.destroyObservers(this.indexedCompositions);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
startIndexing() {
|
startIndexing() {
|
||||||
const rootObject = this.openmct.objects.rootProvider.rootObject;
|
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.scheduleForIndexing(rootObject.identifier);
|
||||||
|
|
||||||
this.indexAnnotations();
|
|
||||||
|
|
||||||
if (typeof SharedWorker !== 'undefined') {
|
if (typeof SharedWorker !== 'undefined') {
|
||||||
this.worker = this.startSharedWorker();
|
this.worker = this.startSharedWorker();
|
||||||
} else {
|
} else {
|
||||||
// we must be on iOS
|
// 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 +95,51 @@ class InMemorySearchProvider {
|
|||||||
return intermediateResponse;
|
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 queryId = uuid();
|
||||||
const pendingQuery = this.getIntermediateResponse();
|
const pendingQuery = this.getIntermediateResponse();
|
||||||
this.pendingQueries[queryId] = pendingQuery;
|
this.pendingQueries[queryId] = pendingQuery;
|
||||||
const searchOptions = {
|
|
||||||
queryId,
|
|
||||||
searchType,
|
|
||||||
query,
|
|
||||||
maxResults: this.DEFAULT_MAX_RESULTS
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.worker) {
|
if (this.worker) {
|
||||||
this.#dispatchSearchToWorker(searchOptions);
|
this.dispatchSearch(queryId, input, maxResults);
|
||||||
} else {
|
} else {
|
||||||
this.#localQueryFallBack(searchOptions);
|
this.localSearch(queryId, input, maxResults);
|
||||||
}
|
}
|
||||||
|
|
||||||
return pendingQuery.promise;
|
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
|
* @private
|
||||||
*/
|
*/
|
||||||
async onWorkerMessage(event) {
|
async onWorkerMessage(event) {
|
||||||
|
if (event.data.request !== 'search') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const pendingQuery = this.pendingQueries[event.data.queryId];
|
const pendingQuery = this.pendingQueries[event.data.queryId];
|
||||||
const modelResults = {
|
const modelResults = {
|
||||||
total: event.data.total
|
total: event.data.total
|
||||||
};
|
};
|
||||||
modelResults.hits = await Promise.all(event.data.results.map(async (hit) => {
|
modelResults.hits = await Promise.all(event.data.results.map(async (hit) => {
|
||||||
if (hit && hit.keyString) {
|
const identifier = this.openmct.objects.parseKeyString(hit.keyString);
|
||||||
const identifier = this.openmct.objects.parseKeyString(hit.keyString);
|
const domainObject = await this.openmct.objects.get(identifier.key);
|
||||||
const domainObject = await this.openmct.objects.get(identifier);
|
|
||||||
|
|
||||||
return domainObject;
|
return domainObject;
|
||||||
}
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
pendingQuery.resolve(modelResults);
|
pendingQuery.resolve(modelResults);
|
||||||
@ -257,72 +213,29 @@ class InMemorySearchProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onAnnotationCreation(annotationObject) {
|
onMutationOfIndexedObject(domainObject) {
|
||||||
const provider = this;
|
const provider = this;
|
||||||
provider.index(annotationObject);
|
provider.index(domainObject.identifier, domainObject);
|
||||||
}
|
|
||||||
|
|
||||||
onNameMutation(domainObject, name) {
|
|
||||||
const provider = this;
|
|
||||||
|
|
||||||
domainObject.name = name;
|
|
||||||
provider.index(domainObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
onTagMutation(domainObject, newTags) {
|
|
||||||
domainObject.oldTags = domainObject.tags;
|
|
||||||
domainObject.tags = newTags;
|
|
||||||
const provider = this;
|
|
||||||
|
|
||||||
provider.index(domainObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
onCompositionMutation(domainObject, composition) {
|
|
||||||
const provider = this;
|
|
||||||
const indexedComposition = domainObject.composition;
|
|
||||||
const identifiersToIndex = composition
|
|
||||||
.filter(identifier => !indexedComposition
|
|
||||||
.some(indexedIdentifier => this.openmct.objects
|
|
||||||
.areIdsEqual([identifier, indexedIdentifier])));
|
|
||||||
|
|
||||||
identifiersToIndex.forEach(identifier => {
|
|
||||||
this.openmct.objects.get(identifier).then(objectToIndex => provider.index(objectToIndex));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pass a domainObject to the worker to be indexed.
|
* Pass an id and model to the worker to be indexed. If the model has
|
||||||
* If the object has composition, schedule those ids for later indexing.
|
* composition, schedule those ids for later indexing.
|
||||||
* Watch for object changes and re-index object and children if so
|
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
* @param domainObject a domainObject
|
* @param id a model id
|
||||||
|
* @param model a model
|
||||||
*/
|
*/
|
||||||
async index(domainObject) {
|
async index(id, domainObject) {
|
||||||
const provider = this;
|
const provider = this;
|
||||||
const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
|
const keyString = this.openmct.objects.makeKeyString(id);
|
||||||
|
|
||||||
if (!this.indexedIds[keyString]) {
|
if (!this.indexedIds[keyString]) {
|
||||||
this.indexedIds[keyString] = this.openmct.objects.observe(
|
this.openmct.objects.observe(domainObject, `*`, this.onMutationOfIndexedObject);
|
||||||
domainObject,
|
|
||||||
'name',
|
|
||||||
this.onNameMutation.bind(this, domainObject)
|
|
||||||
);
|
|
||||||
this.indexedCompositions[keyString] = this.openmct.objects.observe(
|
|
||||||
domainObject,
|
|
||||||
'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')) {
|
this.indexedIds[keyString] = true;
|
||||||
|
|
||||||
|
if ((id.key !== 'ROOT')) {
|
||||||
if (this.worker) {
|
if (this.worker) {
|
||||||
this.worker.port.postMessage({
|
this.worker.port.postMessage({
|
||||||
request: 'index',
|
request: 'index',
|
||||||
@ -334,12 +247,15 @@ class InMemorySearchProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const composition = this.openmct.composition.get(domainObject);
|
const composition = this.openmct.composition.registry.find(foundComposition => {
|
||||||
|
return foundComposition.appliesTo(domainObject);
|
||||||
|
});
|
||||||
|
|
||||||
if (composition !== undefined) {
|
if (composition) {
|
||||||
const children = await composition.load();
|
const childIdentifiers = await composition.load(domainObject);
|
||||||
|
childIdentifiers.forEach(function (childIdentifier) {
|
||||||
children.forEach(child => provider.scheduleForIndexing(child.identifier));
|
provider.scheduleForIndexing(childIdentifier);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -355,12 +271,12 @@ class InMemorySearchProvider {
|
|||||||
const provider = this;
|
const provider = this;
|
||||||
|
|
||||||
this.pendingRequests += 1;
|
this.pendingRequests += 1;
|
||||||
const domainObject = await this.openmct.objects.get(keyString);
|
const identifier = await this.openmct.objects.parseKeyString(keyString);
|
||||||
|
const domainObject = await this.openmct.objects.get(identifier.key);
|
||||||
delete provider.pendingIndex[keyString];
|
delete provider.pendingIndex[keyString];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (domainObject) {
|
if (domainObject) {
|
||||||
await provider.index(domainObject);
|
await provider.index(identifier, domainObject);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to index domain object ' + keyString, error);
|
console.warn('Failed to index domain object ' + keyString, error);
|
||||||
@ -378,87 +294,26 @@ class InMemorySearchProvider {
|
|||||||
* @private
|
* @private
|
||||||
* @returns {String} a unique query Id for the query.
|
* @returns {String} a unique query Id for the query.
|
||||||
*/
|
*/
|
||||||
#dispatchSearchToWorker({queryId, searchType, query, maxResults}) {
|
dispatchSearch(queryId, searchInput, maxResults) {
|
||||||
const message = {
|
const message = {
|
||||||
request: searchType.toString(),
|
request: 'search',
|
||||||
input: query,
|
input: searchInput,
|
||||||
maxResults,
|
maxResults,
|
||||||
queryId
|
queryId
|
||||||
};
|
};
|
||||||
this.worker.port.postMessage(message);
|
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
// remove old tags
|
|
||||||
if (model.oldTags) {
|
|
||||||
model.oldTags.forEach(tagIDToRemove => {
|
|
||||||
const existsInNewModel = model.tags.includes(tagIDToRemove);
|
|
||||||
if (!existsInNewModel && this.localIndexedAnnotationsByTag[tagIDToRemove]) {
|
|
||||||
this.localIndexedAnnotationsByTag[tagIDToRemove] = this.localIndexedAnnotationsByTag[tagIDToRemove].
|
|
||||||
filter(annotationToRemove => {
|
|
||||||
const shouldKeep = annotationToRemove.keyString !== keyString;
|
|
||||||
|
|
||||||
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
|
* A local version of the same SharedWorker function
|
||||||
* if we don't have SharedWorkers available (e.g., iOS)
|
* if we don't have SharedWorkers available (e.g., iOS)
|
||||||
*/
|
*/
|
||||||
localIndexItem(keyString, model) {
|
localIndexItem(keyString, model) {
|
||||||
const objectToIndex = {
|
this.localIndexedItems[keyString] = {
|
||||||
type: model.type,
|
type: model.type,
|
||||||
name: model.name,
|
name: model.name,
|
||||||
keyString
|
keyString
|
||||||
};
|
};
|
||||||
if (model && (model.type === 'annotation')) {
|
|
||||||
if (model.targets && model.targets) {
|
|
||||||
this.localIndexAnnotation(objectToIndex, model);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (model.tags) {
|
|
||||||
this.localIndexTags(keyString, objectToIndex, model);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.localIndexedDomainObjects[keyString] = objectToIndex;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -468,140 +323,29 @@ class InMemorySearchProvider {
|
|||||||
* Gets search results from the indexedItems based on provided search
|
* Gets search results from the indexedItems based on provided search
|
||||||
* input. Returns matching results from indexedItems
|
* input. Returns matching results from indexedItems
|
||||||
*/
|
*/
|
||||||
localSearchForObjects(queryId, searchInput, maxResults) {
|
localSearch(queryId, searchInput, maxResults) {
|
||||||
// This results dictionary will have domain object ID keys which
|
// This results dictionary will have domain object ID keys which
|
||||||
// point to the value the domain object's score.
|
// point to the value the domain object's score.
|
||||||
let results = [];
|
let results;
|
||||||
const input = searchInput.trim().toLowerCase();
|
const input = searchInput.trim().toLowerCase();
|
||||||
const message = {
|
const message = {
|
||||||
request: 'searchForObjects',
|
request: 'search',
|
||||||
results: [],
|
results: {},
|
||||||
total: 0,
|
total: 0,
|
||||||
queryId
|
queryId
|
||||||
};
|
};
|
||||||
|
|
||||||
results = Object.values(this.localIndexedDomainObjects).filter((indexedItem) => {
|
results = Object.values(this.localIndexedItems).filter((indexedItem) => {
|
||||||
return indexedItem.name.toLowerCase().includes(input);
|
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
|
|
||||||
.slice(0, maxResults);
|
|
||||||
const eventToReturn = {
|
|
||||||
data: message
|
|
||||||
};
|
|
||||||
this.onWorkerMessage(eventToReturn);
|
|
||||||
}
|
|
||||||
|
|
||||||
destroyObservers(observers) {
|
|
||||||
Object.entries(observers).forEach(([keyString, unobserve]) => {
|
|
||||||
if (typeof unobserve === 'function') {
|
|
||||||
unobserve();
|
|
||||||
}
|
|
||||||
|
|
||||||
delete observers[keyString];
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
message.total = results.length;
|
||||||
|
message.results = results
|
||||||
|
.slice(0, maxResults);
|
||||||
|
const eventToReturn = {
|
||||||
|
data: message
|
||||||
|
};
|
||||||
|
this.onWorkerMessage(eventToReturn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,27 +26,16 @@
|
|||||||
(function () {
|
(function () {
|
||||||
// An object composed of domain object IDs and models
|
// An object composed of domain object IDs and models
|
||||||
// {id: domainObject's ID, name: domainObject's name}
|
// {id: domainObject's ID, name: domainObject's name}
|
||||||
const indexedDomainObjects = {};
|
const indexedItems = {};
|
||||||
const indexedAnnotationsByDomainObject = {};
|
|
||||||
const indexedAnnotationsByTag = {};
|
|
||||||
|
|
||||||
self.onconnect = function (e) {
|
self.onconnect = function (e) {
|
||||||
const port = e.ports[0];
|
const port = e.ports[0];
|
||||||
|
|
||||||
port.onmessage = function (event) {
|
port.onmessage = function (event) {
|
||||||
const requestType = event.data.request;
|
if (event.data.request === 'index') {
|
||||||
if (requestType === 'index') {
|
|
||||||
indexItem(event.data.keyString, event.data.model);
|
indexItem(event.data.keyString, event.data.model);
|
||||||
} else if (requestType === 'OBJECTS') {
|
} else if (event.data.request === 'search') {
|
||||||
port.postMessage(searchForObjects(event.data));
|
port.postMessage(search(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}`);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -59,73 +48,12 @@
|
|||||||
console.error('Error on feed', error);
|
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
|
|
||||||
if (model.oldTags) {
|
|
||||||
model.oldTags.forEach(tagIDToRemove => {
|
|
||||||
const existsInNewModel = model.tags.includes(tagIDToRemove);
|
|
||||||
if (!existsInNewModel && indexedAnnotationsByTag[tagIDToRemove]) {
|
|
||||||
indexedAnnotationsByTag[tagIDToRemove] = indexedAnnotationsByTag[tagIDToRemove].
|
|
||||||
filter(annotationToRemove => {
|
|
||||||
const shouldKeep = annotationToRemove.keyString !== keyString;
|
|
||||||
|
|
||||||
return shouldKeep;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function indexItem(keyString, model) {
|
function indexItem(keyString, model) {
|
||||||
const objectToIndex = {
|
indexedItems[keyString] = {
|
||||||
type: model.type,
|
type: model.type,
|
||||||
name: model.name,
|
name: model.name,
|
||||||
keyString
|
keyString
|
||||||
};
|
};
|
||||||
if (model && (model.type === 'annotation')) {
|
|
||||||
if (model.targets && model.targets) {
|
|
||||||
indexAnnotation(objectToIndex, model);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (model.tags) {
|
|
||||||
indexTags(keyString, objectToIndex, model);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
indexedDomainObjects[keyString] = objectToIndex;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -137,98 +65,21 @@
|
|||||||
* * maxResults: The maximum number of search results desired
|
* * maxResults: The maximum number of search results desired
|
||||||
* * queryId: an id identifying this query, will be returned.
|
* * queryId: an id identifying this query, will be returned.
|
||||||
*/
|
*/
|
||||||
function searchForObjects(data) {
|
function search(data) {
|
||||||
let results = [];
|
// 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 input = data.input.trim().toLowerCase();
|
||||||
const message = {
|
const message = {
|
||||||
request: 'searchForObjects',
|
request: 'search',
|
||||||
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',
|
|
||||||
results: {},
|
results: {},
|
||||||
total: 0,
|
total: 0,
|
||||||
queryId: data.queryId
|
queryId: data.queryId
|
||||||
};
|
};
|
||||||
|
|
||||||
const matchingAnnotations = indexedAnnotationsByDomainObject[data.input.targetKeyString];
|
results = Object.values(indexedItems).filter((indexedItem) => {
|
||||||
if (matchingAnnotations) {
|
return indexedItem.name.toLowerCase().includes(input);
|
||||||
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));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
message.total = results.length;
|
message.total = results.length;
|
||||||
message.results = results
|
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();
|
openmct = createOpenMct();
|
||||||
|
|
||||||
mockObjectProvider = jasmine.createSpyObj("mock object provider", [
|
mockObjectProvider = jasmine.createSpyObj("mock object provider", [
|
||||||
"search", "supportsSearchType"
|
"search"
|
||||||
]);
|
]);
|
||||||
anotherMockObjectProvider = jasmine.createSpyObj("another mock object provider", [
|
anotherMockObjectProvider = jasmine.createSpyObj("another mock object provider", [
|
||||||
"search", "supportsSearchType"
|
"search"
|
||||||
]);
|
]);
|
||||||
openmct.objects.addProvider('objects', mockObjectProvider);
|
openmct.objects.addProvider('objects', mockObjectProvider);
|
||||||
openmct.objects.addProvider('other-objects', anotherMockObjectProvider);
|
openmct.objects.addProvider('other-objects', anotherMockObjectProvider);
|
||||||
mockObjectProvider.supportsSearchType.and.callFake(() => {
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
mockObjectProvider.search.and.callFake(() => {
|
mockObjectProvider.search.and.callFake(() => {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
const mockProviderSearch = {
|
const mockProviderSearch = {
|
||||||
@ -41,9 +38,6 @@ describe("The Object API Search Function", () => {
|
|||||||
}, MOCK_PROVIDER_SEARCH_DELAY);
|
}, MOCK_PROVIDER_SEARCH_DELAY);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
anotherMockObjectProvider.supportsSearchType.and.callFake(() => {
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
anotherMockObjectProvider.search.and.callFake(() => {
|
anotherMockObjectProvider.search.and.callFake(() => {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
const anotherMockProviderSearch = {
|
const anotherMockProviderSearch = {
|
||||||
@ -111,18 +105,13 @@ describe("The Object API Search Function", () => {
|
|||||||
|
|
||||||
beforeEach((done) => {
|
beforeEach((done) => {
|
||||||
openmct = createOpenMct();
|
openmct = createOpenMct();
|
||||||
const defaultObjectProvider = openmct.objects.getProvider({
|
spyOn(openmct.objects.inMemorySearchProvider, "query").and.callThrough();
|
||||||
key: '',
|
spyOn(openmct.objects.inMemorySearchProvider, "localSearch").and.callThrough();
|
||||||
namespace: ''
|
|
||||||
});
|
|
||||||
openmct.objects.addProvider('foo', defaultObjectProvider);
|
|
||||||
spyOn(openmct.objects.inMemorySearchProvider, "search").and.callThrough();
|
|
||||||
spyOn(openmct.objects.inMemorySearchProvider, "localSearchForObjects").and.callThrough();
|
|
||||||
|
|
||||||
openmct.on('start', async () => {
|
openmct.on('start', async () => {
|
||||||
mockIdentifier1 = {
|
mockIdentifier1 = {
|
||||||
key: 'some-object',
|
key: 'some-object',
|
||||||
namespace: 'foo'
|
namespace: 'some-namespace'
|
||||||
};
|
};
|
||||||
mockDomainObject1 = {
|
mockDomainObject1 = {
|
||||||
type: 'clock',
|
type: 'clock',
|
||||||
@ -131,7 +120,7 @@ describe("The Object API Search Function", () => {
|
|||||||
};
|
};
|
||||||
mockIdentifier2 = {
|
mockIdentifier2 = {
|
||||||
key: 'some-other-object',
|
key: 'some-other-object',
|
||||||
namespace: 'foo'
|
namespace: 'some-namespace'
|
||||||
};
|
};
|
||||||
mockDomainObject2 = {
|
mockDomainObject2 = {
|
||||||
type: 'clock',
|
type: 'clock',
|
||||||
@ -140,16 +129,16 @@ describe("The Object API Search Function", () => {
|
|||||||
};
|
};
|
||||||
mockIdentifier3 = {
|
mockIdentifier3 = {
|
||||||
key: 'yet-another-object',
|
key: 'yet-another-object',
|
||||||
namespace: 'foo'
|
namespace: 'some-namespace'
|
||||||
};
|
};
|
||||||
mockDomainObject3 = {
|
mockDomainObject3 = {
|
||||||
type: 'clock',
|
type: 'clock',
|
||||||
name: 'redBear',
|
name: 'redBear',
|
||||||
identifier: mockIdentifier3
|
identifier: mockIdentifier3
|
||||||
};
|
};
|
||||||
await openmct.objects.inMemorySearchProvider.index(mockDomainObject1);
|
await openmct.objects.inMemorySearchProvider.index(mockIdentifier1, mockDomainObject1);
|
||||||
await openmct.objects.inMemorySearchProvider.index(mockDomainObject2);
|
await openmct.objects.inMemorySearchProvider.index(mockIdentifier2, mockDomainObject2);
|
||||||
await openmct.objects.inMemorySearchProvider.index(mockDomainObject3);
|
await openmct.objects.inMemorySearchProvider.index(mockIdentifier3, mockDomainObject3);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
openmct.startHeadless();
|
openmct.startHeadless();
|
||||||
@ -161,7 +150,7 @@ describe("The Object API Search Function", () => {
|
|||||||
|
|
||||||
it("can provide indexing without a provider", () => {
|
it("can provide indexing without a provider", () => {
|
||||||
openmct.objects.search('foo');
|
openmct.objects.search('foo');
|
||||||
expect(openmct.objects.inMemorySearchProvider.search).toHaveBeenCalled();
|
expect(openmct.objects.inMemorySearchProvider.query).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can do partial search", async () => {
|
it("can do partial search", async () => {
|
||||||
@ -183,22 +172,16 @@ describe("The Object API Search Function", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("Without Shared Workers", () => {
|
describe("Without Shared Workers", () => {
|
||||||
let sharedWorkerToRestore;
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
// use local worker
|
|
||||||
sharedWorkerToRestore = openmct.objects.inMemorySearchProvider.worker;
|
|
||||||
openmct.objects.inMemorySearchProvider.worker = null;
|
openmct.objects.inMemorySearchProvider.worker = null;
|
||||||
// reindex locally
|
// reindex locally
|
||||||
await openmct.objects.inMemorySearchProvider.index(mockDomainObject1);
|
await openmct.objects.inMemorySearchProvider.index(mockIdentifier1, mockDomainObject1);
|
||||||
await openmct.objects.inMemorySearchProvider.index(mockDomainObject2);
|
await openmct.objects.inMemorySearchProvider.index(mockIdentifier2, mockDomainObject2);
|
||||||
await openmct.objects.inMemorySearchProvider.index(mockDomainObject3);
|
await openmct.objects.inMemorySearchProvider.index(mockIdentifier3, mockDomainObject3);
|
||||||
});
|
|
||||||
afterEach(() => {
|
|
||||||
openmct.objects.inMemorySearchProvider.worker = sharedWorkerToRestore;
|
|
||||||
});
|
});
|
||||||
it("calls local search", () => {
|
it("calls local search", () => {
|
||||||
openmct.objects.search('foo');
|
openmct.objects.search('foo');
|
||||||
expect(openmct.objects.inMemorySearchProvider.localSearchForObjects).toHaveBeenCalled();
|
expect(openmct.objects.inMemorySearchProvider.localSearch).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can do partial search", async () => {
|
it("can do partial search", async () => {
|
||||||
|
@ -22,14 +22,12 @@
|
|||||||
|
|
||||||
export default class Transaction {
|
export default class Transaction {
|
||||||
constructor(objectAPI) {
|
constructor(objectAPI) {
|
||||||
this.dirtyObjects = {};
|
this.dirtyObjects = new Set();
|
||||||
this.objectAPI = objectAPI;
|
this.objectAPI = objectAPI;
|
||||||
}
|
}
|
||||||
|
|
||||||
add(object) {
|
add(object) {
|
||||||
const key = this.objectAPI.makeKeyString(object.identifier);
|
this.dirtyObjects.add(object);
|
||||||
|
|
||||||
this.dirtyObjects[key] = object;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel() {
|
cancel() {
|
||||||
@ -39,8 +37,7 @@ export default class Transaction {
|
|||||||
commit() {
|
commit() {
|
||||||
const promiseArray = [];
|
const promiseArray = [];
|
||||||
const save = this.objectAPI.save.bind(this.objectAPI);
|
const save = this.objectAPI.save.bind(this.objectAPI);
|
||||||
|
this.dirtyObjects.forEach(object => {
|
||||||
Object.values(this.dirtyObjects).forEach(object => {
|
|
||||||
promiseArray.push(this.createDirtyObjectPromise(object, save));
|
promiseArray.push(this.createDirtyObjectPromise(object, save));
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -51,9 +48,7 @@ export default class Transaction {
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
action(object)
|
action(object)
|
||||||
.then((success) => {
|
.then((success) => {
|
||||||
const key = this.objectAPI.makeKeyString(object.identifier);
|
this.dirtyObjects.delete(object);
|
||||||
|
|
||||||
delete this.dirtyObjects[key];
|
|
||||||
resolve(success);
|
resolve(success);
|
||||||
})
|
})
|
||||||
.catch(reject);
|
.catch(reject);
|
||||||
@ -62,8 +57,7 @@ export default class Transaction {
|
|||||||
|
|
||||||
getDirtyObject(identifier) {
|
getDirtyObject(identifier) {
|
||||||
let dirtyObject;
|
let dirtyObject;
|
||||||
|
this.dirtyObjects.forEach(object => {
|
||||||
Object.values(this.dirtyObjects).forEach(object => {
|
|
||||||
const areIdsEqual = this.objectAPI.areIdsEqual(object.identifier, identifier);
|
const areIdsEqual = this.objectAPI.areIdsEqual(object.identifier, identifier);
|
||||||
if (areIdsEqual) {
|
if (areIdsEqual) {
|
||||||
dirtyObject = object;
|
dirtyObject = object;
|
||||||
@ -73,11 +67,14 @@ export default class Transaction {
|
|||||||
return dirtyObject;
|
return dirtyObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
this.dirtyObjects = new Set();
|
||||||
|
}
|
||||||
|
|
||||||
_clear() {
|
_clear() {
|
||||||
const promiseArray = [];
|
const promiseArray = [];
|
||||||
const refresh = this.objectAPI.refresh.bind(this.objectAPI);
|
const refresh = this.objectAPI.refresh.bind(this.objectAPI);
|
||||||
|
this.dirtyObjects.forEach(object => {
|
||||||
Object.values(this.dirtyObjects).forEach(object => {
|
|
||||||
promiseArray.push(this.createDirtyObjectPromise(object, refresh));
|
promiseArray.push(this.createDirtyObjectPromise(object, refresh));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -34,24 +34,24 @@ describe("Transaction Class", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('has no dirty objects', () => {
|
it('has no dirty objects', () => {
|
||||||
expect(Object.keys(transaction.dirtyObjects).length).toEqual(0);
|
expect(transaction.dirtyObjects.size).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('add(), adds object to dirtyObjects', () => {
|
it('add(), adds object to dirtyObjects', () => {
|
||||||
const mockDomainObjects = createMockDomainObjects();
|
const mockDomainObjects = createMockDomainObjects();
|
||||||
transaction.add(mockDomainObjects[0]);
|
transaction.add(mockDomainObjects[0]);
|
||||||
expect(Object.keys(transaction.dirtyObjects).length).toEqual(1);
|
expect(transaction.dirtyObjects.size).toEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('cancel(), clears all dirtyObjects', (done) => {
|
it('cancel(), clears all dirtyObjects', (done) => {
|
||||||
const mockDomainObjects = createMockDomainObjects(3);
|
const mockDomainObjects = createMockDomainObjects(3);
|
||||||
mockDomainObjects.forEach(transaction.add.bind(transaction));
|
mockDomainObjects.forEach(transaction.add.bind(transaction));
|
||||||
|
|
||||||
expect(Object.keys(transaction.dirtyObjects).length).toEqual(3);
|
expect(transaction.dirtyObjects.size).toEqual(3);
|
||||||
|
|
||||||
transaction.cancel()
|
transaction.cancel()
|
||||||
.then(success => {
|
.then(success => {
|
||||||
expect(Object.keys(transaction.dirtyObjects).length).toEqual(0);
|
expect(transaction.dirtyObjects.size).toEqual(0);
|
||||||
}).finally(done);
|
}).finally(done);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -59,12 +59,12 @@ describe("Transaction Class", () => {
|
|||||||
const mockDomainObjects = createMockDomainObjects(3);
|
const mockDomainObjects = createMockDomainObjects(3);
|
||||||
mockDomainObjects.forEach(transaction.add.bind(transaction));
|
mockDomainObjects.forEach(transaction.add.bind(transaction));
|
||||||
|
|
||||||
expect(Object.keys(transaction.dirtyObjects).length).toEqual(3);
|
expect(transaction.dirtyObjects.size).toEqual(3);
|
||||||
spyOn(objectAPI, 'save').and.callThrough();
|
spyOn(objectAPI, 'save').and.callThrough();
|
||||||
|
|
||||||
transaction.commit()
|
transaction.commit()
|
||||||
.then(success => {
|
.then(success => {
|
||||||
expect(Object.keys(transaction.dirtyObjects).length).toEqual(0);
|
expect(transaction.dirtyObjects.size).toEqual(0);
|
||||||
expect(objectAPI.save.calls.count()).toEqual(3);
|
expect(objectAPI.save.calls.count()).toEqual(3);
|
||||||
}).finally(done);
|
}).finally(done);
|
||||||
});
|
});
|
||||||
@ -73,7 +73,7 @@ describe("Transaction Class", () => {
|
|||||||
const mockDomainObjects = createMockDomainObjects();
|
const mockDomainObjects = createMockDomainObjects();
|
||||||
transaction.add(mockDomainObjects[0]);
|
transaction.add(mockDomainObjects[0]);
|
||||||
|
|
||||||
expect(Object.keys(transaction.dirtyObjects).length).toEqual(1);
|
expect(transaction.dirtyObjects.size).toEqual(1);
|
||||||
const dirtyObject = transaction.getDirtyObject(mockDomainObjects[0].identifier);
|
const dirtyObject = transaction.getDirtyObject(mockDomainObjects[0].identifier);
|
||||||
|
|
||||||
expect(dirtyObject).toEqual(mockDomainObjects[0]);
|
expect(dirtyObject).toEqual(mockDomainObjects[0]);
|
||||||
@ -82,7 +82,7 @@ describe("Transaction Class", () => {
|
|||||||
it('getDirtyObject(), returns empty dirtyObject for no active transaction', () => {
|
it('getDirtyObject(), returns empty dirtyObject for no active transaction', () => {
|
||||||
const mockDomainObjects = createMockDomainObjects();
|
const mockDomainObjects = createMockDomainObjects();
|
||||||
|
|
||||||
expect(Object.keys(transaction.dirtyObjects).length).toEqual(0);
|
expect(transaction.dirtyObjects.size).toEqual(0);
|
||||||
const dirtyObject = transaction.getDirtyObject(mockDomainObjects[0].identifier);
|
const dirtyObject = transaction.getDirtyObject(mockDomainObjects[0].identifier);
|
||||||
|
|
||||||
expect(dirtyObject).toEqual(undefined);
|
expect(dirtyObject).toEqual(undefined);
|
||||||
|
@ -7,7 +7,6 @@
|
|||||||
<div class="c-overlay__outer">
|
<div class="c-overlay__outer">
|
||||||
<button
|
<button
|
||||||
v-if="dismissable"
|
v-if="dismissable"
|
||||||
aria-label="Close"
|
|
||||||
class="c-click-icon c-overlay__close-button icon-x"
|
class="c-click-icon c-overlay__close-button icon-x"
|
||||||
@click="destroy"
|
@click="destroy"
|
||||||
></button>
|
></button>
|
||||||
|
@ -512,7 +512,7 @@ define([
|
|||||||
TelemetryAPI.prototype.handleMissingRequestProvider = function (domainObject) {
|
TelemetryAPI.prototype.handleMissingRequestProvider = function (domainObject) {
|
||||||
this.noRequestProviderForAllObjects = this.requestProviders.every(requestProvider => {
|
this.noRequestProviderForAllObjects = this.requestProviders.every(requestProvider => {
|
||||||
const supportsRequest = requestProvider.supportsRequest.apply(requestProvider, arguments);
|
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;
|
return supportsRequest && hasRequestProvider;
|
||||||
});
|
});
|
||||||
|
@ -22,7 +22,11 @@
|
|||||||
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import EventEmitter from 'EventEmitter';
|
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. */
|
/** Class representing a Telemetry Collection. */
|
||||||
|
|
||||||
@ -57,7 +61,7 @@ export class TelemetryCollection extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
load() {
|
load() {
|
||||||
if (this.loaded) {
|
if (this.loaded) {
|
||||||
this._error(LOADED_ERROR);
|
this._error(ERRORS.LOADED);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._setTimeSystem(this.openmct.time.timeSystem());
|
this._setTimeSystem(this.openmct.time.timeSystem());
|
||||||
@ -168,7 +172,6 @@ export class TelemetryCollection extends EventEmitter {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_processNewTelemetry(telemetryData) {
|
_processNewTelemetry(telemetryData) {
|
||||||
performance.mark('tlm:process:start');
|
|
||||||
if (telemetryData === undefined) {
|
if (telemetryData === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -263,10 +266,6 @@ export class TelemetryCollection extends EventEmitter {
|
|||||||
this.lastBounds = bounds;
|
this.lastBounds = bounds;
|
||||||
|
|
||||||
if (isTick) {
|
if (isTick) {
|
||||||
if (this.timeKey === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// need to check futureBuffer and need to check
|
// need to check futureBuffer and need to check
|
||||||
// if anything has fallen out of bounds
|
// if anything has fallen out of bounds
|
||||||
let startIndex = 0;
|
let startIndex = 0;
|
||||||
@ -306,6 +305,7 @@ export class TelemetryCollection extends EventEmitter {
|
|||||||
if (added.length > 0) {
|
if (added.length > 0) {
|
||||||
this.emit('add', added);
|
this.emit('add', added);
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// user bounds change, reset
|
// user bounds change, reset
|
||||||
this._reset();
|
this._reset();
|
||||||
@ -325,16 +325,12 @@ export class TelemetryCollection extends EventEmitter {
|
|||||||
let domains = this.metadata.valuesForHints(['domain']);
|
let domains = this.metadata.valuesForHints(['domain']);
|
||||||
let domain = domains.find((d) => d.key === timeSystem.key);
|
let domain = domains.find((d) => d.key === timeSystem.key);
|
||||||
|
|
||||||
if (domain !== undefined) {
|
if (domain === undefined) {
|
||||||
// timeKey is used to create a dummy datum used for sorting
|
this._error(ERRORS.TIMESYSTEM_KEY);
|
||||||
this.timeKey = domain.source;
|
|
||||||
} else {
|
|
||||||
this.timeKey = undefined;
|
|
||||||
|
|
||||||
this._warn(TIMESYSTEM_KEY_WARNING);
|
|
||||||
this.openmct.notifications.alert(TIMESYSTEM_KEY_NOTIFICATION);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 metadataValue = this.metadata.value(timeSystem.key) || { format: timeSystem.key };
|
||||||
let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
|
let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
|
||||||
|
|
||||||
@ -356,7 +352,6 @@ export class TelemetryCollection extends EventEmitter {
|
|||||||
* @todo handle subscriptions more granually
|
* @todo handle subscriptions more granually
|
||||||
*/
|
*/
|
||||||
_reset() {
|
_reset() {
|
||||||
performance.mark('tlm:reset');
|
|
||||||
this.boundedTelemetry = [];
|
this.boundedTelemetry = [];
|
||||||
this.futureBuffer = [];
|
this.futureBuffer = [];
|
||||||
|
|
||||||
@ -405,8 +400,4 @@ export class TelemetryCollection extends EventEmitter {
|
|||||||
_error(message) {
|
_error(message) {
|
||||||
throw new 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);
|
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 () {
|
TelemetryMetadataManager.prototype.getFilterableValues = function () {
|
||||||
return this.valueMetadatas.filter(metadatum => metadatum.filters && metadatum.filters.length > 0);
|
return this.valueMetadatas.filter(metadatum => metadatum.filters && metadatum.filters.length > 0);
|
||||||
};
|
};
|
||||||
@ -150,7 +138,7 @@ define([
|
|||||||
valueMetadata = this.values()[0];
|
valueMetadata = this.values()[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
return valueMetadata;
|
return valueMetadata.key;
|
||||||
};
|
};
|
||||||
|
|
||||||
return TelemetryMetadataManager;
|
return TelemetryMetadataManager;
|
||||||
|
@ -43,23 +43,9 @@ define([
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.valueMetadata = valueMetadata;
|
this.valueMetadata = valueMetadata;
|
||||||
|
this.formatter = formatMap.get(valueMetadata.format) || numberFormatter;
|
||||||
|
|
||||||
function getNonArrayValue(value) {
|
if (valueMetadata.format === 'enum') {
|
||||||
//metadata format could have array formats ex. string[]/number[]
|
|
||||||
const arrayRegex = /\[\]$/g;
|
|
||||||
if (value && value.match(arrayRegex)) {
|
|
||||||
return value.replace(arrayRegex, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
let valueMetadataFormat = getNonArrayValue(valueMetadata.format);
|
|
||||||
|
|
||||||
//Is there an existing formatter for the format specified? If not, default to number format
|
|
||||||
this.formatter = formatMap.get(valueMetadataFormat) || numberFormatter;
|
|
||||||
|
|
||||||
if (valueMetadataFormat === 'enum') {
|
|
||||||
this.formatter = {};
|
this.formatter = {};
|
||||||
this.enumerations = valueMetadata.enumerations.reduce(function (vm, e) {
|
this.enumerations = valueMetadata.enumerations.reduce(function (vm, e) {
|
||||||
vm.byValue[e.value] = e.string;
|
vm.byValue[e.value] = e.string;
|
||||||
@ -91,13 +77,13 @@ define([
|
|||||||
// Check for formatString support once instead of per format call.
|
// Check for formatString support once instead of per format call.
|
||||||
if (valueMetadata.formatString) {
|
if (valueMetadata.formatString) {
|
||||||
const baseFormat = this.formatter.format;
|
const baseFormat = this.formatter.format;
|
||||||
const formatString = getNonArrayValue(valueMetadata.formatString);
|
const formatString = valueMetadata.formatString;
|
||||||
this.formatter.format = function (value) {
|
this.formatter.format = function (value) {
|
||||||
return printj.sprintf(formatString, baseFormat.call(this, value));
|
return printj.sprintf(formatString, baseFormat.call(this, value));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (valueMetadataFormat === 'string') {
|
if (valueMetadata.format === 'string') {
|
||||||
this.formatter.parse = function (value) {
|
this.formatter.parse = function (value) {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
return '';
|
return '';
|
||||||
@ -122,14 +108,7 @@ define([
|
|||||||
|
|
||||||
TelemetryValueFormatter.prototype.parse = function (datum) {
|
TelemetryValueFormatter.prototype.parse = function (datum) {
|
||||||
if (_.isObject(datum)) {
|
if (_.isObject(datum)) {
|
||||||
const objectDatum = datum[this.valueMetadata.source];
|
return this.formatter.parse(datum[this.valueMetadata.source]);
|
||||||
if (Array.isArray(objectDatum)) {
|
|
||||||
return objectDatum.map((item) => {
|
|
||||||
return this.formatter.parse(item);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return this.formatter.parse(objectDatum);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.formatter.parse(datum);
|
return this.formatter.parse(datum);
|
||||||
@ -137,14 +116,7 @@ define([
|
|||||||
|
|
||||||
TelemetryValueFormatter.prototype.format = function (datum) {
|
TelemetryValueFormatter.prototype.format = function (datum) {
|
||||||
if (_.isObject(datum)) {
|
if (_.isObject(datum)) {
|
||||||
const objectDatum = datum[this.valueMetadata.source];
|
return this.formatter.format(datum[this.valueMetadata.source]);
|
||||||
if (Array.isArray(objectDatum)) {
|
|
||||||
return objectDatum.map((item) => {
|
|
||||||
return this.formatter.format(item);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return this.formatter.format(objectDatum);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.formatter.format(datum);
|
return this.formatter.format(datum);
|
||||||
|
@ -1,25 +0,0 @@
|
|||||||
/*****************************************************************************
|
|
||||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
|
||||||
* as represented by the Administrator of the National Aeronautics and Space
|
|
||||||
* Administration. All rights reserved.
|
|
||||||
*
|
|
||||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
|
||||||
* "License"); you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
* License for the specific language governing permissions and limitations
|
|
||||||
* under the License.
|
|
||||||
*
|
|
||||||
* Open MCT includes source code licensed under additional open source
|
|
||||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
|
||||||
* this source code distribution or the Licensing information page available
|
|
||||||
* at runtime from the About dialog for additional information.
|
|
||||||
*****************************************************************************/
|
|
||||||
|
|
||||||
export const TIMESYSTEM_KEY_WARNING = 'All telemetry metadata must have a telemetry value with a key that matches the key of the active time system.';
|
|
||||||
export const TIMESYSTEM_KEY_NOTIFICATION = 'Telemetry metadata does not match the active time system.';
|
|
||||||
export const LOADED_ERROR = 'Telemetry Collection has already been loaded.';
|
|
@ -1,295 +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 EventEmitter from "EventEmitter";
|
|
||||||
|
|
||||||
export default class StatusAPI extends EventEmitter {
|
|
||||||
#userAPI;
|
|
||||||
#openmct;
|
|
||||||
|
|
||||||
constructor(userAPI, openmct) {
|
|
||||||
super();
|
|
||||||
this.#userAPI = userAPI;
|
|
||||||
this.#openmct = openmct;
|
|
||||||
|
|
||||||
this.onProviderStatusChange = this.onProviderStatusChange.bind(this);
|
|
||||||
this.onProviderPollQuestionChange = this.onProviderPollQuestionChange.bind(this);
|
|
||||||
this.listenToStatusEvents = this.listenToStatusEvents.bind(this);
|
|
||||||
|
|
||||||
this.#openmct.once('destroy', () => {
|
|
||||||
const provider = this.#userAPI.getProvider();
|
|
||||||
|
|
||||||
if (typeof provider?.off === 'function') {
|
|
||||||
provider.off('statusChange', this.onProviderStatusChange);
|
|
||||||
provider.off('pollQuestionChange', this.onProviderPollQuestionChange);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.#userAPI.on('providerAdded', this.listenToStatusEvents);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch the currently defined operator status poll question. When presented with a status poll question, all operators will reply with their current status.
|
|
||||||
* @returns {Promise<PollQuestion>}
|
|
||||||
*/
|
|
||||||
getPollQuestion() {
|
|
||||||
const provider = this.#userAPI.getProvider();
|
|
||||||
|
|
||||||
if (provider.getPollQuestion) {
|
|
||||||
return provider.getPollQuestion();
|
|
||||||
} else {
|
|
||||||
this.#userAPI.error("User provider does not support polling questions");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set a poll question for operators to respond to. When presented with a status poll question, all operators will reply with their current status.
|
|
||||||
* @param {String} questionText - The text of the question
|
|
||||||
* @returns {Promise<Boolean>} true if operation was successful, otherwise false.
|
|
||||||
*/
|
|
||||||
async setPollQuestion(questionText) {
|
|
||||||
const canSetPollQuestion = await this.canSetPollQuestion();
|
|
||||||
|
|
||||||
if (canSetPollQuestion) {
|
|
||||||
const provider = this.#userAPI.getProvider();
|
|
||||||
|
|
||||||
const result = await provider.setPollQuestion(questionText);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.resetAllStatuses();
|
|
||||||
} catch (error) {
|
|
||||||
console.warn("Poll question set but unable to clear operator statuses.");
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} else {
|
|
||||||
this.#userAPI.error("User provider does not support setting polling question");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Can the currently logged in user set the operator status poll question.
|
|
||||||
* @returns {Promise<Boolean>}
|
|
||||||
*/
|
|
||||||
canSetPollQuestion() {
|
|
||||||
const provider = this.#userAPI.getProvider();
|
|
||||||
|
|
||||||
if (provider.canSetPollQuestion) {
|
|
||||||
return provider.canSetPollQuestion();
|
|
||||||
} else {
|
|
||||||
return Promise.resolve(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {Promise<Array<Status>>} the complete list of possible states that an operator can reply to a poll question with.
|
|
||||||
*/
|
|
||||||
async getPossibleStatuses() {
|
|
||||||
const provider = this.#userAPI.getProvider();
|
|
||||||
|
|
||||||
if (provider.getPossibleStatuses) {
|
|
||||||
const possibleStatuses = await provider.getPossibleStatuses() || [];
|
|
||||||
|
|
||||||
return possibleStatuses.map(status => status);
|
|
||||||
} else {
|
|
||||||
this.#userAPI.error("User provider cannot provide statuses");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {import("./UserAPI").Role} role The role to fetch the current status for.
|
|
||||||
* @returns {Promise<Status>} the current status of the provided role
|
|
||||||
*/
|
|
||||||
async getStatusForRole(role) {
|
|
||||||
const provider = this.#userAPI.getProvider();
|
|
||||||
|
|
||||||
if (provider.getStatusForRole) {
|
|
||||||
const status = await provider.getStatusForRole(role);
|
|
||||||
|
|
||||||
return status;
|
|
||||||
} else {
|
|
||||||
this.#userAPI.error("User provider does not support role status");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {import("./UserAPI").Role} role
|
|
||||||
* @returns {Promise<Boolean>} true if the configured UserProvider can provide status for the given role
|
|
||||||
* @see StatusUserProvider
|
|
||||||
*/
|
|
||||||
canProvideStatusForRole(role) {
|
|
||||||
const provider = this.#userAPI.getProvider();
|
|
||||||
|
|
||||||
if (provider.canProvideStatusForRole) {
|
|
||||||
return provider.canProvideStatusForRole(role);
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {import("./UserAPI").Role} role The role to set the status for.
|
|
||||||
* @param {Status} status The status to set for the provided role
|
|
||||||
* @returns {Promise<Boolean>} true if operation was successful, otherwise false.
|
|
||||||
*/
|
|
||||||
setStatusForRole(role, status) {
|
|
||||||
const provider = this.#userAPI.getProvider();
|
|
||||||
|
|
||||||
if (provider.setStatusForRole) {
|
|
||||||
return provider.setStatusForRole(role, status);
|
|
||||||
} else {
|
|
||||||
this.#userAPI.error("User provider does not support setting role status");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resets the status of the provided role back to its default status.
|
|
||||||
* @param {import("./UserAPI").Role} role The role to set the status for.
|
|
||||||
* @returns {Promise<Boolean>} true if operation was successful, otherwise false.
|
|
||||||
*/
|
|
||||||
async resetStatusForRole(role) {
|
|
||||||
const provider = this.#userAPI.getProvider();
|
|
||||||
const defaultStatus = await this.getDefaultStatus();
|
|
||||||
|
|
||||||
if (provider.setStatusForRole) {
|
|
||||||
return provider.setStatusForRole(role, defaultStatus);
|
|
||||||
} else {
|
|
||||||
this.#userAPI.error("User provider does not support resetting role status");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resets the status of all operators to their default status
|
|
||||||
* @returns {Promise<Boolean>} true if operation was successful, otherwise false.
|
|
||||||
*/
|
|
||||||
async resetAllStatuses() {
|
|
||||||
const allStatusRoles = await this.getAllStatusRoles();
|
|
||||||
|
|
||||||
return Promise.all(allStatusRoles.map(role => this.resetStatusForRole(role)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The default status. This is the status that will be used before the user has selected any status.
|
|
||||||
* @param {import("./UserAPI").Role} role
|
|
||||||
* @returns {Promise<Status>} the default operator status if no other has been set.
|
|
||||||
*/
|
|
||||||
async getDefaultStatusForRole(role) {
|
|
||||||
const provider = this.#userAPI.getProvider();
|
|
||||||
const defaultStatus = await provider.getDefaultStatusForRole(role);
|
|
||||||
|
|
||||||
return defaultStatus;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* All possible status roles. A status role is a user role that can provide status. In some systems
|
|
||||||
* this may be all user roles, but there may be cases where some users are not are not polled
|
|
||||||
* for status if they do not have a real-time operational role.
|
|
||||||
*
|
|
||||||
* @returns {Promise<Array<import("./UserAPI").Role>>} the default operator status if no other has been set.
|
|
||||||
*/
|
|
||||||
getAllStatusRoles() {
|
|
||||||
const provider = this.#userAPI.getProvider();
|
|
||||||
|
|
||||||
if (provider.getAllStatusRoles) {
|
|
||||||
return provider.getAllStatusRoles();
|
|
||||||
} else {
|
|
||||||
this.#userAPI.error("User provider cannot provide all status roles");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The status role of the current user. A user may have multiple roles, but will only have one role
|
|
||||||
* that provides status at any time.
|
|
||||||
* @returns {Promise<import("./UserAPI").Role>} the role for which the current user can provide status.
|
|
||||||
*/
|
|
||||||
getStatusRoleForCurrentUser() {
|
|
||||||
const provider = this.#userAPI.getProvider();
|
|
||||||
|
|
||||||
if (provider.getStatusRoleForCurrentUser) {
|
|
||||||
return provider.getStatusRoleForCurrentUser();
|
|
||||||
} else {
|
|
||||||
this.#userAPI.error("User provider cannot provide role status for this user");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {Promise<Boolean>} true if the configured UserProvider can provide status for the currently logged in user, false otherwise.
|
|
||||||
* @see StatusUserProvider
|
|
||||||
*/
|
|
||||||
async canProvideStatusForCurrentUser() {
|
|
||||||
const provider = this.#userAPI.getProvider();
|
|
||||||
|
|
||||||
if (provider.getStatusRoleForCurrentUser) {
|
|
||||||
const activeStatusRole = await this.#userAPI.getProvider().getStatusRoleForCurrentUser();
|
|
||||||
const canProvideStatus = await this.canProvideStatusForRole(activeStatusRole);
|
|
||||||
|
|
||||||
return canProvideStatus;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Private internal function that cannot be made #private because it needs to be registered as a callback to the user provider
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
listenToStatusEvents(provider) {
|
|
||||||
if (typeof provider.on === 'function') {
|
|
||||||
provider.on('statusChange', this.onProviderStatusChange);
|
|
||||||
provider.on('pollQuestionChange', this.onProviderPollQuestionChange);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
onProviderStatusChange(newStatus) {
|
|
||||||
this.emit('statusChange', newStatus);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
onProviderPollQuestionChange(pollQuestion) {
|
|
||||||
this.emit('pollQuestionChange', pollQuestion);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {import('./UserProvider')} UserProvider
|
|
||||||
*/
|
|
||||||
/**
|
|
||||||
* @typedef {import('./StatusUserProvider')} StatusUserProvider
|
|
||||||
*/
|
|
||||||
/**
|
|
||||||
* The PollQuestion type
|
|
||||||
* @typedef {Object} PollQuestion
|
|
||||||
* @property {String} question - The question to be presented to users
|
|
||||||
* @property {Number} timestamp - The time that the poll question was set.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Status type
|
|
||||||
* @typedef {Object} Status
|
|
||||||
* @property {String} key - A unique identifier for this status
|
|
||||||
* @property {Number} label - A human readable label for this status
|
|
||||||
*/
|
|
@ -1,81 +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 UserProvider from "./UserProvider";
|
|
||||||
|
|
||||||
export default class StatusUserProvider extends UserProvider {
|
|
||||||
/**
|
|
||||||
* @param {('statusChange'|'pollQuestionChange')} event the name of the event to listen to
|
|
||||||
* @param {Function} callback a function to invoke when this event occurs
|
|
||||||
*/
|
|
||||||
on(event, callback) {}
|
|
||||||
/**
|
|
||||||
* @param {('statusChange'|'pollQuestionChange')} event the name of the event to stop listen to
|
|
||||||
* @param {Function} callback the callback function used to register the listener
|
|
||||||
*/
|
|
||||||
off(event, callback) {}
|
|
||||||
/**
|
|
||||||
* @returns {import("./StatusAPI").PollQuestion} the current status poll question
|
|
||||||
*/
|
|
||||||
async getPollQuestion() {}
|
|
||||||
/**
|
|
||||||
* @param {import("./StatusAPI").PollQuestion} pollQuestion a new poll question to set
|
|
||||||
* @returns {Promise<Boolean>} true if operation was successful, otherwise false
|
|
||||||
*/
|
|
||||||
async setPollQuestion(pollQuestion) {}
|
|
||||||
/**
|
|
||||||
* @returns {Promise<Boolean>} true if the current user can set the poll question, otherwise false
|
|
||||||
*/
|
|
||||||
async canSetPollQuestion() {}
|
|
||||||
/**
|
|
||||||
* @returns {Promise<Array<import("./StatusAPI").Status>>} a list of the possible statuses that an operator can be in
|
|
||||||
*/
|
|
||||||
async getPossibleStatuses() {}
|
|
||||||
/**
|
|
||||||
* @param {import("./UserAPI").Role} role
|
|
||||||
* @returns {Promise<import("./StatusAPI").Status}
|
|
||||||
*/
|
|
||||||
async getStatusForRole(role) {}
|
|
||||||
/**
|
|
||||||
* @param {import("./UserAPI").Role} role
|
|
||||||
* @returns {Promise<import("./StatusAPI").Status}
|
|
||||||
*/
|
|
||||||
async getDefaultStatusForRole(role) {}
|
|
||||||
/**
|
|
||||||
* @param {import("./UserAPI").Role} role
|
|
||||||
* @param {*} status
|
|
||||||
* @returns {Promise<Boolean>} true if operation was successful, otherwise false.
|
|
||||||
*/
|
|
||||||
async setStatusForRole(role, status) {}
|
|
||||||
/**
|
|
||||||
* @param {import("./UserAPI").Role} role
|
|
||||||
* @returns {Promise<Boolean} true if the user provider can provide status for the given role
|
|
||||||
*/
|
|
||||||
async canProvideStatusForRole(role) {}
|
|
||||||
/**
|
|
||||||
* @returns {Promise<Array<import("./UserAPI").Role>>} a list of all available status roles, if user permissions allow it.
|
|
||||||
*/
|
|
||||||
async getAllStatusRoles() {}
|
|
||||||
/**
|
|
||||||
* @returns {Promise<import("./UserAPI").Role>} the active status role for the currently logged in user
|
|
||||||
*/
|
|
||||||
async getStatusRoleForCurrentUser() {}
|
|
||||||
}
|
|
@ -25,22 +25,16 @@ import {
|
|||||||
MULTIPLE_PROVIDER_ERROR,
|
MULTIPLE_PROVIDER_ERROR,
|
||||||
NO_PROVIDER_ERROR
|
NO_PROVIDER_ERROR
|
||||||
} from './constants';
|
} from './constants';
|
||||||
import StatusAPI from './StatusAPI';
|
|
||||||
import User from './User';
|
import User from './User';
|
||||||
|
|
||||||
class UserAPI extends EventEmitter {
|
class UserAPI extends EventEmitter {
|
||||||
/**
|
constructor(openmct) {
|
||||||
* @param {OpenMCT} openmct
|
|
||||||
* @param {UserAPIConfiguration} config
|
|
||||||
*/
|
|
||||||
constructor(openmct, config) {
|
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this._openmct = openmct;
|
this._openmct = openmct;
|
||||||
this._provider = undefined;
|
this._provider = undefined;
|
||||||
|
|
||||||
this.User = User;
|
this.User = User;
|
||||||
this.status = new StatusAPI(this, openmct, config);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -53,15 +47,12 @@ class UserAPI extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
setProvider(provider) {
|
setProvider(provider) {
|
||||||
if (this.hasProvider()) {
|
if (this.hasProvider()) {
|
||||||
this.error(MULTIPLE_PROVIDER_ERROR);
|
this._error(MULTIPLE_PROVIDER_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._provider = provider;
|
this._provider = provider;
|
||||||
this.emit('providerAdded', this._provider);
|
|
||||||
}
|
|
||||||
|
|
||||||
getProvider() {
|
this.emit('providerAdded', this._provider);
|
||||||
return this._provider;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -83,7 +74,7 @@ class UserAPI extends EventEmitter {
|
|||||||
* @throws Will throw an error if no user provider is set
|
* @throws Will throw an error if no user provider is set
|
||||||
*/
|
*/
|
||||||
getCurrentUser() {
|
getCurrentUser() {
|
||||||
this.noProviderCheck();
|
this._noProviderCheck();
|
||||||
|
|
||||||
return this._provider.getCurrentUser();
|
return this._provider.getCurrentUser();
|
||||||
}
|
}
|
||||||
@ -114,7 +105,7 @@ class UserAPI extends EventEmitter {
|
|||||||
* @throws Will throw an error if no user provider is set
|
* @throws Will throw an error if no user provider is set
|
||||||
*/
|
*/
|
||||||
hasRole(roleId) {
|
hasRole(roleId) {
|
||||||
this.noProviderCheck();
|
this._noProviderCheck();
|
||||||
|
|
||||||
return this._provider.hasRole(roleId);
|
return this._provider.hasRole(roleId);
|
||||||
}
|
}
|
||||||
@ -125,9 +116,9 @@ class UserAPI extends EventEmitter {
|
|||||||
* @private
|
* @private
|
||||||
* @throws Will throw an error if no user provider is set
|
* @throws Will throw an error if no user provider is set
|
||||||
*/
|
*/
|
||||||
noProviderCheck() {
|
_noProviderCheck() {
|
||||||
if (!this.hasProvider()) {
|
if (!this.hasProvider()) {
|
||||||
this.error(NO_PROVIDER_ERROR);
|
this._error(NO_PROVIDER_ERROR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,26 +129,9 @@ class UserAPI extends EventEmitter {
|
|||||||
* @param {string} error description of error
|
* @param {string} error description of error
|
||||||
* @throws Will throw error passed in
|
* @throws Will throw error passed in
|
||||||
*/
|
*/
|
||||||
error(error) {
|
_error(error) {
|
||||||
throw new Error(error);
|
throw new Error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default UserAPI;
|
export default UserAPI;
|
||||||
/**
|
|
||||||
* @typedef {String} Role
|
|
||||||
*/
|
|
||||||
/**
|
|
||||||
* @typedef {Object} OpenMCT
|
|
||||||
*/
|
|
||||||
/**
|
|
||||||
* @typedef {{statusStyles: Object.<string, StatusStyleDefinition>}} UserAPIConfiguration
|
|
||||||
*/
|
|
||||||
/**
|
|
||||||
* @typedef {Object} StatusStyleDefinition
|
|
||||||
* @property {String} iconClass The icon class to apply to the status indicator when this status is active "icon-circle-slash",
|
|
||||||
* @property {String} iconClassPoll The icon class to apply to the poll question indicator when this style is active eg. "icon-status-poll-question-mark"
|
|
||||||
* @property {String} statusClass The class to apply to the indicator when this status is active eg. "s-status-error"
|
|
||||||
* @property {String} statusBgColor The background color to apply in the status summary section of the poll question popup for this status eg."#9900cc"
|
|
||||||
* @property {String} statusFgColor The foreground color to apply in the status summary section of the poll question popup for this status eg. "#fff"
|
|
||||||
*/
|
|
||||||
|
@ -40,13 +40,11 @@ describe("The User API", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
const activeOverlays = openmct.overlays.activeOverlays;
|
|
||||||
activeOverlays.forEach(overlay => overlay.dismiss());
|
|
||||||
|
|
||||||
return resetApplicationState(openmct);
|
return resetApplicationState(openmct);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('with regard to user providers', () => {
|
describe('with regard to user providers', () => {
|
||||||
|
|
||||||
it('allows you to specify a user provider', () => {
|
it('allows you to specify a user provider', () => {
|
||||||
openmct.user.on('providerAdded', (provider) => {
|
openmct.user.on('providerAdded', (provider) => {
|
||||||
expect(provider).toBeInstanceOf(ExampleUserProvider);
|
expect(provider).toBeInstanceOf(ExampleUserProvider);
|
||||||
|
@ -1,36 +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 UserProvider {
|
|
||||||
/**
|
|
||||||
* @returns {Promise<User>} A promise that resolves with the currently logged in user
|
|
||||||
*/
|
|
||||||
getCurrentUser() {}
|
|
||||||
/**
|
|
||||||
* @returns {Boolean} true if a user is currently logged in, otherwise false
|
|
||||||
*/
|
|
||||||
isLoggedIn() {}
|
|
||||||
/**
|
|
||||||
* @param {String} role
|
|
||||||
* @returns {Promise<Boolean>} true if the current user has the given role
|
|
||||||
*/
|
|
||||||
hasRole(role) {}
|
|
||||||
}
|
|
@ -1,103 +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 User Status API", () => {
|
|
||||||
let openmct;
|
|
||||||
let userProvider;
|
|
||||||
let mockUser;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
userProvider = jasmine.createSpyObj("userProvider", [
|
|
||||||
"setPollQuestion",
|
|
||||||
"getPollQuestion",
|
|
||||||
"getCurrentUser",
|
|
||||||
"getPossibleStatuses",
|
|
||||||
"getAllStatusRoles",
|
|
||||||
"canSetPollQuestion",
|
|
||||||
"isLoggedIn",
|
|
||||||
"on"
|
|
||||||
]);
|
|
||||||
openmct = createOpenMct();
|
|
||||||
mockUser = new openmct.user.User("test-user", "A test user");
|
|
||||||
userProvider.getCurrentUser.and.returnValue(Promise.resolve(mockUser));
|
|
||||||
userProvider.getPossibleStatuses.and.returnValue(Promise.resolve([]));
|
|
||||||
userProvider.getAllStatusRoles.and.returnValue(Promise.resolve([]));
|
|
||||||
userProvider.canSetPollQuestion.and.returnValue(Promise.resolve(false));
|
|
||||||
userProvider.isLoggedIn.and.returnValue(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
return resetApplicationState(openmct);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("the poll question", () => {
|
|
||||||
it('can be set via a user status provider if supported', () => {
|
|
||||||
openmct.user.setProvider(userProvider);
|
|
||||||
userProvider.canSetPollQuestion.and.returnValue(Promise.resolve(true));
|
|
||||||
|
|
||||||
return openmct.user.status.setPollQuestion('This is a poll question').then(() => {
|
|
||||||
expect(userProvider.setPollQuestion).toHaveBeenCalledWith('This is a poll question');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
// fit('emits an event when the poll question changes', () => {
|
|
||||||
// const pollQuestionChangeCallback = jasmine.createSpy('pollQuestionChangeCallback');
|
|
||||||
// let pollQuestionListener;
|
|
||||||
|
|
||||||
// userProvider.canSetPollQuestion.and.returnValue(Promise.resolve(true));
|
|
||||||
// userProvider.on.and.callFake((eventName, listener) => {
|
|
||||||
// if (eventName === 'pollQuestionChange') {
|
|
||||||
// pollQuestionListener = listener;
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
|
|
||||||
// openmct.user.on('pollQuestionChange', pollQuestionChangeCallback);
|
|
||||||
|
|
||||||
// openmct.user.setProvider(userProvider);
|
|
||||||
|
|
||||||
// return openmct.user.status.setPollQuestion('This is a poll question').then(() => {
|
|
||||||
// expect(pollQuestionListener).toBeDefined();
|
|
||||||
// pollQuestionListener();
|
|
||||||
// expect(pollQuestionChangeCallback).toHaveBeenCalled();
|
|
||||||
|
|
||||||
// const pollQuestion = pollQuestionChangeCallback.calls.mostRecent().args[0];
|
|
||||||
// expect(pollQuestion.question).toBe('This is a poll question');
|
|
||||||
|
|
||||||
// openmct.user.off('pollQuestionChange', pollQuestionChangeCallback);
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
it('cannot be set if the user is not permitted', () => {
|
|
||||||
openmct.user.setProvider(userProvider);
|
|
||||||
userProvider.canSetPollQuestion.and.returnValue(Promise.resolve(false));
|
|
||||||
|
|
||||||
return openmct.user.status.setPollQuestion('This is a poll question').catch((error) => {
|
|
||||||
expect(error).toBeInstanceOf(Error);
|
|
||||||
}).finally(() => {
|
|
||||||
expect(userProvider.setPollQuestion).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@ -33,7 +33,7 @@ function replaceDotsWithUnderscores(filename) {
|
|||||||
|
|
||||||
import {saveAs} from 'saveAs';
|
import {saveAs} from 'saveAs';
|
||||||
import html2canvas from 'html2canvas';
|
import html2canvas from 'html2canvas';
|
||||||
import { v4 as uuid } from 'uuid';
|
import uuid from 'uuid';
|
||||||
|
|
||||||
class ImageExporter {
|
class ImageExporter {
|
||||||
constructor(openmct) {
|
constructor(openmct) {
|
||||||
@ -51,7 +51,7 @@ class ImageExporter {
|
|||||||
const overlays = this.openmct.overlays;
|
const overlays = this.openmct.overlays;
|
||||||
const dialog = overlays.dialog({
|
const dialog = overlays.dialog({
|
||||||
iconClass: 'info',
|
iconClass: 'info',
|
||||||
message: 'Capturing image, please wait...',
|
message: 'Caputuring an image',
|
||||||
buttons: [
|
buttons: [
|
||||||
{
|
{
|
||||||
label: 'Cancel',
|
label: 'Cancel',
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user