mirror of
https://github.com/nasa/openmct.git
synced 2025-06-25 18:50:11 +00:00
Compare commits
165 Commits
add-events
...
persistenc
Author | SHA1 | Date | |
---|---|---|---|
5de5ff347c | |||
2bfe632e7e | |||
4ac39a3990 | |||
169d148c58 | |||
40d2f3295f | |||
0e707150e0 | |||
2540d96617 | |||
1c8784fec5 | |||
2943d2b6ec | |||
4246a597a9 | |||
0af7965021 | |||
e9c0909415 | |||
0f0a3dc48f | |||
4c82680b87 | |||
c4734b8ad6 | |||
9786ff5de4 | |||
437154a5c0 | |||
2bd38dab9f | |||
063df721ae | |||
a09db30b32 | |||
9d89bdd6d3 | |||
ed9ca2829b | |||
eacbac6aad | |||
69153fe8f0 | |||
51196530fd | |||
fefa46ce7e | |||
e08ab8ef24 | |||
7011877e64 | |||
34ecc08238 | |||
a07c043a29 | |||
2999a5135e | |||
2766452b38 | |||
f3cdf69288 | |||
a040bb30c2 | |||
0a2e0a4e65 | |||
e8df2bd437 | |||
ccd2a8b64c | |||
2bd35bb2a5 | |||
28dbd724d6 | |||
5a1c329c66 | |||
00a5cbd2fd | |||
a2d698d5c1 | |||
5685a5b393 | |||
164f39695e | |||
c384cf67da | |||
417b225505 | |||
e5e93f311c | |||
39e6d9c90c | |||
60d021ef82 | |||
59880955a2 | |||
b51ed7e844 | |||
0f0c6a7b17 | |||
370e6a0c37 | |||
815506cf17 | |||
bdb1867c73 | |||
e288fdffea | |||
194060f30a | |||
45bc317a59 | |||
e103ea44d8 | |||
d13d7dc8f3 | |||
7bbaec4006 | |||
05e3303828 | |||
aa0fc70e54 | |||
9fbb695379 | |||
584d11a2ef | |||
162cc6bc77 | |||
111b0d0d68 | |||
59c0da1b57 | |||
3c70cf1767 | |||
2aec1ee854 | |||
ab60e3c3bd | |||
4445d7116a | |||
93abc18001 | |||
7fb37de721 | |||
1c525f50c8 | |||
40a7451064 | |||
04ee6f49d6 | |||
f5796c984e | |||
50b642fabe | |||
dfb726b924 | |||
8d761f729b | |||
d88ead502c | |||
c0f24b3925 | |||
c46849b166 | |||
6c71fa01f5 | |||
c56d458ecb | |||
f74a35f45a | |||
4e79725897 | |||
d9ac0182c3 | |||
7bb108c36b | |||
77804cff75 | |||
2d73296b36 | |||
405418b9d5 | |||
f999b9e12b | |||
664ba399ea | |||
c6078a234a | |||
17c16eba50 | |||
9f9c69ee68 | |||
037886aa01 | |||
48916564e4 | |||
1ca5271c3e | |||
0674c9fc33 | |||
6521b888d6 | |||
85fce3c456 | |||
8d577a8958 | |||
9c8ee09960 | |||
9568da9d5f | |||
2aa3b810ba | |||
1cdbb34e21 | |||
95299336d0 | |||
b8ff5c7f33 | |||
9ede023cfa | |||
308e621b5d | |||
e6b5870234 | |||
03e7d912be | |||
09da373d1c | |||
b8d9e41c01 | |||
815e7d169c | |||
58387e0902 | |||
0a0826f87e | |||
de1b877954 | |||
e063442d8c | |||
6a5823ab5c | |||
0493e5ae3c | |||
24f13b6249 | |||
4db2f547d9 | |||
221fb4d6bf | |||
257742b45b | |||
44edec4f04 | |||
ab4d0dd37f | |||
c089a4760d | |||
b77a4066f2 | |||
20d7e80502 | |||
d63fec51a7 | |||
d30c4fcb53 | |||
fff3ce0acf | |||
db5cb2517f | |||
5236f1c796 | |||
1ed253cb07 | |||
a6553ba010 | |||
cf6bc5be2d | |||
a53a3a0297 | |||
402cd15726 | |||
a5580912e3 | |||
54d1b8991c | |||
7b6acee793 | |||
04e1c60e5c | |||
91bcd78d40 | |||
a3c0e073c8 | |||
21ae9f45c1 | |||
0a40c8dd0b | |||
ef1ea8e712 | |||
5c4fad77ff | |||
dbac9e6cd2 | |||
4b7bcf9c89 | |||
2b42abd495 | |||
1f2102b845 | |||
2ccb90aa41 | |||
525496fbca | |||
47099786cb | |||
3a11291a3b | |||
476f1b2579 | |||
6153ad8e1e | |||
77c0b16050 | |||
d19088cec6 |
@ -2,7 +2,7 @@ version: 2.1
|
||||
executors:
|
||||
pw-focal-development:
|
||||
docker:
|
||||
- image: mcr.microsoft.com/playwright:v1.19.2-focal
|
||||
- image: mcr.microsoft.com/playwright:v1.23.0-focal
|
||||
environment:
|
||||
NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed
|
||||
parameters:
|
||||
@ -12,7 +12,7 @@ parameters:
|
||||
type: boolean
|
||||
commands:
|
||||
build_and_install:
|
||||
description: "All steps used to build and install. Will not work on node10"
|
||||
description: "All steps used to build and install. Will use cache if found"
|
||||
parameters:
|
||||
node-version:
|
||||
type: string
|
||||
@ -23,7 +23,7 @@ commands:
|
||||
- node/install:
|
||||
install-npm: true
|
||||
node-version: << parameters.node-version >>
|
||||
- run: npm install
|
||||
- run: npm install --prefer-offline --no-audit --progress=false
|
||||
restore_cache_cmd:
|
||||
description: "Custom command for restoring cache with the ability to bust cache. When BUST_CACHE is set to true, jobs will not restore cache"
|
||||
parameters:
|
||||
@ -31,7 +31,7 @@ commands:
|
||||
type: string
|
||||
steps:
|
||||
- when:
|
||||
condition:
|
||||
condition:
|
||||
equal: [false, << pipeline.parameters.BUST_CACHE >> ]
|
||||
steps:
|
||||
- restore_cache:
|
||||
@ -41,7 +41,7 @@ commands:
|
||||
parameters:
|
||||
node-version:
|
||||
type: string
|
||||
steps:
|
||||
steps:
|
||||
- save_cache:
|
||||
key: deps-{{ .Branch }}--<< parameters.node-version >>--{{ checksum "package.json" }}-{{ checksum ".circleci/config.yml" }}
|
||||
paths:
|
||||
@ -58,13 +58,17 @@ commands:
|
||||
ls -latR >> /tmp/artifacts/dir.txt
|
||||
- store_artifacts:
|
||||
path: /tmp/artifacts/
|
||||
upload_code_covio:
|
||||
description: "Command to upload code coverage reports to codecov.io"
|
||||
steps:
|
||||
- run: curl -Os https://uploader.codecov.io/latest/linux/codecov;chmod +x codecov;./codecov
|
||||
generate_e2e_code_cov_report:
|
||||
description: "Generate e2e code coverage artifacts and publish to codecov.io. Needed to that we can ignore the exit code status of the npm run test"
|
||||
parameters:
|
||||
suite:
|
||||
type: string
|
||||
steps:
|
||||
- run: npm run cov:e2e:report
|
||||
- run: npm run cov:e2e:<<parameters.suite>>:publish
|
||||
orbs:
|
||||
node: circleci/node@4.9.0
|
||||
browser-tools: circleci/browser-tools@1.2.3
|
||||
browser-tools: circleci/browser-tools@1.3.0
|
||||
jobs:
|
||||
npm-audit:
|
||||
parameters:
|
||||
@ -101,7 +105,7 @@ jobs:
|
||||
equal: [ "FirefoxESR", <<parameters.browser>> ]
|
||||
steps:
|
||||
- browser-tools/install-firefox:
|
||||
version: "91.7.1esr" #https://archive.mozilla.org/pub/firefox/releases/
|
||||
version: "91.7.1esr" #https://archive.mozilla.org/pub/firefox/releases/
|
||||
- when:
|
||||
condition:
|
||||
equal: [ "FirefoxHeadless", <<parameters.browser>> ]
|
||||
@ -114,12 +118,13 @@ jobs:
|
||||
- browser-tools/install-chrome:
|
||||
replace-existing: false
|
||||
- run: npm run test -- --browsers=<<parameters.browser>>
|
||||
- run: npm run cov:unit:publish
|
||||
- save_cache_cmd:
|
||||
node-version: <<parameters.node-version>>
|
||||
- store_test_results:
|
||||
path: dist/reports/tests/
|
||||
- store_artifacts:
|
||||
path: dist/reports/
|
||||
path: coverage
|
||||
- generate_and_store_version_and_filesystem_artifacts
|
||||
e2e-test:
|
||||
parameters:
|
||||
@ -128,36 +133,63 @@ jobs:
|
||||
suite:
|
||||
type: string
|
||||
executor: pw-focal-development
|
||||
parallelism: 4
|
||||
steps:
|
||||
- build_and_install:
|
||||
node-version: <<parameters.node-version>>
|
||||
- run: npx playwright install
|
||||
- run: npm run test:e2e:<<parameters.suite>>
|
||||
- when: #Only install chrome-beta when running the full suite to save $$$
|
||||
condition:
|
||||
equal: [ "full", <<parameters.suite>> ]
|
||||
steps:
|
||||
- run: npx playwright install chrome-beta
|
||||
- run: SHARD="$((${CIRCLE_NODE_INDEX}+1))"; npm run test:e2e:<<parameters.suite>> -- --shard=${SHARD}/${CIRCLE_NODE_TOTAL}
|
||||
- generate_e2e_code_cov_report:
|
||||
suite: <<parameters.suite>>
|
||||
- store_test_results:
|
||||
path: test-results/results.xml
|
||||
- store_artifacts:
|
||||
path: test-results
|
||||
- store_artifacts:
|
||||
path: coverage
|
||||
- store_artifacts:
|
||||
path: html-test-results
|
||||
- generate_and_store_version_and_filesystem_artifacts
|
||||
perf-test:
|
||||
parameters:
|
||||
node-version:
|
||||
type: string
|
||||
executor: pw-focal-development
|
||||
steps:
|
||||
- build_and_install:
|
||||
node-version: <<parameters.node-version>>
|
||||
- run: npm run test:perf
|
||||
- store_test_results:
|
||||
path: test-results/results.xml
|
||||
- store_artifacts:
|
||||
path: test-results
|
||||
- store_artifacts:
|
||||
path: html-test-results
|
||||
- generate_and_store_version_and_filesystem_artifacts
|
||||
workflows:
|
||||
overall-circleci-commit-status: #These jobs run on every commit
|
||||
jobs:
|
||||
- lint:
|
||||
name: node16-lint
|
||||
node-version: lts/gallium
|
||||
- unit-test:
|
||||
name: node14-chrome
|
||||
name: node14-lint
|
||||
node-version: lts/fermium
|
||||
browser: ChromeHeadless
|
||||
post-steps:
|
||||
- upload_code_covio
|
||||
- unit-test:
|
||||
name: node16-chrome
|
||||
node-version: lts/gallium
|
||||
browser: ChromeHeadless
|
||||
browser: ChromeHeadless
|
||||
- unit-test:
|
||||
name: node18-chrome
|
||||
node-version: "18"
|
||||
browser: ChromeHeadless
|
||||
- e2e-test:
|
||||
name: e2e-ci
|
||||
node-version: lts/gallium
|
||||
suite: ci
|
||||
- perf-test:
|
||||
node-version: lts/gallium
|
||||
the-nightly: #These jobs do not run on PRs, but against master at night
|
||||
jobs:
|
||||
- unit-test:
|
||||
@ -176,6 +208,10 @@ workflows:
|
||||
name: node16-chrome-nightly
|
||||
node-version: lts/gallium
|
||||
browser: ChromeHeadless
|
||||
- unit-test:
|
||||
name: node18-chrome
|
||||
node-version: "18"
|
||||
browser: ChromeHeadless
|
||||
- npm-audit:
|
||||
node-version: lts/gallium
|
||||
- e2e-test:
|
||||
|
@ -29,6 +29,7 @@ module.exports = {
|
||||
"you-dont-need-lodash-underscore/omit": "off",
|
||||
"you-dont-need-lodash-underscore/throttle": "off",
|
||||
"you-dont-need-lodash-underscore/flatten": "off",
|
||||
"you-dont-need-lodash-underscore/get": "off",
|
||||
"no-bitwise": "error",
|
||||
"curly": "error",
|
||||
"eqeqeq": "error",
|
||||
|
4
.github/ISSUE_TEMPLATE/bug_report.md
vendored
4
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -27,7 +27,7 @@ assignees: ''
|
||||
|
||||
#### Environment
|
||||
<!--- If encountered on local machine, execute the following:
|
||||
<!--- npx envinfo --system --browsers --npmPackages --binaries --languages --markdown -->
|
||||
<!--- npx envinfo --system --browsers --npmPackages --binaries --markdown -->
|
||||
* Open MCT Version: <!--- date of build, version, or SHA -->
|
||||
* Deployment Type: <!--- npm dev? VIPER Dev? openmct-yamcs? -->
|
||||
* OS:
|
||||
@ -40,6 +40,8 @@ assignees: ''
|
||||
- [ ] Is there a workaround available?
|
||||
- [ ] Does this impact a critical component?
|
||||
- [ ] Is this just a visual bug with no functional impact?
|
||||
- [ ] Does this block the execution of e2e tests?
|
||||
- [ ] Does this have an impact on Performance?
|
||||
|
||||
#### Additional Information
|
||||
<!--- Include any screenshots, gifs, or logs which will expedite triage -->
|
||||
|
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -16,7 +16,7 @@ Closes <!--- Insert Issue Number(s) this PR addresses. Start by typing # will op
|
||||
* [ ] Unit tests included and/or updated with changes?
|
||||
* [ ] Command line build passes?
|
||||
* [ ] Has this been smoke tested?
|
||||
* [ ] Testing instructions included in associated issue?
|
||||
* [ ] Testing instructions included in associated issue OR is this a dependency/testcase change?
|
||||
|
||||
### Reviewer Checklist
|
||||
|
||||
|
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
@ -32,12 +32,12 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: javascript
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
uses: github/codeql-action/analyze@v2
|
||||
|
5
.github/workflows/e2e-pr.yml
vendored
5
.github/workflows/e2e-pr.yml
vendored
@ -30,11 +30,12 @@ jobs:
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
- run: npx playwright@1.19.2 install
|
||||
- run: npx playwright@1.23.0 install
|
||||
- run: npx playwright install chrome-beta
|
||||
- run: npm install
|
||||
- run: npm run test:e2e:full
|
||||
- name: Archive test results
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: test-results
|
||||
- name: Test success
|
||||
|
2
.github/workflows/e2e-visual.yml
vendored
2
.github/workflows/e2e-visual.yml
vendored
@ -17,7 +17,7 @@ jobs:
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
- run: npx playwright@1.19.2 install
|
||||
- run: npx playwright@1.23.0 install
|
||||
- run: npm install
|
||||
- name: Run the e2e visual tests
|
||||
run: npm run test:e2e:visual
|
||||
|
1
.github/workflows/pr-platform.yml
vendored
1
.github/workflows/pr-platform.yml
vendored
@ -18,6 +18,7 @@ jobs:
|
||||
node_version:
|
||||
- 14
|
||||
- 16
|
||||
- 18
|
||||
architecture:
|
||||
- x64
|
||||
name: Node ${{ matrix.node_version }} - ${{ matrix.architecture }} on ${{ matrix.os }}
|
||||
|
22
.gitignore
vendored
22
.gitignore
vendored
@ -15,8 +15,6 @@
|
||||
*.idea
|
||||
*.iml
|
||||
|
||||
# External dependencies
|
||||
|
||||
# Build output
|
||||
target
|
||||
dist
|
||||
@ -24,30 +22,24 @@ dist
|
||||
# Mac OS X Finder
|
||||
.DS_Store
|
||||
|
||||
# Closed source libraries
|
||||
closed-lib
|
||||
|
||||
# Node, Bower dependencies
|
||||
node_modules
|
||||
bower_components
|
||||
|
||||
# Protractor logs
|
||||
protractor/logs
|
||||
|
||||
# npm-debug log
|
||||
npm-debug.log
|
||||
|
||||
# karma reports
|
||||
report.*.json
|
||||
|
||||
# Lighthouse reports
|
||||
.lighthouseci
|
||||
|
||||
# e2e test artifacts
|
||||
test-results
|
||||
allure-results
|
||||
html-test-results
|
||||
|
||||
package-lock.json
|
||||
|
||||
#codecov artifacts
|
||||
# codecov artifacts
|
||||
.nyc_output
|
||||
coverage
|
||||
codecov
|
||||
|
||||
# :(
|
||||
package-lock.json
|
||||
|
31
app.js
31
app.js
@ -12,6 +12,7 @@ const express = require('express');
|
||||
const app = express();
|
||||
const fs = require('fs');
|
||||
const request = require('request');
|
||||
const __DEV__ = process.env.NODE_ENV === 'development';
|
||||
|
||||
// Defaults
|
||||
options.port = options.port || options.p || 8080;
|
||||
@ -49,14 +50,18 @@ class WatchRunPlugin {
|
||||
}
|
||||
|
||||
const webpack = require('webpack');
|
||||
const webpackConfig = require('./webpack.dev.js');
|
||||
webpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin());
|
||||
webpackConfig.plugins.push(new WatchRunPlugin());
|
||||
|
||||
webpackConfig.entry.openmct = [
|
||||
'webpack-hot-middleware/client?reload=true',
|
||||
webpackConfig.entry.openmct
|
||||
];
|
||||
let webpackConfig;
|
||||
if (__DEV__) {
|
||||
webpackConfig = require('./webpack.dev');
|
||||
webpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin());
|
||||
webpackConfig.entry.openmct = [
|
||||
'webpack-hot-middleware/client?reload=true',
|
||||
webpackConfig.entry.openmct
|
||||
];
|
||||
webpackConfig.plugins.push(new WatchRunPlugin());
|
||||
} else {
|
||||
webpackConfig = require('./webpack.coverage');
|
||||
}
|
||||
|
||||
const compiler = webpack(webpackConfig);
|
||||
|
||||
@ -68,10 +73,12 @@ app.use(require('webpack-dev-middleware')(
|
||||
}
|
||||
));
|
||||
|
||||
app.use(require('webpack-hot-middleware')(
|
||||
compiler,
|
||||
{}
|
||||
));
|
||||
if (__DEV__) {
|
||||
app.use(require('webpack-hot-middleware')(
|
||||
compiler,
|
||||
{}
|
||||
));
|
||||
}
|
||||
|
||||
// Expose index.html for development users.
|
||||
app.get('/', function (req, res) {
|
||||
|
17
codecov.yml
17
codecov.yml
@ -13,17 +13,16 @@ coverage:
|
||||
round: down
|
||||
range: "66...100"
|
||||
|
||||
ignore:
|
||||
|
||||
parsers:
|
||||
gcov:
|
||||
branch_detection:
|
||||
conditional: true
|
||||
loop: true
|
||||
method: false
|
||||
macro: false
|
||||
flags:
|
||||
unit:
|
||||
carryforward: true
|
||||
e2e-ci:
|
||||
carryforward: true
|
||||
e2e-full:
|
||||
carryforward: true
|
||||
|
||||
comment:
|
||||
layout: "reach,diff,flags,files,footer"
|
||||
behavior: default
|
||||
require_changes: false
|
||||
show_carryforward_flags: true
|
@ -1,4 +1,12 @@
|
||||
/* eslint-disable no-undef */
|
||||
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"
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
69
e2e/fixtures.js
Normal file
69
e2e/fixtures.js
Normal file
@ -0,0 +1,69 @@
|
||||
/* This file extends the base functionality of the playwright test framework to enable
|
||||
* code coverage instrumentation, console log error detection and working with a 3rd
|
||||
* party Chrome-as-a-service extension called Browserless.
|
||||
*/
|
||||
|
||||
const base = require('@playwright/test');
|
||||
const { expect } = require('@playwright/test');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { v4: uuid } = require('uuid');
|
||||
|
||||
/**
|
||||
* Takes a `ConsoleMessage` and returns a formatted string
|
||||
* @param {import('@playwright/test').ConsoleMessage} msg
|
||||
* @returns {String} formatted string with message type, text, url, and line and column numbers
|
||||
*/
|
||||
function consoleMessageToString(msg) {
|
||||
const { url, lineNumber, columnNumber } = msg.location();
|
||||
|
||||
return `[${msg.type()}] ${msg.text()}
|
||||
at (${url} ${lineNumber}:${columnNumber})`;
|
||||
}
|
||||
|
||||
//The following is based on https://github.com/mxschmitt/playwright-test-coverage
|
||||
// eslint-disable-next-line no-undef
|
||||
const istanbulCLIOutput = path.join(process.cwd(), '.nyc_output');
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
exports.test = base.test.extend({
|
||||
//The following is based on https://github.com/mxschmitt/playwright-test-coverage
|
||||
context: async ({ context }, use) => {
|
||||
await context.addInitScript(() =>
|
||||
window.addEventListener('beforeunload', () =>
|
||||
(window).collectIstanbulCoverage(JSON.stringify((window).__coverage__))
|
||||
)
|
||||
);
|
||||
await fs.promises.mkdir(istanbulCLIOutput, { recursive: true });
|
||||
await context.exposeFunction('collectIstanbulCoverage', (coverageJSON) => {
|
||||
if (coverageJSON) {
|
||||
fs.writeFileSync(path.join(istanbulCLIOutput, `playwright_coverage_${uuid()}.json`), coverageJSON);
|
||||
}
|
||||
});
|
||||
await use(context);
|
||||
for (const page of context.pages()) {
|
||||
await page.evaluate(() => (window).collectIstanbulCoverage(JSON.stringify((window).__coverage__)));
|
||||
}
|
||||
},
|
||||
page: async ({ baseURL, page }, use) => {
|
||||
const messages = [];
|
||||
page.on('console', (msg) => messages.push(msg));
|
||||
await use(page);
|
||||
messages.forEach(
|
||||
msg => expect.soft(msg.type(), `Console error detected: ${consoleMessageToString(msg)}`).not.toEqual('error')
|
||||
);
|
||||
},
|
||||
browser: async ({ playwright, browser }, use, workerInfo) => {
|
||||
// Use browserless if configured
|
||||
if (workerInfo.project.name.match(/browserless/)) {
|
||||
const vBrowser = await playwright.chromium.connectOverCDP({
|
||||
endpointURL: 'ws://localhost:3003'
|
||||
});
|
||||
await use(vBrowser);
|
||||
} else {
|
||||
// Use Local Browser for testing.
|
||||
await use(browser);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -2,38 +2,44 @@
|
||||
// playwright.config.js
|
||||
// @ts-check
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { devices } = require('@playwright/test');
|
||||
const MAX_FAILURES = 5;
|
||||
const NUM_WORKERS = 2;
|
||||
|
||||
/** @type {import('@playwright/test').PlaywrightTestConfig} */
|
||||
const config = {
|
||||
retries: 2,
|
||||
retries: 3, //Retries 3 times for a total of 4. When running sharded and with maxFailures = 5, this should ensure that flake is managed without failing the full suite
|
||||
testDir: 'tests',
|
||||
timeout: 90 * 1000,
|
||||
testIgnore: '**/*.perf.spec.js', //Ignore performance tests and define in playwright-perfromance.config.js
|
||||
timeout: 60 * 1000,
|
||||
webServer: {
|
||||
command: 'npm run start',
|
||||
port: 8080,
|
||||
command: 'cross-env NODE_ENV=test npm run start',
|
||||
url: 'http://localhost:8080/#',
|
||||
timeout: 200 * 1000,
|
||||
reuseExistingServer: !process.env.CI
|
||||
reuseExistingServer: false
|
||||
},
|
||||
workers: 2, //Limit to 2 for CircleCI Agent
|
||||
maxFailures: MAX_FAILURES, //Limits failures to 5 to reduce CI Waste
|
||||
workers: NUM_WORKERS, //Limit to 2 for CircleCI Agent
|
||||
use: {
|
||||
baseURL: 'http://localhost:8080/',
|
||||
headless: true,
|
||||
ignoreHTTPSErrors: true,
|
||||
screenshot: 'on',
|
||||
trace: 'on',
|
||||
video: 'on'
|
||||
screenshot: 'only-on-failure',
|
||||
trace: 'on-first-retry',
|
||||
video: 'off'
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chrome',
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
...devices['Desktop Chrome']
|
||||
browserName: 'chromium'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'MMOC',
|
||||
testMatch: '**/*.e2e.spec.js', // only run e2e tests
|
||||
grepInvert: /@snapshot/,
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
viewport: {
|
||||
@ -41,19 +47,32 @@ const config = {
|
||||
height: 1440
|
||||
}
|
||||
}
|
||||
}
|
||||
/*{
|
||||
name: 'ipad',
|
||||
},
|
||||
{
|
||||
name: 'firefox',
|
||||
testMatch: '**/*.e2e.spec.js', // only run e2e tests
|
||||
grepInvert: /@snapshot/,
|
||||
use: {
|
||||
browserName: 'webkit',
|
||||
...devices['iPad (gen 7) landscape'] // Complete List https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json
|
||||
browserName: 'firefox'
|
||||
}
|
||||
}*/
|
||||
},
|
||||
{
|
||||
name: 'chrome-beta', //Only Chrome Beta is available on ubuntu -- not chrome canary
|
||||
testMatch: '**/*.e2e.spec.js', // only run e2e tests
|
||||
grepInvert: /@snapshot/,
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
channel: 'chrome-beta'
|
||||
}
|
||||
}
|
||||
],
|
||||
reporter: [
|
||||
['list'],
|
||||
['html', {
|
||||
open: 'never',
|
||||
outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840
|
||||
}],
|
||||
['junit', { outputFile: 'test-results/results.xml' }],
|
||||
['allure-playwright'],
|
||||
['github']
|
||||
]
|
||||
};
|
||||
|
@ -2,18 +2,20 @@
|
||||
// playwright.config.js
|
||||
// @ts-check
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { devices } = require('@playwright/test');
|
||||
|
||||
/** @type {import('@playwright/test').PlaywrightTestConfig} */
|
||||
const config = {
|
||||
retries: 0,
|
||||
testDir: 'tests',
|
||||
testIgnore: '**/*.perf.spec.js',
|
||||
timeout: 30 * 1000,
|
||||
webServer: {
|
||||
command: 'npm run start',
|
||||
port: 8080,
|
||||
command: 'cross-env NODE_ENV=test npm run start',
|
||||
url: 'http://localhost:8080/#',
|
||||
timeout: 120 * 1000,
|
||||
reuseExistingServer: !process.env.CI
|
||||
reuseExistingServer: true
|
||||
},
|
||||
workers: 1,
|
||||
use: {
|
||||
@ -21,20 +23,21 @@ const config = {
|
||||
baseURL: 'http://localhost:8080/',
|
||||
headless: false,
|
||||
ignoreHTTPSErrors: true,
|
||||
screenshot: 'on',
|
||||
trace: 'on',
|
||||
video: 'on'
|
||||
screenshot: 'only-on-failure',
|
||||
trace: 'retain-on-failure',
|
||||
video: 'off'
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chrome',
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
...devices['Desktop Chrome']
|
||||
browserName: 'chromium'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'MMOC',
|
||||
testMatch: '**/*.e2e.spec.js', // only run e2e tests
|
||||
grepInvert: /@snapshot/,
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
viewport: {
|
||||
@ -42,18 +45,59 @@ const config = {
|
||||
height: 1440
|
||||
}
|
||||
}
|
||||
}
|
||||
/*{
|
||||
},
|
||||
{
|
||||
name: 'safari',
|
||||
testMatch: '**/*.e2e.spec.js', // only run e2e tests
|
||||
grep: /@ipad/, // only run ipad tests due to this bug https://github.com/microsoft/playwright/issues/8340
|
||||
grepInvert: /@snapshot/,
|
||||
use: {
|
||||
browserName: 'webkit'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'firefox',
|
||||
testMatch: '**/*.e2e.spec.js', // only run e2e tests
|
||||
grepInvert: /@snapshot/,
|
||||
use: {
|
||||
browserName: 'firefox'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'canary',
|
||||
testMatch: '**/*.e2e.spec.js', // only run e2e tests
|
||||
grepInvert: /@snapshot/,
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
channel: 'chrome-canary' //Note this is not available in ubuntu/CircleCI
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'chrome-beta',
|
||||
testMatch: '**/*.e2e.spec.js', // only run e2e tests
|
||||
grepInvert: /@snapshot/,
|
||||
use: {
|
||||
browserName: 'chromium',
|
||||
channel: 'chrome-beta'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'ipad',
|
||||
testMatch: '**/*.e2e.spec.js', // only run e2e tests
|
||||
grep: /@ipad/,
|
||||
grepInvert: /@snapshot/,
|
||||
use: {
|
||||
browserName: 'webkit',
|
||||
...devices['iPad (gen 7) landscape'] // Complete List https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json
|
||||
}
|
||||
}*/
|
||||
}
|
||||
],
|
||||
reporter: [
|
||||
['list'],
|
||||
['allure-playwright']
|
||||
['html', {
|
||||
open: 'on-failure',
|
||||
outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840
|
||||
}]
|
||||
]
|
||||
};
|
||||
|
||||
|
43
e2e/playwright-performance.config.js
Normal file
43
e2e/playwright-performance.config.js
Normal file
@ -0,0 +1,43 @@
|
||||
/* eslint-disable no-undef */
|
||||
// playwright.config.js
|
||||
// @ts-check
|
||||
|
||||
const CI = process.env.CI === 'true';
|
||||
|
||||
/** @type {import('@playwright/test').PlaywrightTestConfig} */
|
||||
const config = {
|
||||
retries: 1, //Only for debugging purposes because trace is enabled only on first retry
|
||||
testDir: 'tests/performance/',
|
||||
timeout: 60 * 1000,
|
||||
workers: 1, //Only run in serial with 1 worker
|
||||
webServer: {
|
||||
command: 'cross-env NODE_ENV=test npm run start',
|
||||
url: 'http://localhost:8080/#',
|
||||
timeout: 200 * 1000,
|
||||
reuseExistingServer: !CI
|
||||
},
|
||||
use: {
|
||||
browserName: "chromium",
|
||||
baseURL: 'http://localhost:8080/',
|
||||
headless: CI, //Only if running locally
|
||||
ignoreHTTPSErrors: true,
|
||||
screenshot: 'off',
|
||||
trace: 'on-first-retry',
|
||||
video: 'off'
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chrome',
|
||||
use: {
|
||||
browserName: 'chromium'
|
||||
}
|
||||
}
|
||||
],
|
||||
reporter: [
|
||||
['list'],
|
||||
['junit', { outputFile: 'test-results/results.xml' }],
|
||||
['json', { outputFile: 'test-results/results.json' }]
|
||||
]
|
||||
};
|
||||
|
||||
module.exports = config;
|
@ -4,29 +4,28 @@
|
||||
|
||||
/** @type {import('@playwright/test').PlaywrightTestConfig} */
|
||||
const config = {
|
||||
retries: 0,
|
||||
testDir: 'tests',
|
||||
retries: 0, // visual tests should never retry due to snapshot comparison errors
|
||||
testDir: 'tests/visual',
|
||||
timeout: 90 * 1000,
|
||||
workers: 1,
|
||||
workers: 1, // visual tests should never run in parallel due to test pollution
|
||||
webServer: {
|
||||
command: 'npm run start',
|
||||
port: 8080,
|
||||
command: 'cross-env NODE_ENV=test npm run start',
|
||||
url: 'http://localhost:8080/#',
|
||||
timeout: 200 * 1000,
|
||||
reuseExistingServer: !process.env.CI
|
||||
},
|
||||
use: {
|
||||
browserName: "chromium",
|
||||
baseURL: 'http://localhost:8080/',
|
||||
headless: true,
|
||||
headless: true, // this needs to remain headless to avoid visual changes due to GPU
|
||||
ignoreHTTPSErrors: true,
|
||||
screenshot: 'on',
|
||||
trace: 'off',
|
||||
video: 'on'
|
||||
video: 'off'
|
||||
},
|
||||
reporter: [
|
||||
['list'],
|
||||
['junit', { outputFile: 'test-results/results.xml' }],
|
||||
['allure-playwright']
|
||||
['junit', { outputFile: 'test-results/results.xml' }]
|
||||
]
|
||||
};
|
||||
|
||||
|
1
e2e/test-data/PerformanceDisplayLayout.json
Normal file
1
e2e/test-data/PerformanceDisplayLayout.json
Normal file
@ -0,0 +1 @@
|
||||
{"openmct":{"b3cee102-86dd-4c0a-8eec-4d5d276f8691":{"identifier":{"key":"b3cee102-86dd-4c0a-8eec-4d5d276f8691","namespace":""},"name":"Performance Display Layout","type":"layout","composition":[{"key":"9666e7b4-be0c-47a5-94b8-99accad7155e","namespace":""}],"configuration":{"items":[{"width":32,"height":18,"x":12,"y":9,"identifier":{"key":"9666e7b4-be0c-47a5-94b8-99accad7155e","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"23ca351d-a67d-46aa-a762-290eb742d2f1"}],"layoutGrid":[10,10]},"modified":1654299875432,"location":"mine","persisted":1654299878751},"9666e7b4-be0c-47a5-94b8-99accad7155e":{"identifier":{"key":"9666e7b4-be0c-47a5-94b8-99accad7155e","namespace":""},"name":"Performance Example Imagery","type":"example.imagery","configuration":{"imageLocation":"","imageLoadDelayInMilliSeconds":20000,"imageSamples":[],"layers":[{"source":"dist/imagery/example-imagery-layer-16x9.png","name":"16:9","visible":false},{"source":"dist/imagery/example-imagery-layer-safe.png","name":"Safe","visible":false},{"source":"dist/imagery/example-imagery-layer-scale.png","name":"Scale","visible":false}]},"telemetry":{"values":[{"name":"Name","key":"name"},{"name":"Time","key":"utc","format":"utc","hints":{"domain":2}},{"name":"Local Time","key":"local","format":"local-format","hints":{"domain":1}},{"name":"Image","key":"url","format":"image","hints":{"image":1},"layers":[{"source":"dist/imagery/example-imagery-layer-16x9.png","name":"16:9"},{"source":"dist/imagery/example-imagery-layer-safe.png","name":"Safe"},{"source":"dist/imagery/example-imagery-layer-scale.png","name":"Scale"}]},{"name":"Image Download Name","key":"imageDownloadName","format":"imageDownloadName","hints":{"imageDownloadName":1}}]},"modified":1654299840077,"location":"b3cee102-86dd-4c0a-8eec-4d5d276f8691","persisted":1654299840078}},"rootId":"b3cee102-86dd-4c0a-8eec-4d5d276f8691"}
|
1
e2e/test-data/PerformanceNotebook.json
Normal file
1
e2e/test-data/PerformanceNotebook.json
Normal file
@ -0,0 +1 @@
|
||||
{"openmct":{"6d2fa9fd-f2aa-461a-a1e1-164ac44bec9d":{"identifier":{"key":"6d2fa9fd-f2aa-461a-a1e1-164ac44bec9d","namespace":""},"name":"Performance Notebook","type":"notebook","configuration":{"defaultSort":"oldest","entries":{"3e31c412-33ba-4757-8ade-e9821f6ba321":{"8c8f6035-631c-45af-8c24-786c60295335":[{"id":"entry-1652815305457","createdOn":1652815305457,"createdBy":"","text":"Existing Entry 1","embeds":[]},{"id":"entry-1652815313465","createdOn":1652815313465,"createdBy":"","text":"Existing Entry 2","embeds":[]},{"id":"entry-1652815399955","createdOn":1652815399955,"createdBy":"","text":"Existing Entry 3","embeds":[]}]}},"imageMigrationVer":"v1","pageTitle":"Page","sections":[{"id":"3e31c412-33ba-4757-8ade-e9821f6ba321","isDefault":false,"isSelected":false,"name":"Section1","pages":[{"id":"8c8f6035-631c-45af-8c24-786c60295335","isDefault":false,"isSelected":false,"name":"Page1","pageTitle":"Page"},{"id":"36555942-c9aa-439c-bbdb-0aaf50db50f5","isDefault":false,"isSelected":false,"name":"Page2","pageTitle":"Page"}],"sectionTitle":"Section"},{"id":"dab0bd1d-2c5a-405c-987f-107123d6189a","isDefault":false,"isSelected":true,"name":"Section2","pages":[{"id":"f625a86a-cb99-4898-8082-80543c8de534","isDefault":false,"isSelected":false,"name":"Page1","pageTitle":"Page"},{"id":"e77ef810-f785-42a7-942e-07e999b79c59","isDefault":false,"isSelected":true,"name":"Page2","pageTitle":"Page"}],"sectionTitle":"Section"}],"sectionTitle":"Section","type":"General","showTime":"0"},"modified":1652815915219,"location":"mine","persisted":1652815915222}},"rootId":"6d2fa9fd-f2aa-461a-a1e1-164ac44bec9d"}
|
22
e2e/test-data/VisualTestData_storage.json
Normal file
22
e2e/test-data/VisualTestData_storage.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"cookies": [],
|
||||
"origins": [
|
||||
{
|
||||
"origin": "http://localhost:8080",
|
||||
"localStorage": [
|
||||
{
|
||||
"name": "tcHistory",
|
||||
"value": "{\"utc\":[{\"start\":1654548551471,\"end\":1654550351471}]}"
|
||||
},
|
||||
{
|
||||
"name": "mct",
|
||||
"value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"527856c0-cced-4b64-bb19-f943432326d0\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1654550352296,\"modified\":1654550352296},\"527856c0-cced-4b64-bb19-f943432326d0\":{\"identifier\":{\"key\":\"527856c0-cced-4b64-bb19-f943432326d0\",\"namespace\":\"\"},\"name\":\"Unnamed Overlay Plot\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"ce88ce37-8bb9-45e1-a85b-bb7e3c8453b9\",\"namespace\":\"\"}],\"configuration\":{\"series\":[{\"identifier\":{\"key\":\"ce88ce37-8bb9-45e1-a85b-bb7e3c8453b9\",\"namespace\":\"\"}}],\"yAxis\":{},\"xAxis\":{}},\"modified\":1654550353356,\"location\":\"mine\",\"persisted\":1654550353357},\"ce88ce37-8bb9-45e1-a85b-bb7e3c8453b9\":{\"name\":\"Unnamed Sine Wave Generator\",\"type\":\"generator\",\"identifier\":{\"key\":\"ce88ce37-8bb9-45e1-a85b-bb7e3c8453b9\",\"namespace\":\"\"},\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":\"5000\"},\"modified\":1654550353350,\"location\":\"527856c0-cced-4b64-bb19-f943432326d0\",\"persisted\":1654550353350}}"
|
||||
},
|
||||
{
|
||||
"name": "mct-tree-expanded",
|
||||
"value": "[\"/browse/mine\"]"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
22
e2e/test-data/recycled_local_storage.json
Normal file
22
e2e/test-data/recycled_local_storage.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"cookies": [],
|
||||
"origins": [
|
||||
{
|
||||
"origin": "http://localhost:8080",
|
||||
"localStorage": [
|
||||
{
|
||||
"name": "tcHistory",
|
||||
"value": "{\"utc\":[{\"start\":1654537164464,\"end\":1654538964464},{\"start\":1652301954635,\"end\":1652303754635}]}"
|
||||
},
|
||||
{
|
||||
"name": "mct",
|
||||
"value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1654538965703,\"modified\":1654538965703},\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"73f2d9ae-d1f3-4561-b7fc-ecd5df557249\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1652303755999,\"location\":\"mine\",\"persisted\":1652303756002},\"2d02a680-eb7e-4645-bba2-dd298f76efb8\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"4291d80c-303c-4d8d-85e1-10f012b864fb\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1654538965702,\"location\":\"mine\",\"persisted\":1654538965702}}"
|
||||
},
|
||||
{
|
||||
"name": "mct-tree-expanded",
|
||||
"value": "[]"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
77
e2e/tests/api/forms/forms.e2e.spec.js
Normal file
77
e2e/tests/api/forms/forms.e2e.spec.js
Normal file
@ -0,0 +1,77 @@
|
||||
/*****************************************************************************
|
||||
* 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,7 +24,8 @@
|
||||
This test suite is dedicated to tests which verify branding related components.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const { test } = require('../fixtures.js');
|
||||
const { expect } = require('@playwright/test');
|
||||
|
||||
test.describe('Branding tests', () => {
|
||||
test('About Modal launches with basic branding properties', async ({ page }) => {
|
||||
@ -35,7 +36,7 @@ test.describe('Branding tests', () => {
|
||||
await page.click('.l-shell__app-logo');
|
||||
|
||||
// Verify that the NASA Logo Appears
|
||||
await expect(await page.locator('.c-about__image')).toBeVisible();
|
||||
await expect(page.locator('.c-about__image')).toBeVisible();
|
||||
|
||||
// Modify the Build information in 'about' Modal
|
||||
const versionInformationLocator = page.locator('ul.t-info.l-info.s-info');
|
||||
@ -57,6 +58,7 @@ test.describe('Branding tests', () => {
|
||||
page.waitForEvent('popup'),
|
||||
page.locator('text=click here for third party licensing information').click()
|
||||
]);
|
||||
expect(page2.waitForURL('**\/licenses**')).toBeTruthy();
|
||||
await page2.waitForLoadState('networkidle'); //Avoids timing issues with juggler/firefox
|
||||
expect(page2.waitForURL('**/licenses**')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
@ -24,7 +24,8 @@
|
||||
This test suite is dedicated to tests which verify the basic operations surrounding the example event generator.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const { test } = require('../../fixtures.js');
|
||||
const { expect } = require('@playwright/test');
|
||||
|
||||
test.describe('Example Event Generator Operations', () => {
|
||||
test('Can create example event generator with a name', async ({ page }) => {
|
||||
|
@ -24,10 +24,13 @@
|
||||
This test suite is dedicated to tests which verify the basic operations surrounding conditionSets.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const { test } = require('../../../fixtures.js');
|
||||
const { expect } = require('@playwright/test');
|
||||
|
||||
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, browserName }) => {
|
||||
test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox');
|
||||
|
||||
//Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
@ -39,44 +42,45 @@ test.describe('Sine Wave Generator', () => {
|
||||
|
||||
// Verify that the each required field has required indicator
|
||||
// Title
|
||||
await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(['c-form-row__state-indicator req']);
|
||||
await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(/req/);
|
||||
|
||||
// Verify that the Notes row does not have a required indicator
|
||||
await expect(page.locator('.c-form__section div:nth-child(3) .form-row .c-form-row__state-indicator')).not.toContain('.req');
|
||||
await page.locator('textarea[type="text"]').fill('Optional Note Text');
|
||||
|
||||
// Period
|
||||
await expect(page.locator('.c-form__section div:nth-child(4) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator req']);
|
||||
await expect(page.locator('div:nth-child(4) .c-form-row__state-indicator')).toHaveClass(/req/);
|
||||
|
||||
// Amplitude
|
||||
await expect(page.locator('.c-form__section div:nth-child(5) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator req']);
|
||||
await expect(page.locator('div:nth-child(5) .c-form-row__state-indicator')).toHaveClass(/req/);
|
||||
|
||||
// Offset
|
||||
await expect(page.locator('.c-form__section div:nth-child(6) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator req']);
|
||||
await expect(page.locator('div:nth-child(6) .c-form-row__state-indicator')).toHaveClass(/req/);
|
||||
|
||||
// Data Rate
|
||||
await expect(page.locator('.c-form__section div:nth-child(7) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator req']);
|
||||
await expect(page.locator('div:nth-child(7) .c-form-row__state-indicator')).toHaveClass(/req/);
|
||||
|
||||
// Phase
|
||||
await expect(page.locator('.c-form__section div:nth-child(8) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator req']);
|
||||
await expect(page.locator('div:nth-child(8) .c-form-row__state-indicator')).toHaveClass(/req/);
|
||||
|
||||
// Randomness
|
||||
await expect(page.locator('.c-form__section div:nth-child(9) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator req']);
|
||||
await expect(page.locator('div:nth-child(9) .c-form-row__state-indicator')).toHaveClass(/req/);
|
||||
|
||||
// Verify that by removing value from required text field shows invalid indicator
|
||||
await page.locator('text=Properties Title Notes Period Amplitude Offset Data Rate (hz) Phase (radians) Ra >> input[type="text"]').fill('');
|
||||
await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(['c-form-row__state-indicator req invalid']);
|
||||
await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(/invalid/);
|
||||
|
||||
// Verify that by adding value to empty required text field changes invalid to valid indicator
|
||||
await page.locator('text=Properties Title Notes Period Amplitude Offset Data Rate (hz) Phase (radians) Ra >> input[type="text"]').fill('non empty');
|
||||
await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(['c-form-row__state-indicator req valid']);
|
||||
await page.locator('text=Properties Title Notes Period Amplitude Offset Data Rate (hz) Phase (radians) Ra >> input[type="text"]').fill('New Sine Wave Generator');
|
||||
await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(/valid/);
|
||||
|
||||
// Verify that by removing value from required number field shows invalid indicator
|
||||
await page.locator('.field.control.l-input-sm input').first().fill('');
|
||||
await expect(page.locator('.c-form__section div:nth-child(4) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator req invalid']);
|
||||
await expect(page.locator('div:nth-child(4) .c-form-row__state-indicator')).toHaveClass(/invalid/);
|
||||
|
||||
// Verify that by adding value to empty required number field changes invalid to valid indicator
|
||||
await page.locator('.field.control.l-input-sm input').first().fill('3');
|
||||
await expect(page.locator('.c-form__section div:nth-child(4) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator req valid']);
|
||||
await expect(page.locator('div:nth-child(4) .c-form-row__state-indicator')).toHaveClass(/valid/);
|
||||
|
||||
// Verify that can change value of number field by up/down arrows keys
|
||||
// Click .field.control.l-input-sm input >> nth=0
|
||||
@ -89,57 +93,6 @@ test.describe('Sine Wave Generator', () => {
|
||||
const value = await page.locator('.field.control.l-input-sm input').first().inputValue();
|
||||
await expect(value).toBe('6');
|
||||
|
||||
// Click .c-form-row__state-indicator.grows
|
||||
await page.locator('.c-form-row__state-indicator.grows').click();
|
||||
|
||||
// Click text=Properties Title Notes Period Amplitude Offset Data Rate (hz) Phase (radians) Ra >> input[type="text"]
|
||||
await page.locator('text=Properties Title Notes Period Amplitude Offset Data Rate (hz) Phase (radians) Ra >> input[type="text"]').click();
|
||||
|
||||
// Click .c-form-row__state-indicator >> nth=0
|
||||
await page.locator('.c-form-row__state-indicator').first().click();
|
||||
|
||||
// Fill text=Properties Title Notes Period Amplitude Offset Data Rate (hz) Phase (radians) Ra >> input[type="text"]
|
||||
await page.locator('text=Properties Title Notes Period Amplitude Offset Data Rate (hz) Phase (radians) Ra >> input[type="text"]').fill('New Sine Wave Generator');
|
||||
|
||||
// Double click div:nth-child(4) .form-row .c-form-row__controls
|
||||
await page.locator('div:nth-child(4) .form-row .c-form-row__controls').dblclick();
|
||||
|
||||
// Click .field.control.l-input-sm input >> nth=0
|
||||
await page.locator('.field.control.l-input-sm input').first().click();
|
||||
|
||||
// Click div:nth-child(4) .form-row .c-form-row__state-indicator
|
||||
await page.locator('div:nth-child(4) .form-row .c-form-row__state-indicator').click();
|
||||
|
||||
// Click .field.control.l-input-sm input >> nth=0
|
||||
await page.locator('.field.control.l-input-sm input').first().click();
|
||||
|
||||
// Click .field.control.l-input-sm input >> nth=0
|
||||
await page.locator('.field.control.l-input-sm input').first().click();
|
||||
|
||||
// Click div:nth-child(5) .form-row .c-form-row__controls .form-control .field input
|
||||
await page.locator('div:nth-child(5) .form-row .c-form-row__controls .form-control .field input').click();
|
||||
|
||||
// Click div:nth-child(5) .form-row .c-form-row__controls .form-control .field input
|
||||
await page.locator('div:nth-child(5) .form-row .c-form-row__controls .form-control .field input').click();
|
||||
|
||||
// Click div:nth-child(5) .form-row .c-form-row__controls .form-control .field input
|
||||
await page.locator('div:nth-child(5) .form-row .c-form-row__controls .form-control .field input').click();
|
||||
|
||||
// Click div:nth-child(6) .form-row .c-form-row__controls .form-control .field input
|
||||
await page.locator('div:nth-child(6) .form-row .c-form-row__controls .form-control .field input').click();
|
||||
|
||||
// Double click div:nth-child(7) .form-row .c-form-row__controls .form-control .field input
|
||||
await page.locator('div:nth-child(7) .form-row .c-form-row__controls .form-control .field input').dblclick();
|
||||
|
||||
// Click div:nth-child(7) .form-row .c-form-row__state-indicator
|
||||
await page.locator('div:nth-child(7) .form-row .c-form-row__state-indicator').click();
|
||||
|
||||
// Click div:nth-child(7) .form-row .c-form-row__controls .form-control .field input
|
||||
await page.locator('div:nth-child(7) .form-row .c-form-row__controls .form-control .field input').click();
|
||||
|
||||
// Fill div:nth-child(7) .form-row .c-form-row__controls .form-control .field input
|
||||
await page.locator('div:nth-child(7) .form-row .c-form-row__controls .form-control .field input').fill('3');
|
||||
|
||||
//Click text=OK
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
@ -150,7 +103,7 @@ test.describe('Sine Wave Generator', () => {
|
||||
// Verify object properties
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText('New Sine Wave Generator');
|
||||
|
||||
// Verify canvas rendered
|
||||
// Verify canvas rendered and can be interacted with
|
||||
await page.locator('canvas').nth(1).click({
|
||||
position: {
|
||||
x: 341,
|
||||
|
55
e2e/tests/framework.e2e.spec.js
Normal file
55
e2e/tests/framework.e2e.spec.js
Normal file
@ -0,0 +1,55 @@
|
||||
/*****************************************************************************
|
||||
* 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 testing our use of the playwright framework as it
|
||||
relates to how we've extended it (i.e. ./e2e/fixtures.js) and assumptions made in our dev environment
|
||||
(app.js and ./e2e/webpack-dev-middleware.js)
|
||||
*/
|
||||
|
||||
const { test } = require('../fixtures.js');
|
||||
|
||||
test.describe('fixtures.js tests', () => {
|
||||
test('Verify that tests fail if console.error is thrown', async ({ page }) => {
|
||||
test.fail();
|
||||
//Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
//Verify that ../fixtures.js detects console log errors
|
||||
await Promise.all([
|
||||
page.evaluate(() => console.error('This should result in a failure')),
|
||||
page.waitForEvent('console') // always wait for the event to happen while triggering it!
|
||||
]);
|
||||
|
||||
});
|
||||
test('Verify that tests pass if console.warn is thrown', async ({ page }) => {
|
||||
//Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
//Verify that ../fixtures.js detects console log errors
|
||||
await Promise.all([
|
||||
page.evaluate(() => console.warn('This should result in a pass')),
|
||||
page.waitForEvent('console') // always wait for the event to happen while triggering it!
|
||||
]);
|
||||
|
||||
});
|
||||
});
|
@ -24,19 +24,114 @@
|
||||
This test suite is dedicated to tests which verify the basic operations surrounding moving objects.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const { test } = require('../fixtures.js');
|
||||
const { expect } = require('@playwright/test');
|
||||
|
||||
test.describe('Move item tests', () => {
|
||||
test.fixme('Create a basic object and verify that it can be moved to another Folder', async ({ page }) => {
|
||||
//Create and save Folder
|
||||
//Create and save Domain Object
|
||||
//Verify that the newly created domain object can be moved to Folder from Step 1.
|
||||
//Verify that newly moved object appears in the correct point in Tree
|
||||
//Verify that newly moved object appears correctly in Inspector panel
|
||||
test('Create a basic object and verify that it can be moved to another folder', async ({ page }) => {
|
||||
// Go to Open MCT
|
||||
await page.goto('/');
|
||||
|
||||
// Create a new folder in the root my items folder
|
||||
let folder1 = "Folder1";
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
await page.locator('li.icon-folder').click();
|
||||
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"]').click();
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder1);
|
||||
|
||||
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);
|
||||
|
||||
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.fixme('Create a basic object and verify that it cannot be moved to object without Composition Provider', async ({ page }) => {
|
||||
//Create and save Telemetry Object
|
||||
//Create and save Domain Object
|
||||
//Verify that the newly created domain object cannot be moved to Telemetry Object from step 1.
|
||||
test('Create a basic object and verify that it cannot be moved to telemetry object without Composition Provider', async ({ page }) => {
|
||||
// Go to Open MCT
|
||||
await page.goto('/');
|
||||
|
||||
// Create Telemetry Table
|
||||
let telemetryTable = 'Test Telemetry Table';
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
await page.locator('li:has-text("Telemetry Table")').click();
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"]').click();
|
||||
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(telemetryTable);
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
177
e2e/tests/performance/imagery.perf.spec.js
Normal file
177
e2e/tests/performance/imagery.perf.spec.js
Normal file
@ -0,0 +1,177 @@
|
||||
/*****************************************************************************
|
||||
* 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);
|
||||
|
||||
});
|
||||
});
|
119
e2e/tests/performance/memleak-imagery.perf.spec.js
Normal file
119
e2e/tests/performance/memleak-imagery.perf.spec.js
Normal file
@ -0,0 +1,119 @@
|
||||
/*****************************************************************************
|
||||
* 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');
|
||||
|
||||
});
|
||||
});
|
158
e2e/tests/performance/notebook.perf.spec.js
Normal file
158
e2e/tests/performance/notebook.perf.spec.js
Normal file
@ -0,0 +1,158 @@
|
||||
/*****************************************************************************
|
||||
* 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,12 +24,11 @@
|
||||
This test suite is dedicated to tests which verify the basic operations surrounding conditionSets.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const { test } = require('../../fixtures.js');
|
||||
const { expect } = require('@playwright/test');
|
||||
const path = require('path');
|
||||
|
||||
// https://github.com/nasa/openmct/issues/4323#issuecomment-1067282651
|
||||
|
||||
test.describe('Persistence operations', () => {
|
||||
test.describe('Persistence operations @addInit', () => {
|
||||
// add non persistable root item
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// eslint-disable-next-line no-undef
|
||||
@ -37,6 +36,10 @@ test.describe('Persistence operations', () => {
|
||||
});
|
||||
|
||||
test('Persistability should be respected in the create form location field', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/4323'
|
||||
});
|
||||
// Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
|
@ -24,7 +24,10 @@
|
||||
This test suite is dedicated to tests which verify the basic operations surrounding exportAsJSON.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const { test } = require('../../../fixtures.js');
|
||||
// FIXME: Remove this eslint exception once tests are implemented
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { expect } = require('@playwright/test');
|
||||
|
||||
test.describe('ExportAsJSON', () => {
|
||||
test.fixme('Create a basic object and verify that it can be exported as JSON from Tree', async ({ page }) => {
|
||||
|
@ -24,7 +24,10 @@
|
||||
This test suite is dedicated to tests which verify the basic operations surrounding importAsJSON.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const { test } = require('../../../fixtures.js');
|
||||
// FIXME: Remove this eslint exception once tests are implemented
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { expect } = require('@playwright/test');
|
||||
|
||||
test.describe('ExportAsJSON', () => {
|
||||
test.fixme('Verify that domain object can be importAsJSON from Tree', async ({ page }) => {
|
||||
|
@ -24,7 +24,8 @@
|
||||
This test suite is dedicated to tests which verify the basic operations surrounding Clock.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const { test } = require('../../../fixtures.js');
|
||||
const { expect } = require('@playwright/test');
|
||||
|
||||
test.describe('Clock Generator', () => {
|
||||
|
||||
@ -45,22 +46,22 @@ test.describe('Clock Generator', () => {
|
||||
// Click .icon-arrow-down
|
||||
await page.locator('.icon-arrow-down').click();
|
||||
//verify if the autocomplete dropdown is visible
|
||||
await expect(page.locator(".optionPreSelected")).toBeVisible();
|
||||
await expect(page.locator(".c-input--autocomplete__options")).toBeVisible();
|
||||
// Click .icon-arrow-down
|
||||
await page.locator('.icon-arrow-down').click();
|
||||
|
||||
// Verify clicking on the autocomplete arrow collapses the dropdown
|
||||
await expect(page.locator(".optionPreSelected")).not.toBeVisible();
|
||||
await expect(page.locator(".c-input--autocomplete__options")).not.toBeVisible();
|
||||
|
||||
// Click timezone input to open dropdown
|
||||
await page.locator('.autocompleteInput').click();
|
||||
await page.locator('.c-input--autocomplete__input').click();
|
||||
//verify if the autocomplete dropdown is visible
|
||||
await expect(page.locator(".optionPreSelected")).toBeVisible();
|
||||
await expect(page.locator(".c-input--autocomplete__options")).toBeVisible();
|
||||
|
||||
// Verify clicking outside the autocomplete dropdown collapses it
|
||||
await page.locator('text=Timezone').click();
|
||||
// Verify clicking on the autocomplete arrow collapses the dropdown
|
||||
await expect(page.locator(".optionPreSelected")).not.toBeVisible();
|
||||
await expect(page.locator(".c-input--autocomplete__options")).not.toBeVisible();
|
||||
|
||||
});
|
||||
});
|
||||
|
@ -21,13 +21,21 @@
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
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. Note: this
|
||||
suite is sharing state between tests which is considered an anti-pattern. Implimenting in this way to
|
||||
demonstrate some playwright for test developers. This pattern should not be re-used in other CRUD suites.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const { test } = require('../../../fixtures.js');
|
||||
const { expect } = require('@playwright/test');
|
||||
|
||||
test.describe('Condition Set Operations', () => {
|
||||
test('Create new button `condition set` creates new condition object', async ({ page }) => {
|
||||
let conditionSetUrl;
|
||||
let getConditionSetIdentifierFromUrl;
|
||||
|
||||
test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
|
||||
test.beforeAll(async ({ browser}) => {
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
//Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
@ -35,31 +43,139 @@ test.describe('Condition Set Operations', () => {
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
// Click text=Condition Set
|
||||
await page.click('text=Condition Set');
|
||||
await page.locator('li:has-text("Condition Set")').click();
|
||||
|
||||
// Click text=OK
|
||||
await Promise.all([
|
||||
page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine/dab945d4-5a84-480e-8180-222b4aa730fa?tc.mode=fixed&tc.startBound=1639696164435&tc.endBound=1639697964435&tc.timeSystem=utc&view=conditionSet.view' }*/),
|
||||
page.waitForNavigation(),
|
||||
page.click('text=OK')
|
||||
]);
|
||||
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
|
||||
//Save localStorage for future test execution
|
||||
await context.storageState({ path: './e2e/test-data/recycled_local_storage.json' });
|
||||
|
||||
//Set object identifier from url
|
||||
conditionSetUrl = page.url();
|
||||
console.log('conditionSetUrl ' + conditionSetUrl);
|
||||
|
||||
getConditionSetIdentifierFromUrl = conditionSetUrl.split('/').pop().split('?')[0];
|
||||
console.debug('getConditionSetIdentifierFromUrl ' + getConditionSetIdentifierFromUrl);
|
||||
});
|
||||
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()
|
||||
|
||||
//Load localStorage for subsequent tests
|
||||
test.use({ storageState: './e2e/test-data/recycled_local_storage.json' });
|
||||
//Begin suite of tests again localStorage
|
||||
test('Condition set object properties persist in main view and inspector @localStorage', async ({ page }) => {
|
||||
//Navigate to baseURL with injected localStorage
|
||||
await page.goto(conditionSetUrl, { waitUntil: 'networkidle' });
|
||||
|
||||
//Assertions on loaded Condition Set in main view. This is a stateful transition step after page.goto()
|
||||
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
|
||||
|
||||
//Assertions on loaded Condition Set in Inspector
|
||||
expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy();
|
||||
|
||||
//Reload Page
|
||||
await Promise.all([
|
||||
page.reload(),
|
||||
page.waitForLoadState('networkidle')
|
||||
]);
|
||||
|
||||
//Re-verify after reload
|
||||
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
|
||||
//Assertions on loaded Condition Set in Inspector
|
||||
expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy();
|
||||
|
||||
});
|
||||
test.fixme('condition set object can be modified', async ({ page }) => {
|
||||
//Go to object created in step one
|
||||
test('condition set object can be modified on @localStorage', async ({ page }) => {
|
||||
await page.goto(conditionSetUrl, { waitUntil: 'networkidle' });
|
||||
|
||||
//Assertions on loaded Condition Set in main view. This is a stateful transition step after page.goto()
|
||||
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
|
||||
|
||||
//Update the Condition Set properties
|
||||
//Verify the Condition Set properties persist on Save
|
||||
//Verify the Condition Set properties persist on page.reload()
|
||||
// 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
|
||||
expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy();
|
||||
// Verify Inspector Details has updated Name property
|
||||
expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy();
|
||||
|
||||
// Verify Tree reflects updated Name proprety
|
||||
// Expand Tree
|
||||
await page.locator('text=Open MCT My Items >> span >> nth=3').click();
|
||||
// Verify Condition Set Object is renamed in Tree
|
||||
expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
|
||||
// Verify Search Tree reflects renamed Name property
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Renamed');
|
||||
expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
|
||||
|
||||
//Reload Page
|
||||
await Promise.all([
|
||||
page.reload(),
|
||||
page.waitForLoadState('networkidle')
|
||||
]);
|
||||
|
||||
//Verify Main section reflects updated Name Property
|
||||
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Renamed Condition Set');
|
||||
|
||||
// Verify Inspector properties
|
||||
// Verify Inspector has updated Name property
|
||||
expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy();
|
||||
// Verify Inspector Details has updated Name property
|
||||
expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy();
|
||||
|
||||
// Verify 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
|
||||
expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
|
||||
// Verify Search Tree reflects renamed Name property
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Renamed');
|
||||
expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
|
||||
});
|
||||
test.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
|
||||
test('condition set object can be deleted by Search Tree Actions menu on @localStorage', async ({ page }) => {
|
||||
//Navigate to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
//Assertions on loaded Condition Set in main view. This is a stateful transition step after page.goto()
|
||||
await expect(page.locator('a:has-text("Unnamed Condition Set Condition Set") >> nth=0')).toBeVisible();
|
||||
|
||||
const numberOfConditionSetsToStart = await page.locator('a:has-text("Unnamed Condition Set Condition Set")').count();
|
||||
|
||||
// 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');
|
||||
|
||||
});
|
||||
});
|
||||
|
@ -24,13 +24,17 @@
|
||||
This test suite is dedicated to tests which verify the basic operations surrounding imagery,
|
||||
but only assume that example imagery is present.
|
||||
*/
|
||||
/* globals process */
|
||||
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const { test } = require('../../../fixtures.js');
|
||||
const { expect } = require('@playwright/test');
|
||||
|
||||
test.describe('Example Imagery', () => {
|
||||
const backgroundImageSelector = '.c-imagery__main-image__background-image';
|
||||
|
||||
//The following block of tests verifies the basic functionality of example imagery and serves as a template for Imagery objects embedded in other objects.
|
||||
test.describe('Example Imagery Object', () => {
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
page.on('console', msg => console.log(msg.text()))
|
||||
//Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
@ -42,30 +46,35 @@ test.describe('Example Imagery', () => {
|
||||
|
||||
// Click text=OK
|
||||
await Promise.all([
|
||||
page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine/dab945d4-5a84-480e-8180-222b4aa730fa?tc.mode=fixed&tc.startBound=1639696164435&tc.endBound=1639697964435&tc.timeSystem=utc&view=conditionSet.view' }*/),
|
||||
page.click('text=OK')
|
||||
page.waitForNavigation({waitUntil: 'networkidle'}),
|
||||
page.click('text=OK'),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
// Close Banner
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
|
||||
//Wait until Save Banner is gone
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
});
|
||||
|
||||
const backgroundImageSelector = '.c-imagery__main-image__background-image';
|
||||
test('Can use Mouse Wheel to zoom in and out of latest image', async ({ page }) => {
|
||||
const bgImageLocator = await page.locator(backgroundImageSelector);
|
||||
const deltaYStep = 100; //equivalent to 1x zoom
|
||||
await bgImageLocator.hover();
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox();
|
||||
// zoom in
|
||||
await bgImageLocator.hover();
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
await page.mouse.wheel(0, deltaYStep * 2);
|
||||
// wait for zoom animation to finish
|
||||
await bgImageLocator.hover();
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
const imageMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox();
|
||||
// zoom out
|
||||
await bgImageLocator.hover();
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
await page.mouse.wheel(0, -deltaYStep);
|
||||
// wait for zoom animation to finish
|
||||
await bgImageLocator.hover();
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
const imageMouseZoomedOut = await page.locator(backgroundImageSelector).boundingBox();
|
||||
|
||||
expect(imageMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height);
|
||||
@ -77,13 +86,14 @@ test.describe('Example Imagery', () => {
|
||||
|
||||
test('Can use alt+drag to move around image once zoomed in', async ({ page }) => {
|
||||
const deltaYStep = 100; //equivalent to 1x zoom
|
||||
const panHotkey = process.platform === 'linux' ? ['Control', 'Alt'] : ['Alt'];
|
||||
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
|
||||
const bgImageLocator = await page.locator(backgroundImageSelector);
|
||||
await bgImageLocator.hover();
|
||||
// zoom in
|
||||
await page.mouse.wheel(0, deltaYStep * 2);
|
||||
await bgImageLocator.hover();
|
||||
const zoomedBoundingBox = await bgImageLocator.boundingBox();
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
const zoomedBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;
|
||||
const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
|
||||
// move to the right
|
||||
@ -91,127 +101,653 @@ test.describe('Example Imagery', () => {
|
||||
// center the mouse pointer
|
||||
await page.mouse.move(imageCenterX, imageCenterY);
|
||||
|
||||
//Get Diagnostic info about process environment
|
||||
console.log('process.platform is ' + process.platform);
|
||||
const getUA = await page.evaluate(() => navigator.userAgent);
|
||||
console.log('navigator.userAgent ' + getUA);
|
||||
// Pan Imagery Hints
|
||||
const expectedAltText = process.platform === 'linux' ? 'Ctrl+Alt drag to pan' : 'Alt drag to pan';
|
||||
const imageryHintsText = await page.locator('.c-imagery__hints').innerText();
|
||||
expect(expectedAltText).toEqual(imageryHintsText);
|
||||
|
||||
// pan right
|
||||
await page.keyboard.down('Alt');
|
||||
await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(imageCenterX - 200, imageCenterY, 10);
|
||||
await page.mouse.up();
|
||||
await page.keyboard.up('Alt');
|
||||
const afterRightPanBoundingBox = await bgImageLocator.boundingBox();
|
||||
await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
|
||||
const afterRightPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
expect(zoomedBoundingBox.x).toBeGreaterThan(afterRightPanBoundingBox.x);
|
||||
|
||||
// pan left
|
||||
await page.keyboard.down('Alt');
|
||||
await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(imageCenterX, imageCenterY, 10);
|
||||
await page.mouse.up();
|
||||
await page.keyboard.up('Alt');
|
||||
const afterLeftPanBoundingBox = await bgImageLocator.boundingBox();
|
||||
await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
|
||||
const afterLeftPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
expect(afterRightPanBoundingBox.x).toBeLessThan(afterLeftPanBoundingBox.x);
|
||||
|
||||
// pan up
|
||||
await page.mouse.move(imageCenterX, imageCenterY);
|
||||
await page.keyboard.down('Alt');
|
||||
await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(imageCenterX, imageCenterY + 200, 10);
|
||||
await page.mouse.up();
|
||||
await page.keyboard.up('Alt');
|
||||
const afterUpPanBoundingBox = await bgImageLocator.boundingBox();
|
||||
await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
|
||||
const afterUpPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
expect(afterUpPanBoundingBox.y).toBeGreaterThan(afterLeftPanBoundingBox.y);
|
||||
|
||||
// pan down
|
||||
await page.keyboard.down('Alt');
|
||||
await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(imageCenterX, imageCenterY - 200, 10);
|
||||
await page.mouse.up();
|
||||
await page.keyboard.up('Alt');
|
||||
const afterDownPanBoundingBox = await bgImageLocator.boundingBox();
|
||||
await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
|
||||
const afterDownPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
expect(afterDownPanBoundingBox.y).toBeLessThan(afterUpPanBoundingBox.y);
|
||||
|
||||
});
|
||||
|
||||
test('Can use + - buttons to zoom on the image', async ({ page }) => {
|
||||
const bgImageLocator = await page.locator(backgroundImageSelector);
|
||||
await bgImageLocator.hover();
|
||||
const zoomInBtn = await page.locator('.t-btn-zoom-in');
|
||||
const zoomOutBtn = await page.locator('.t-btn-zoom-out');
|
||||
const initialBoundingBox = await bgImageLocator.boundingBox();
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
const zoomInBtn = page.locator('.t-btn-zoom-in').nth(0);
|
||||
const zoomOutBtn = page.locator('.t-btn-zoom-out').nth(0);
|
||||
const initialBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
|
||||
await zoomInBtn.click();
|
||||
await zoomInBtn.click();
|
||||
// wait for zoom animation to finish
|
||||
await bgImageLocator.hover();
|
||||
const zoomedInBoundingBox = await bgImageLocator.boundingBox();
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
const zoomedInBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
expect(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
|
||||
expect(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);
|
||||
|
||||
await zoomOutBtn.click();
|
||||
// wait for zoom animation to finish
|
||||
await bgImageLocator.hover();
|
||||
const zoomedOutBoundingBox = await bgImageLocator.boundingBox();
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
const zoomedOutBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
expect(zoomedOutBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height);
|
||||
expect(zoomedOutBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width);
|
||||
|
||||
});
|
||||
|
||||
test('Can use the reset button to reset the image', async ({ page }) => {
|
||||
const bgImageLocator = await page.locator(backgroundImageSelector);
|
||||
await bgImageLocator.hover();
|
||||
const zoomInBtn = await page.locator('.t-btn-zoom-in');
|
||||
const zoomResetBtn = await page.locator('.t-btn-zoom-reset');
|
||||
const initialBoundingBox = await bgImageLocator.boundingBox();
|
||||
test('Can use the reset button to reset the image', async ({ page }, testInfo) => {
|
||||
test.slow(testInfo.project === 'chrome-beta', "This test is slow in chrome-beta");
|
||||
// wait for zoom animation to finish
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
|
||||
const zoomInBtn = page.locator('.t-btn-zoom-in').nth(0);
|
||||
const zoomResetBtn = page.locator('.t-btn-zoom-reset').nth(0);
|
||||
const initialBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
|
||||
await zoomInBtn.click();
|
||||
// wait for zoom animation to finish
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
await zoomInBtn.click();
|
||||
// wait for zoom animation to finish
|
||||
await bgImageLocator.hover();
|
||||
const zoomedInBoundingBox = await bgImageLocator.boundingBox();
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
|
||||
const zoomedInBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
expect.soft(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
|
||||
expect.soft(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);
|
||||
|
||||
await zoomResetBtn.click();
|
||||
await bgImageLocator.hover();
|
||||
// wait for zoom animation to finish
|
||||
// FIXME: The zoom is flakey, sometimes not returning to original dimensions
|
||||
// https://github.com/nasa/openmct/issues/5491
|
||||
await expect.poll(async () => {
|
||||
await zoomResetBtn.click();
|
||||
const boundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
|
||||
const resetBoundingBox = await bgImageLocator.boundingBox();
|
||||
expect.soft(resetBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height);
|
||||
expect.soft(resetBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width);
|
||||
|
||||
expect.soft(resetBoundingBox.height).toEqual(initialBoundingBox.height);
|
||||
expect(resetBoundingBox.width).toEqual(initialBoundingBox.width);
|
||||
return boundingBox;
|
||||
}, {
|
||||
timeout: 10 * 1000
|
||||
}).toEqual(initialBoundingBox);
|
||||
});
|
||||
|
||||
test('Using the zoom features does not pause telemetry', async ({ page }) => {
|
||||
const pausePlayButton = page.locator('.c-button.pause-play');
|
||||
// wait for zoom animation to finish
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
|
||||
// open the time conductor drop down
|
||||
await page.locator('button:has-text("Fixed Timespan")').click();
|
||||
// Click local clock
|
||||
await page.locator('[data-testid="conductor-modeOption-realtime"]').click();
|
||||
|
||||
await expect.soft(pausePlayButton).not.toHaveClass(/is-paused/);
|
||||
const zoomInBtn = page.locator('.t-btn-zoom-in').nth(0);
|
||||
await zoomInBtn.click();
|
||||
// wait for zoom animation to finish
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
|
||||
return expect(pausePlayButton).not.toHaveClass(/is-paused/);
|
||||
});
|
||||
|
||||
//test('Can use Mouse Wheel to zoom in and out of previous image');
|
||||
//test('Can zoom into the latest image and the real-time/fixed-time imagery will pause');
|
||||
//test.skip('Can zoom into a previous image from thumbstrip in real-time or fixed-time');
|
||||
//test.skip('Clicking on the left arrow should pause the imagery and go to previous image');
|
||||
//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');
|
||||
});
|
||||
|
||||
test.describe('Example Imagery in Display layout', () => {
|
||||
test.skip('Can use Mouse Wheel to zoom in and out of previous image');
|
||||
test.skip('Can use alt+drag to move around image once zoomed in');
|
||||
test.skip('Can zoom into the latest image and the real-time/fixed-time imagery will pause');
|
||||
test.skip('Clicking on the left arrow should pause the imagery and go to previous image');
|
||||
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');
|
||||
// The following test case will cover these scenarios
|
||||
// ('Can use Mouse Wheel to zoom in and out of previous image');
|
||||
// ('Can use alt+drag to move around image once zoomed in');
|
||||
// ('Clicking on the left arrow should pause the imagery and go to previous image');
|
||||
// ('If the imagery view is in pause mode, it should not be updated when new images come in');
|
||||
// ('If the imagery view is not in pause mode, it should be updated when new images come in');
|
||||
test('Example Imagery in Display layout', async ({ page, browserName }) => {
|
||||
test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox');
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/5265'
|
||||
});
|
||||
|
||||
// Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
// Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
// Click text=Example Imagery
|
||||
await page.click('text=Example Imagery');
|
||||
|
||||
// Clear and set Image load delay to minimum value
|
||||
// FIXME: Update the value to 5000 ms when this bug is fixed.
|
||||
// See: https://github.com/nasa/openmct/issues/5265
|
||||
await page.locator('input[type="number"]').fill('');
|
||||
await page.locator('input[type="number"]').fill('0');
|
||||
|
||||
// Click text=OK
|
||||
await Promise.all([
|
||||
page.waitForNavigation({waitUntil: 'networkidle'}),
|
||||
page.click('text=OK'),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
|
||||
// Wait until Save Banner is gone
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
|
||||
// Click previous image button
|
||||
const previousImageButton = page.locator('.c-nav--prev');
|
||||
await previousImageButton.click();
|
||||
|
||||
// Verify previous image
|
||||
const selectedImage = page.locator('.selected');
|
||||
await expect(selectedImage).toBeVisible();
|
||||
|
||||
// Zoom in
|
||||
const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox();
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
const deltaYStep = 100; // equivalent to 1x zoom
|
||||
await page.mouse.wheel(0, deltaYStep * 2);
|
||||
const zoomedBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;
|
||||
const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
|
||||
|
||||
// Wait for zoom animation to finish
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
const imageMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox();
|
||||
expect(imageMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height);
|
||||
expect(imageMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width);
|
||||
|
||||
// Center the mouse pointer
|
||||
await page.mouse.move(imageCenterX, imageCenterY);
|
||||
|
||||
// Pan Imagery Hints
|
||||
const expectedAltText = process.platform === 'linux' ? 'Ctrl+Alt drag to pan' : 'Alt drag to pan';
|
||||
const imageryHintsText = await page.locator('.c-imagery__hints').innerText();
|
||||
expect(expectedAltText).toEqual(imageryHintsText);
|
||||
|
||||
// Click next image button
|
||||
const nextImageButton = page.locator('.c-nav--next');
|
||||
await nextImageButton.click();
|
||||
|
||||
// Click time conductor mode button
|
||||
await page.locator('.c-mode-button').click();
|
||||
|
||||
// Select local clock mode
|
||||
await page.locator('[data-testid=conductor-modeOption-realtime]').click();
|
||||
|
||||
// Zoom in on next image
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
await page.mouse.wheel(0, deltaYStep * 2);
|
||||
|
||||
// Wait for zoom animation to finish
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
const imageNextMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox();
|
||||
expect(imageNextMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height);
|
||||
expect(imageNextMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width);
|
||||
|
||||
// Click previous image button
|
||||
await previousImageButton.click();
|
||||
|
||||
// Verify previous image
|
||||
await expect(selectedImage).toBeVisible();
|
||||
|
||||
const imageCount = await page.locator('.c-imagery__thumb').count();
|
||||
await expect.poll(async () => {
|
||||
const newImageCount = await page.locator('.c-imagery__thumb').count();
|
||||
|
||||
return newImageCount;
|
||||
}, {
|
||||
message: "verify that old images are discarded",
|
||||
timeout: 6 * 1000
|
||||
}).toBe(imageCount);
|
||||
|
||||
// Verify selected image is still displayed
|
||||
await expect(selectedImage).toBeVisible();
|
||||
});
|
||||
|
||||
test.describe('Example imagery thumbnails resize in display layouts', () => {
|
||||
test('Resizing the layout changes thumbnail visibility and size', async ({ page }) => {
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
const thumbsWrapperLocator = page.locator('.c-imagery__thumbs-wrapper');
|
||||
// Click button:has-text("Create")
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
|
||||
// Click li:has-text("Display Layout")
|
||||
await page.locator('li:has-text("Display Layout")').click();
|
||||
const displayLayoutTitleField = page.locator('text=Properties Title Notes Horizontal grid (px) Vertical grid (px) Horizontal size ( >> input[type="text"]');
|
||||
await displayLayoutTitleField.click();
|
||||
|
||||
await displayLayoutTitleField.fill('Thumbnail Display Layout');
|
||||
|
||||
// Click text=OK
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=OK').click()
|
||||
]);
|
||||
|
||||
// Click text=Snapshot Save and Finish Editing Save and Continue Editing >> button >> nth=1
|
||||
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
|
||||
|
||||
// Click text=Save and Finish Editing
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
// Click button:has-text("Create")
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
|
||||
// Click li:has-text("Example Imagery")
|
||||
await page.locator('li:has-text("Example Imagery")').click();
|
||||
|
||||
const imageryTitleField = page.locator('text=Properties Title Notes Images url list (comma separated) Image load delay (milli >> input[type="text"]');
|
||||
// Click text=Properties Title Notes Images url list (comma separated) Image load delay (milli >> input[type="text"]
|
||||
await imageryTitleField.click();
|
||||
|
||||
// Fill text=Properties Title Notes Images url list (comma separated) Image load delay (milli >> input[type="text"]
|
||||
await imageryTitleField.fill('Thumbnail Example Imagery');
|
||||
|
||||
// Click text=OK
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=OK').click()
|
||||
]);
|
||||
|
||||
// Click text=Thumbnail Example Imagery Imagery Layout Snapshot >> button >> nth=0
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=Thumbnail Example Imagery Imagery Layout Snapshot >> button').first().click()
|
||||
]);
|
||||
|
||||
// Edit mode
|
||||
await page.locator('text=Thumbnail Display Layout Snapshot >> button').nth(3).click();
|
||||
|
||||
// Click on example imagery to expose toolbar
|
||||
await page.locator('text=Thumbnail Example Imagery Snapshot Large View').click();
|
||||
|
||||
// expect thumbnails not be visible when first added
|
||||
expect.soft(thumbsWrapperLocator.isHidden()).toBeTruthy();
|
||||
|
||||
// Resize the example imagery vertically to change the thumbnail visibility
|
||||
/*
|
||||
The following arbitrary values are added to observe the separate visual
|
||||
conditions of the thumbnails (hidden, small thumbnails, regular thumbnails).
|
||||
Specifically, height is set to 50px for small thumbs and 100px for regular
|
||||
*/
|
||||
// Click #mct-input-id-103
|
||||
await page.locator('#mct-input-id-103').click();
|
||||
|
||||
// Fill #mct-input-id-103
|
||||
await page.locator('#mct-input-id-103').fill('50');
|
||||
|
||||
expect(thumbsWrapperLocator.isVisible()).toBeTruthy();
|
||||
await expect(thumbsWrapperLocator).toHaveClass(/is-small-thumbs/);
|
||||
|
||||
// Resize the example imagery vertically to change the thumbnail visibility
|
||||
// Click #mct-input-id-103
|
||||
await page.locator('#mct-input-id-103').click();
|
||||
|
||||
// Fill #mct-input-id-103
|
||||
await page.locator('#mct-input-id-103').fill('100');
|
||||
|
||||
expect(thumbsWrapperLocator.isVisible()).toBeTruthy();
|
||||
await expect(thumbsWrapperLocator).not.toHaveClass(/is-small-thumbs/);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Example Imagery in Flexible layout', () => {
|
||||
test.skip('Can use Mouse Wheel to zoom in and out of previous image');
|
||||
test.skip('Can use alt+drag to move around image once zoomed in');
|
||||
test.skip('Can zoom into the latest image and the real-time/fixed-time imagery will pause');
|
||||
test.skip('Clicking on the left arrow should pause the imagery and go to previous image');
|
||||
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');
|
||||
test('Example Imagery in Flexible layout', async ({ page, browserName }) => {
|
||||
test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox');
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/5326'
|
||||
});
|
||||
|
||||
// Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
// Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
// Click text=Example Imagery
|
||||
await page.click('text=Example Imagery');
|
||||
|
||||
// Clear and set Image load delay (milliseconds)
|
||||
await page.click('input[type="number"]', {clickCount: 3});
|
||||
await page.type('input[type="number"]', "20");
|
||||
|
||||
// Click text=OK
|
||||
await Promise.all([
|
||||
page.waitForNavigation({waitUntil: 'networkidle'}),
|
||||
page.click('text=OK'),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
// Wait until Save Banner is gone
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
|
||||
// Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
// Click text=Flexible Layout
|
||||
await page.click('text=Flexible Layout');
|
||||
|
||||
// Assert Flexable layout
|
||||
await expect(page.locator('.js-form-title')).toHaveText('Create a New Flexible Layout');
|
||||
|
||||
await page.locator('form[name="mctForm"] >> text=My Items').click();
|
||||
|
||||
// Click My Items
|
||||
await Promise.all([
|
||||
page.locator('text=OK').click(),
|
||||
page.waitForNavigation({waitUntil: 'networkidle'})
|
||||
]);
|
||||
|
||||
// Click My Items
|
||||
await page.locator('.c-disclosure-triangle').click();
|
||||
|
||||
// Right click example imagery
|
||||
await page.click(('text=Unnamed Example Imagery'), { button: 'right' });
|
||||
|
||||
// Click move
|
||||
await page.locator('.icon-move').click();
|
||||
|
||||
// Click triangle to open sub menu
|
||||
await page.locator('.c-form__section .c-disclosure-triangle').click();
|
||||
|
||||
// Click Flexable Layout
|
||||
await page.click('.c-overlay__outer >> text=Unnamed Flexible Layout');
|
||||
|
||||
// Click text=OK
|
||||
await page.locator('text=OK').click();
|
||||
|
||||
// Save template
|
||||
await saveTemplate(page);
|
||||
|
||||
// Zoom in
|
||||
await mouseZoomIn(page);
|
||||
|
||||
// Center the mouse pointer
|
||||
const zoomedBoundingBox = await await page.locator(backgroundImageSelector).boundingBox();
|
||||
const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;
|
||||
const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
|
||||
await page.mouse.move(imageCenterX, imageCenterY);
|
||||
|
||||
// Pan zoom
|
||||
await panZoomAndAssertImageProperties(page);
|
||||
|
||||
// Click previous image button
|
||||
const previousImageButton = page.locator('.c-nav--prev');
|
||||
await previousImageButton.click();
|
||||
|
||||
// Verify previous image
|
||||
const selectedImage = page.locator('.selected');
|
||||
await expect(selectedImage).toBeVisible();
|
||||
|
||||
// Click time conductor mode button
|
||||
await page.locator('.c-mode-button').click();
|
||||
|
||||
// Select local clock mode
|
||||
await page.locator('[data-testid=conductor-modeOption-realtime]').click();
|
||||
|
||||
// Zoom in on next image
|
||||
await mouseZoomIn(page);
|
||||
|
||||
// Click previous image button
|
||||
await previousImageButton.click();
|
||||
|
||||
// Verify previous image
|
||||
await expect(selectedImage).toBeVisible();
|
||||
|
||||
const imageCount = await page.locator('.c-imagery__thumb').count();
|
||||
await expect.poll(async () => {
|
||||
const newImageCount = await page.locator('.c-imagery__thumb').count();
|
||||
|
||||
return newImageCount;
|
||||
}, {
|
||||
message: "verify that old images are discarded",
|
||||
timeout: 6 * 1000
|
||||
}).toBe(imageCount);
|
||||
|
||||
// Verify selected image is still displayed
|
||||
await expect(selectedImage).toBeVisible();
|
||||
|
||||
// Unpause imagery
|
||||
await page.locator('.pause-play').click();
|
||||
|
||||
//Get background-image url from background-image css prop
|
||||
await assertBackgroundImageUrlFromBackgroundCss(page);
|
||||
|
||||
// Open the image filter menu
|
||||
await page.locator('[role=toolbar] button[title="Brightness and contrast"]').click();
|
||||
|
||||
// Drag the brightness and contrast sliders around and assert filter values
|
||||
await dragBrightnessSliderAndAssertFilterValues(page);
|
||||
await dragContrastSliderAndAssertFilterValues(page);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Example Imagery in Tabs view', () => {
|
||||
test.skip('Can use Mouse Wheel to zoom in and out of previous image');
|
||||
test.skip('Can use alt+drag to move around image once zoomed in');
|
||||
test.skip('Can zoom into the latest image and the real-time/fixed-time imagery will pause');
|
||||
test.skip('Can zoom into a previous image from thumbstrip in real-time or fixed-time');
|
||||
test.skip('Clicking on the left arrow should pause the imagery and go to previous image');
|
||||
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');
|
||||
test.fixme('Can use Mouse Wheel to zoom in and out of previous image');
|
||||
test.fixme('Can use alt+drag to move around image once zoomed in');
|
||||
test.fixme('Can zoom into the latest image and the real-time/fixed-time imagery will pause');
|
||||
test.fixme('Can zoom into a previous image from thumbstrip in real-time or fixed-time');
|
||||
test.fixme('Clicking on the left arrow should pause the imagery and go to previous image');
|
||||
test.fixme('If the imagery view is in pause mode, it should not be updated when new images come in');
|
||||
test.fixme('If the imagery view is not in pause mode, it should be updated when new images come in');
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function saveTemplate(page) {
|
||||
await page.locator('.c-button--menu.c-button--major.icon-save').click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Drag the brightness slider to max, min, and midpoint and assert the filter values
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function dragBrightnessSliderAndAssertFilterValues(page) {
|
||||
const brightnessSlider = 'div.c-image-controls__slider-wrapper.icon-brightness > input';
|
||||
const brightnessBoundingBox = await page.locator(brightnessSlider).boundingBox();
|
||||
const brightnessMidX = brightnessBoundingBox.x + brightnessBoundingBox.width / 2;
|
||||
const brightnessMidY = brightnessBoundingBox.y + brightnessBoundingBox.height / 2;
|
||||
|
||||
await page.locator(brightnessSlider).hover({trial: true});
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(brightnessBoundingBox.x + brightnessBoundingBox.width, brightnessMidY);
|
||||
await assertBackgroundImageBrightness(page, '500');
|
||||
await page.mouse.move(brightnessBoundingBox.x, brightnessMidY);
|
||||
await assertBackgroundImageBrightness(page, '0');
|
||||
await page.mouse.move(brightnessMidX, brightnessMidY);
|
||||
await assertBackgroundImageBrightness(page, '250');
|
||||
await page.mouse.up();
|
||||
}
|
||||
|
||||
/**
|
||||
* Drag the contrast slider to max, min, and midpoint and assert the filter values
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function dragContrastSliderAndAssertFilterValues(page) {
|
||||
const contrastSlider = 'div.c-image-controls__slider-wrapper.icon-contrast > input';
|
||||
const contrastBoundingBox = await page.locator(contrastSlider).boundingBox();
|
||||
const contrastMidX = contrastBoundingBox.x + contrastBoundingBox.width / 2;
|
||||
const contrastMidY = contrastBoundingBox.y + contrastBoundingBox.height / 2;
|
||||
|
||||
await page.locator(contrastSlider).hover({trial: true});
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(contrastBoundingBox.x + contrastBoundingBox.width, contrastMidY);
|
||||
await assertBackgroundImageContrast(page, '500');
|
||||
await page.mouse.move(contrastBoundingBox.x, contrastMidY);
|
||||
await assertBackgroundImageContrast(page, '0');
|
||||
await page.mouse.move(contrastMidX, contrastMidY);
|
||||
await assertBackgroundImageContrast(page, '250');
|
||||
await page.mouse.up();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the filter:brightness value of the current background-image and
|
||||
* asserts against an expected value
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {String} expected The expected brightness value
|
||||
*/
|
||||
async function assertBackgroundImageBrightness(page, expected) {
|
||||
const backgroundImage = page.locator('.c-imagery__main-image__background-image');
|
||||
|
||||
// Get the brightness filter value (i.e: filter: brightness(500%) => "500")
|
||||
const actual = await backgroundImage.evaluate((el) => {
|
||||
return el.style.filter.match(/brightness\((\d{1,3})%\)/)[1];
|
||||
});
|
||||
expect(actual).toBe(expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the filter:contrast value of the current background-image and
|
||||
* asserts against an expected value
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {String} expected The expected contrast value
|
||||
*/
|
||||
async function assertBackgroundImageContrast(page, expected) {
|
||||
const backgroundImage = page.locator('.c-imagery__main-image__background-image');
|
||||
|
||||
// Get the contrast filter value (i.e: filter: contrast(500%) => "500")
|
||||
const actual = await backgroundImage.evaluate((el) => {
|
||||
return el.style.filter.match(/contrast\((\d{1,3})%\)/)[1];
|
||||
});
|
||||
expect(actual).toBe(expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function assertBackgroundImageUrlFromBackgroundCss(page) {
|
||||
const backgroundImage = page.locator('.c-imagery__main-image__background-image');
|
||||
let backgroundImageUrl = await backgroundImage.evaluate((el) => {
|
||||
return window.getComputedStyle(el).getPropertyValue('background-image').match(/url\(([^)]+)\)/)[1];
|
||||
});
|
||||
let backgroundImageUrl1 = backgroundImageUrl.slice(1, -1); //forgive me, padre
|
||||
console.log('backgroundImageUrl1 ' + backgroundImageUrl1);
|
||||
|
||||
let backgroundImageUrl2;
|
||||
await expect.poll(async () => {
|
||||
// Verify next image has updated
|
||||
let backgroundImageUrlNext = await backgroundImage.evaluate((el) => {
|
||||
return window.getComputedStyle(el).getPropertyValue('background-image').match(/url\(([^)]+)\)/)[1];
|
||||
});
|
||||
backgroundImageUrl2 = backgroundImageUrlNext.slice(1, -1); //forgive me, padre
|
||||
|
||||
return backgroundImageUrl2;
|
||||
}, {
|
||||
message: "verify next image has updated",
|
||||
timeout: 6 * 1000
|
||||
}).not.toBe(backgroundImageUrl1);
|
||||
console.log('backgroundImageUrl2 ' + backgroundImageUrl2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function panZoomAndAssertImageProperties(page) {
|
||||
const panHotkey = process.platform === 'linux' ? ['Control', 'Alt'] : ['Alt'];
|
||||
const expectedAltText = process.platform === 'linux' ? 'Ctrl+Alt drag to pan' : 'Alt drag to pan';
|
||||
const imageryHintsText = await page.locator('.c-imagery__hints').innerText();
|
||||
expect(expectedAltText).toEqual(imageryHintsText);
|
||||
const zoomedBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;
|
||||
const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
|
||||
|
||||
// Pan right
|
||||
await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(imageCenterX - 200, imageCenterY, 10);
|
||||
await page.mouse.up();
|
||||
await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
|
||||
const afterRightPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
expect(zoomedBoundingBox.x).toBeGreaterThan(afterRightPanBoundingBox.x);
|
||||
|
||||
// Pan left
|
||||
await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(imageCenterX, imageCenterY, 10);
|
||||
await page.mouse.up();
|
||||
await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
|
||||
const afterLeftPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
expect(afterRightPanBoundingBox.x).toBeLessThan(afterLeftPanBoundingBox.x);
|
||||
|
||||
// Pan up
|
||||
await page.mouse.move(imageCenterX, imageCenterY);
|
||||
await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(imageCenterX, imageCenterY + 200, 10);
|
||||
await page.mouse.up();
|
||||
await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
|
||||
const afterUpPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
expect(afterUpPanBoundingBox.y).toBeGreaterThanOrEqual(afterLeftPanBoundingBox.y);
|
||||
|
||||
// Pan down
|
||||
await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(imageCenterX, imageCenterY - 200, 10);
|
||||
await page.mouse.up();
|
||||
await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
|
||||
const afterDownPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
expect(afterDownPanBoundingBox.y).toBeLessThanOrEqual(afterUpPanBoundingBox.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function mouseZoomIn(page) {
|
||||
// Zoom in
|
||||
const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox();
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
const deltaYStep = 100; // equivalent to 1x zoom
|
||||
await page.mouse.wheel(0, deltaYStep * 2);
|
||||
const zoomedBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;
|
||||
const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
|
||||
|
||||
// center the mouse pointer
|
||||
await page.mouse.move(imageCenterX, imageCenterY);
|
||||
|
||||
// Wait for zoom animation to finish
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
const imageMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox();
|
||||
expect(imageMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height);
|
||||
expect(imageMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width);
|
||||
}
|
||||
|
30
e2e/tests/plugins/notebook/addInitRestrictedNotebook.js
Normal file
30
e2e/tests/plugins/notebook/addInitRestrictedNotebook.js
Normal file
@ -0,0 +1,30 @@
|
||||
/*****************************************************************************
|
||||
* 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'));
|
||||
});
|
198
e2e/tests/plugins/notebook/notebook.e2e.spec.js
Normal file
198
e2e/tests/plugins/notebook/notebook.e2e.spec.js
Normal file
@ -0,0 +1,198 @@
|
||||
/*****************************************************************************
|
||||
* 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
|
||||
});
|
||||
});
|
255
e2e/tests/plugins/notebook/restrictedNotebook.e2e.spec.js
Normal file
255
e2e/tests/plugins/notebook/restrictedNotebook.e2e.spec.js
Normal file
@ -0,0 +1,255 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
const { test } = require('../../../fixtures');
|
||||
const { expect } = require('@playwright/test');
|
||||
const path = require('path');
|
||||
|
||||
const TEST_TEXT = 'Testing text for entries.';
|
||||
const TEST_TEXT_NAME = 'Test Page';
|
||||
const CUSTOM_NAME = 'CUSTOM_NAME';
|
||||
const NOTEBOOK_DROP_AREA = '.c-notebook__drag-area';
|
||||
|
||||
test.describe('Restricted Notebook', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await startAndAddRestrictedNotebookObject(page);
|
||||
});
|
||||
|
||||
test('Can be renamed @addInit', async ({ page }) => {
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText(`Unnamed ${CUSTOM_NAME}`);
|
||||
});
|
||||
|
||||
test('Can be deleted if there are no locked pages @addInit', async ({ page }) => {
|
||||
await openContextMenuRestrictedNotebook(page);
|
||||
|
||||
const menuOptions = page.locator('.c-menu ul');
|
||||
await expect.soft(menuOptions).toContainText('Remove');
|
||||
|
||||
const restrictedNotebookTreeObject = page.locator(`a:has-text("Unnamed ${CUSTOM_NAME}")`);
|
||||
|
||||
// notbook tree object exists
|
||||
expect.soft(await restrictedNotebookTreeObject.count()).toEqual(1);
|
||||
|
||||
// Click Remove Text
|
||||
await page.locator('text=Remove').click();
|
||||
|
||||
// Click 'OK' on confirmation window and wait for save banner to appear
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=OK').click(),
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
|
||||
// has been deleted
|
||||
expect(await restrictedNotebookTreeObject.count()).toEqual(0);
|
||||
});
|
||||
|
||||
test('Can be locked if at least one page has one entry @addInit', async ({ page }) => {
|
||||
|
||||
await enterTextEntry(page);
|
||||
|
||||
const commitButton = page.locator('button:has-text("Commit Entries")');
|
||||
expect(await commitButton.count()).toEqual(1);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
test.describe('Restricted Notebook with at least one entry and with the page locked @addInit', () => {
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await startAndAddRestrictedNotebookObject(page);
|
||||
await enterTextEntry(page);
|
||||
await lockPage(page);
|
||||
|
||||
// FIXME: Give ample time for the mutation to happen
|
||||
// https://github.com/nasa/openmct/issues/5409
|
||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||
await page.waitForTimeout(1 * 1000);
|
||||
|
||||
// open sidebar
|
||||
await page.locator('button.c-notebook__toggle-nav-button').click();
|
||||
});
|
||||
|
||||
test('Locked page should now be in a locked state @addInit', async ({ page }, testInfo) => {
|
||||
test.fixme(testInfo.project === 'chrome-beta', "Test is unreliable on chrome-beta");
|
||||
// main lock message on page
|
||||
const lockMessage = page.locator('text=This page has been committed and cannot be modified or removed');
|
||||
expect.soft(await lockMessage.count()).toEqual(1);
|
||||
|
||||
// lock icon on page in sidebar
|
||||
const pageLockIcon = page.locator('ul.c-notebook__pages li div.icon-lock');
|
||||
expect.soft(await pageLockIcon.count()).toEqual(1);
|
||||
|
||||
// no way to remove a restricted notebook with a locked page
|
||||
await openContextMenuRestrictedNotebook(page);
|
||||
const menuOptions = page.locator('.c-menu ul');
|
||||
|
||||
await expect(menuOptions).not.toContainText('Remove');
|
||||
});
|
||||
|
||||
test('Can still: add page, rename, add entry, delete unlocked pages @addInit', async ({ page }) => {
|
||||
// Click text=Page Add >> button
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=Page Add >> button').click()
|
||||
]);
|
||||
// Click text=Unnamed Page >> nth=1
|
||||
await page.locator('text=Unnamed Page').nth(1).click();
|
||||
// Press a with modifiers
|
||||
await page.locator('text=Unnamed Page').nth(1).fill(TEST_TEXT_NAME);
|
||||
|
||||
// expect to be able to rename unlocked pages
|
||||
const newPageElement = page.locator(`text=${TEST_TEXT_NAME}`);
|
||||
const newPageCount = await newPageElement.count();
|
||||
await newPageElement.press('Enter'); // exit contenteditable state
|
||||
expect.soft(newPageCount).toEqual(1);
|
||||
|
||||
// enter test text
|
||||
await enterTextEntry(page);
|
||||
|
||||
// expect new page to be lockable
|
||||
const commitButton = page.locator('BUTTON:HAS-TEXT("COMMIT ENTRIES")');
|
||||
expect.soft(await commitButton.count()).toEqual(1);
|
||||
|
||||
// Click text=Unnamed PageTest Page >> button
|
||||
await page.locator('text=Unnamed PageTest Page >> button').click();
|
||||
// Click text=Delete Page
|
||||
await page.locator('text=Delete Page').click();
|
||||
// Click text=Ok
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=Ok').click()
|
||||
]);
|
||||
|
||||
// deleted page, should no longer exist
|
||||
const deletedPageElement = page.locator(`text=${TEST_TEXT_NAME}`);
|
||||
expect(await deletedPageElement.count()).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Restricted Notebook with a page locked and with an embed @addInit', () => {
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await startAndAddRestrictedNotebookObject(page);
|
||||
await dragAndDropEmbed(page);
|
||||
});
|
||||
|
||||
test('Allows embeds to be deleted if page unlocked @addInit', async ({ page }) => {
|
||||
// Click .c-ne__embed__name .c-popup-menu-button
|
||||
await page.locator('.c-ne__embed__name .c-popup-menu-button').click(); // embed popup menu
|
||||
|
||||
const embedMenu = page.locator('body >> .c-menu');
|
||||
await expect(embedMenu).toContainText('Remove This Embed');
|
||||
});
|
||||
|
||||
test('Disallows embeds to be deleted if page locked @addInit', async ({ page }) => {
|
||||
await lockPage(page);
|
||||
// Click .c-ne__embed__name .c-popup-menu-button
|
||||
await page.locator('.c-ne__embed__name .c-popup-menu-button').click(); // embed popup menu
|
||||
|
||||
const embedMenu = page.locator('body >> .c-menu');
|
||||
await expect(embedMenu).not.toContainText('Remove This Embed');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function startAndAddRestrictedNotebookObject(page) {
|
||||
// eslint-disable-next-line no-undef
|
||||
await page.addInitScript({ path: path.join(__dirname, 'addInitRestrictedNotebook.js') });
|
||||
//Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
//Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
// Click text=CUSTOME_NAME
|
||||
await page.click(`text=${CUSTOM_NAME}`); // secondarily tests renamability also
|
||||
// Click text=OK
|
||||
await Promise.all([
|
||||
page.waitForNavigation({waitUntil: 'networkidle'}),
|
||||
page.click('text=OK')
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function enterTextEntry(page) {
|
||||
// Click .c-notebook__drag-area
|
||||
await page.locator(NOTEBOOK_DROP_AREA).click();
|
||||
|
||||
// enter text
|
||||
await page.locator('div.c-ne__text').click();
|
||||
await page.locator('div.c-ne__text').fill(TEST_TEXT);
|
||||
await page.locator('div.c-ne__text').press('Enter');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function dragAndDropEmbed(page) {
|
||||
// Click button:has-text("Create")
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
// Click li:has-text("Sine Wave Generator")
|
||||
await page.locator('li:has-text("Sine Wave Generator")').click();
|
||||
// Click form[name="mctForm"] >> text=My Items
|
||||
await page.locator('form[name="mctForm"] >> text=My Items').click();
|
||||
// Click text=OK
|
||||
await page.locator('text=OK').click();
|
||||
// Click text=Open MCT My Items >> span >> nth=3
|
||||
await page.locator('text=Open MCT My Items >> span').nth(3).click();
|
||||
// Click text=Unnamed CUSTOM_NAME
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=Unnamed CUSTOM_NAME').click()
|
||||
]);
|
||||
|
||||
await page.dragAndDrop('text=UNNAMED SINE WAVE GENERATOR', NOTEBOOK_DROP_AREA);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function lockPage(page) {
|
||||
const commitButton = page.locator('button:has-text("Commit Entries")');
|
||||
await commitButton.click();
|
||||
|
||||
//Wait until Lock Banner is visible
|
||||
await page.locator('text=Lock Page').click();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function openContextMenuRestrictedNotebook(page) {
|
||||
const myItemsFolder = page.locator('text=Open MCT My Items >> span').nth(3);
|
||||
const className = await myItemsFolder.getAttribute('class');
|
||||
if (!className.includes('c-disclosure-triangle--expanded')) {
|
||||
await myItemsFolder.click();
|
||||
}
|
||||
|
||||
// Click a:has-text("Unnamed CUSTOM_NAME")
|
||||
await page.locator(`a:has-text("Unnamed ${CUSTOM_NAME}")`).click({
|
||||
button: 'right'
|
||||
});
|
||||
}
|
205
e2e/tests/plugins/notebook/tags.e2e.spec.js
Normal file
205
e2e/tests/plugins/notebook/tags.e2e.spec.js
Normal file
@ -0,0 +1,205 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
This test suite is dedicated to tests which verify form functionality.
|
||||
*/
|
||||
|
||||
const { expect } = require('@playwright/test');
|
||||
const { test } = require('../../../fixtures');
|
||||
|
||||
/**
|
||||
* Creates a notebook object and adds an entry.
|
||||
* @param {import('@playwright/test').Page} - page to load
|
||||
* @param {number} [iterations = 1] - the number of entries to create
|
||||
*/
|
||||
async function createNotebookAndEntry(page, iterations = 1) {
|
||||
//Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
// Click button:has-text("Create")
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
await page.locator('[title="Create and save timestamped notes with embedded object snapshots."]').click();
|
||||
// Click button:has-text("OK")
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('[name="mctForm"] >> text=My Items').click(),
|
||||
page.locator('button:has-text("OK")').click()
|
||||
]);
|
||||
|
||||
for (let iteration = 0; iteration < iterations; iteration++) {
|
||||
// Click text=To start a new entry, click here or drag and drop any object
|
||||
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
|
||||
const entryLocator = `[aria-label="Notebook Entry Input"] >> nth = ${iteration}`;
|
||||
await page.locator(entryLocator).click();
|
||||
await page.locator(entryLocator).fill(`Entry ${iteration}`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a notebook object, adds an entry, and adds a tag.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {number} [iterations = 1] - the number of entries (and tags) to create
|
||||
*/
|
||||
async function createNotebookEntryAndTags(page, iterations = 1) {
|
||||
await createNotebookAndEntry(page, iterations);
|
||||
|
||||
for (let iteration = 0; iteration < iterations; iteration++) {
|
||||
// Click text=To start a new entry, click here or drag and drop any object
|
||||
await page.locator(`button:has-text("Add Tag") >> nth = ${iteration}`).click();
|
||||
|
||||
// Click [placeholder="Type to select tag"]
|
||||
await page.locator('[placeholder="Type to select tag"]').click();
|
||||
// Click text=Driving
|
||||
await page.locator('[aria-label="Autocomplete Options"] >> text=Driving').click();
|
||||
|
||||
// Click button:has-text("Add Tag")
|
||||
await page.locator(`button:has-text("Add Tag") >> nth = ${iteration}`).click();
|
||||
// Click [placeholder="Type to select tag"]
|
||||
await page.locator('[placeholder="Type to select tag"]').click();
|
||||
// Click text=Science
|
||||
await page.locator('[aria-label="Autocomplete Options"] >> text=Science').click();
|
||||
}
|
||||
}
|
||||
|
||||
test.describe('Tagging in Notebooks', () => {
|
||||
test('Can load tags', async ({ page }) => {
|
||||
await createNotebookAndEntry(page);
|
||||
// Click text=To start a new entry, click here or drag and drop any object
|
||||
await page.locator('button:has-text("Add Tag")').click();
|
||||
|
||||
// Click [placeholder="Type to select tag"]
|
||||
await page.locator('[placeholder="Type to select tag"]').click();
|
||||
|
||||
await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Science");
|
||||
await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Drilling");
|
||||
await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Driving");
|
||||
});
|
||||
test('Can add tags', async ({ page }) => {
|
||||
await createNotebookEntryAndTags(page);
|
||||
|
||||
await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Science");
|
||||
await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Driving");
|
||||
|
||||
// Click button:has-text("Add Tag")
|
||||
await page.locator('button:has-text("Add Tag")').click();
|
||||
// Click [placeholder="Type to select tag"]
|
||||
await page.locator('[placeholder="Type to select tag"]').click();
|
||||
|
||||
await expect(page.locator('[aria-label="Autocomplete Options"]')).not.toContainText("Science");
|
||||
await expect(page.locator('[aria-label="Autocomplete Options"]')).not.toContainText("Driving");
|
||||
await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Drilling");
|
||||
});
|
||||
test('Can search for tags', async ({ page }) => {
|
||||
await createNotebookEntryAndTags(page);
|
||||
// Click [aria-label="OpenMCT Search"] input[type="search"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
|
||||
// Fill [aria-label="OpenMCT Search"] input[type="search"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc');
|
||||
await expect(page.locator('[aria-label="Search Result"]')).toContainText("Science");
|
||||
await expect(page.locator('[aria-label="Search Result"]')).toContainText("Driving");
|
||||
|
||||
// Click [aria-label="OpenMCT Search"] input[type="search"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
|
||||
// Fill [aria-label="OpenMCT Search"] input[type="search"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Sc');
|
||||
await expect(page.locator('[aria-label="Search Result"]')).toContainText("Science");
|
||||
await expect(page.locator('[aria-label="Search Result"]')).toContainText("Driving");
|
||||
|
||||
// Click [aria-label="OpenMCT Search"] input[type="search"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
|
||||
// Fill [aria-label="OpenMCT Search"] input[type="search"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Xq');
|
||||
await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible();
|
||||
await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('Can delete tags', async ({ page }) => {
|
||||
await createNotebookEntryAndTags(page);
|
||||
await page.locator('[aria-label="Notebook Entries"]').click();
|
||||
// Delete Driving
|
||||
await page.locator('text=Science Driving Add Tag >> button').nth(1).click();
|
||||
|
||||
await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Science");
|
||||
await expect(page.locator('[aria-label="Notebook Entry"]')).not.toContainText("Driving");
|
||||
|
||||
// Fill [aria-label="OpenMCT Search"] input[type="search"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc');
|
||||
await expect(page.locator('[aria-label="Search Result"]')).not.toContainText("Driving");
|
||||
});
|
||||
test('Tags persist across reload', async ({ page }) => {
|
||||
//Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
// Create a clock object we can navigate to
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
// Click Clock
|
||||
await page.click('text=Clock');
|
||||
// Click button:has-text("OK")
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('[name="mctForm"] >> text=My Items').click(),
|
||||
page.locator('button:has-text("OK")').click()
|
||||
]);
|
||||
|
||||
await page.click('.c-disclosure-triangle');
|
||||
|
||||
const ITERATIONS = 4;
|
||||
await createNotebookEntryAndTags(page, ITERATIONS);
|
||||
|
||||
for (let iteration = 0; iteration < ITERATIONS; iteration++) {
|
||||
const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`;
|
||||
await expect(page.locator(entryLocator)).toContainText("Science");
|
||||
await expect(page.locator(entryLocator)).toContainText("Driving");
|
||||
}
|
||||
|
||||
// Click Unnamed Clock
|
||||
await page.click('text="Unnamed Clock"');
|
||||
|
||||
// Click Unnamed Notebook
|
||||
await page.click('text="Unnamed Notebook"');
|
||||
|
||||
for (let iteration = 0; iteration < ITERATIONS; iteration++) {
|
||||
const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`;
|
||||
await expect(page.locator(entryLocator)).toContainText("Science");
|
||||
await expect(page.locator(entryLocator)).toContainText("Driving");
|
||||
}
|
||||
|
||||
//Reload Page
|
||||
await Promise.all([
|
||||
page.reload(),
|
||||
page.waitForLoadState('networkidle')
|
||||
]);
|
||||
|
||||
// Click Unnamed Notebook
|
||||
await page.click('text="Unnamed Notebook"');
|
||||
|
||||
for (let iteration = 0; iteration < ITERATIONS; iteration++) {
|
||||
const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`;
|
||||
await expect(page.locator(entryLocator)).toContainText("Science");
|
||||
await expect(page.locator(entryLocator)).toContainText("Driving");
|
||||
}
|
||||
|
||||
});
|
||||
});
|
185
e2e/tests/plugins/plot/autoscale.e2e.spec.js
Normal file
185
e2e/tests/plugins/plot/autoscale.e2e.spec.js
Normal file
@ -0,0 +1,185 @@
|
||||
/*****************************************************************************
|
||||
* 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 } = require('../../../fixtures.js');
|
||||
const { expect } = require('@playwright/test');
|
||||
|
||||
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.
|
||||
test.slow();
|
||||
|
||||
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);
|
||||
|
||||
// Make sure that after turning off autoscale, the user selected range values start at the same values the plot had prior.
|
||||
await testYTicks(page, ['-1.00', '-0.50', '0.00', '0.50', '1.00']);
|
||||
|
||||
const canvas = page.locator('canvas').nth(1);
|
||||
|
||||
await canvas.hover({trial: true});
|
||||
|
||||
expect(await canvas.screenshot()).toMatchSnapshot('autoscale-canvas-prepan');
|
||||
|
||||
//Alt Drag Start
|
||||
await page.keyboard.down('Alt');
|
||||
|
||||
await canvas.dragTo(canvas, {
|
||||
sourcePosition: {
|
||||
x: 200,
|
||||
y: 200
|
||||
},
|
||||
targetPosition: {
|
||||
x: 400,
|
||||
y: 400
|
||||
}
|
||||
});
|
||||
|
||||
//Alt Drag End
|
||||
await page.keyboard.up('Alt');
|
||||
|
||||
// Ensure the drag worked.
|
||||
await testYTicks(page, ['0.00', '0.50', '1.00', '1.50', '2.00']);
|
||||
|
||||
await canvas.hover({trial: true});
|
||||
|
||||
expect(await canvas.screenshot()).toMatchSnapshot('autoscale-canvas-panned');
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @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.
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
298
e2e/tests/plugins/plot/logPlot.e2e.spec.js
Normal file
298
e2e/tests/plugins/plot/logPlot.e2e.spec.js
Normal file
@ -0,0 +1,298 @@
|
||||
/*****************************************************************************
|
||||
* 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 }) => {
|
||||
//Test.slow decorator is currently broken. Needs to be fixed in https://github.com/nasa/openmct/issues/5374
|
||||
test.slow();
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
// 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) .c-form-row__controls .form-control .field input').click();
|
||||
await page.locator('div:nth-child(5) .c-form-row__controls .form-control .field input').fill('6');
|
||||
|
||||
await page.locator('div:nth-child(6) .c-form-row__controls .form-control .field input').click();
|
||||
await page.locator('div:nth-child(6) .c-form-row__controls .form-control .field input').fill('4');
|
||||
|
||||
await page.locator('div:nth-child(7) .c-form-row__controls .form-control .field input').click();
|
||||
await page.locator('div:nth-child(7) .c-form-row__controls .form-control .field input').fill('2');
|
||||
|
||||
// Click OK to make generator
|
||||
|
||||
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);
|
||||
}
|
155
e2e/tests/plugins/plot/missingPlotObj.e2e.spec.js
Normal file
155
e2e/tests/plugins/plot/missingPlotObj.e2e.spec.js
Normal file
@ -0,0 +1,155 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
Tests to verify log plot functionality when objects are missing
|
||||
*/
|
||||
|
||||
const { test } = require('../../../fixtures.js');
|
||||
const { expect } = require('@playwright/test');
|
||||
|
||||
test.describe('Handle missing object for plots', () => {
|
||||
test('Displays empty div for missing stacked plot item', async ({ page, browserName }) => {
|
||||
test.fixme(browserName === 'firefox', 'Firefox failing due to console events being missed');
|
||||
const errorLogs = [];
|
||||
|
||||
page.on("console", (message) => {
|
||||
if (message.type() === 'warning' && message.text().includes('Missing domain object')) {
|
||||
errorLogs.push(message.text());
|
||||
}
|
||||
});
|
||||
|
||||
//Make stacked plot
|
||||
await makeStackedPlot(page);
|
||||
|
||||
//Gets local storage and deletes the last sine wave generator in the stacked plot
|
||||
const localStorage = await page.evaluate(() => window.localStorage);
|
||||
const parsedData = JSON.parse(localStorage.mct);
|
||||
const keys = Object.keys(parsedData);
|
||||
const lastKey = keys[keys.length - 1];
|
||||
|
||||
delete parsedData[lastKey];
|
||||
|
||||
//Sets local storage with missing object
|
||||
await page.evaluate(
|
||||
`window.localStorage.setItem('mct', '${JSON.stringify(parsedData)}')`
|
||||
);
|
||||
|
||||
//Reloads page and clicks on stacked plot
|
||||
await Promise.all([
|
||||
page.reload(),
|
||||
page.waitForLoadState('networkidle')
|
||||
]);
|
||||
|
||||
//Verify Main section is there on load
|
||||
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Stacked Plot');
|
||||
|
||||
await page.locator('text=Open MCT My Items >> span').nth(3).click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=Unnamed Stacked Plot').first().click()
|
||||
]);
|
||||
|
||||
//Check that there is only one stacked item plot with a plot, the missing one will be empty
|
||||
await expect(page.locator(".c-plot--stacked-container:has(.gl-plot)")).toHaveCount(1);
|
||||
//Verify that console.warn is thrown
|
||||
expect(errorLogs).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* This is used the create a stacked plot object
|
||||
* @private
|
||||
*/
|
||||
async function makeStackedPlot(page) {
|
||||
// fresh page with time range from 2022-03-29 22:00:00.000Z to 2022-03-29 22:00:30.000Z
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
// create stacked plot
|
||||
await page.locator('button.c-create-button').click();
|
||||
await page.locator('li:has-text("Stacked Plot")').click();
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation({ waitUntil: 'networkidle'}),
|
||||
page.locator('text=OK').click(),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
|
||||
// save the stacked plot
|
||||
await saveStackedPlot(page);
|
||||
|
||||
// create a sinewave generator
|
||||
await createSineWaveGenerator(page);
|
||||
|
||||
// click on stacked plot
|
||||
await page.locator('text=Open MCT My Items >> span').nth(3).click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=Unnamed Stacked Plot').first().click()
|
||||
]);
|
||||
|
||||
// create a second sinewave generator
|
||||
await createSineWaveGenerator(page);
|
||||
|
||||
// click on stacked plot
|
||||
await page.locator('text=Open MCT My Items >> span').nth(3).click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=Unnamed Stacked Plot').first().click()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is used to save a stacked plot object
|
||||
* @private
|
||||
*/
|
||||
async function saveStackedPlot(page) {
|
||||
// save stacked plot
|
||||
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
|
||||
|
||||
await Promise.all([
|
||||
page.locator('text=Save and Finish Editing').click(),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
//Wait until Save Banner is gone
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached' });
|
||||
}
|
||||
|
||||
/**
|
||||
* This is used to create a sine wave generator object
|
||||
* @private
|
||||
*/
|
||||
async function createSineWaveGenerator(page) {
|
||||
//Create sine wave generator
|
||||
await page.locator('button.c-create-button').click();
|
||||
await page.locator('li:has-text("Sine Wave Generator")').click();
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation({ waitUntil: 'networkidle'}),
|
||||
page.locator('text=OK').click(),
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
}
|
41
e2e/tests/plugins/remoteClock/remoteClock.e2e.spec.js
Normal file
41
e2e/tests/plugins/remoteClock/remoteClock.e2e.spec.js
Normal file
@ -0,0 +1,41 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
const { test } = require('../../../fixtures.js');
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { expect } = require('@playwright/test');
|
||||
|
||||
test.describe('Remote Clock', () => {
|
||||
// eslint-disable-next-line require-await
|
||||
test.fixme('blocks historical requests until first tick is received', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/5221'
|
||||
});
|
||||
// addInitScript to with remote clock
|
||||
// Switch time conductor mode to 'remote clock'
|
||||
// Navigate to telemetry
|
||||
// Verify that the plot renders historical data within the correct bounds
|
||||
// Refresh the page
|
||||
// Verify again that the plot renders historical data within the correct bounds
|
||||
});
|
||||
});
|
104
e2e/tests/plugins/telemetryTable/telemetryTable.e2e.spec.js
Normal file
104
e2e/tests/plugins/telemetryTable/telemetryTable.e2e.spec.js
Normal file
@ -0,0 +1,104 @@
|
||||
/*****************************************************************************
|
||||
* 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 and filters data when paused by button and user changes bounds', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/5113'
|
||||
});
|
||||
|
||||
const bannerMessage = '.c-message-banner__message';
|
||||
const createButton = 'button:has-text("Create")';
|
||||
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
// Click create button
|
||||
await page.locator(createButton).click();
|
||||
await page.locator('li:has-text("Telemetry Table")').click();
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=OK').click(),
|
||||
// Wait for Save Banner to appear
|
||||
page.waitForSelector(bannerMessage)
|
||||
]);
|
||||
|
||||
// Save (exit edit mode)
|
||||
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(3).click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
// Click create button
|
||||
await page.locator(createButton).click();
|
||||
|
||||
// add Sine Wave Generator with defaults
|
||||
await page.locator('li:has-text("Sine Wave Generator")').click();
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=OK').click(),
|
||||
// Wait for Save Banner to appear
|
||||
page.waitForSelector(bannerMessage)
|
||||
]);
|
||||
|
||||
// focus the Telemetry Table
|
||||
await page.locator('text=Open MCT My Items >> span').nth(3).click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=Unnamed Telemetry Table').first().click()
|
||||
]);
|
||||
|
||||
// Click pause button
|
||||
const pauseButton = page.locator('button.c-button.icon-pause');
|
||||
await pauseButton.click();
|
||||
|
||||
const tableWrapper = page.locator('div.c-table-wrapper');
|
||||
await expect(tableWrapper).toHaveClass(/is-paused/);
|
||||
|
||||
// Subtract 5 minutes from the current end bound datetime and set it
|
||||
const endTimeInput = page.locator('input[type="text"].c-input--datetime').nth(1);
|
||||
await endTimeInput.click();
|
||||
|
||||
let endDate = await endTimeInput.inputValue();
|
||||
endDate = new Date(endDate);
|
||||
|
||||
endDate.setUTCMinutes(endDate.getUTCMinutes() - 5);
|
||||
endDate = endDate.toISOString().replace(/T/, ' ');
|
||||
|
||||
await endTimeInput.fill('');
|
||||
await endTimeInput.fill(endDate);
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
await expect(tableWrapper).not.toHaveClass(/is-paused/);
|
||||
|
||||
// Get the most recent telemetry date
|
||||
const latestTelemetryDate = await page.locator('table.c-telemetry-table__body > tbody > tr').last().locator('td').nth(1).getAttribute('title');
|
||||
|
||||
// Verify that it is <= our new end bound
|
||||
const latestMilliseconds = Date.parse(latestTelemetryDate);
|
||||
const endBoundMilliseconds = Date.parse(endDate);
|
||||
expect(latestMilliseconds).toBeLessThanOrEqual(endBoundMilliseconds);
|
||||
});
|
||||
});
|
@ -20,11 +20,12 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const { test } = require('../../../fixtures.js');
|
||||
const { expect } = require('@playwright/test');
|
||||
|
||||
test.describe('Time counductor operations', () => {
|
||||
test.describe('Time conductor operations', () => {
|
||||
test('validate start time does not exceeds end time', async ({ page }) => {
|
||||
//Go to baseURL
|
||||
// Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
const year = new Date().getFullYear();
|
||||
|
||||
@ -67,3 +68,168 @@ test.describe('Time counductor operations', () => {
|
||||
expect(endDateValidityStatus).not.toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
// Testing instructions:
|
||||
// Try to change the realtime offsets when in realtime (local clock) mode.
|
||||
test.describe('Time conductor input fields real-time mode', () => {
|
||||
test('validate input fields in real-time mode', async ({ page }) => {
|
||||
const startOffset = {
|
||||
secs: '23'
|
||||
};
|
||||
|
||||
const endOffset = {
|
||||
secs: '31'
|
||||
};
|
||||
|
||||
// Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
// Switch to real-time mode
|
||||
await setRealTimeMode(page);
|
||||
|
||||
// Set start time offset
|
||||
await setStartOffset(page, startOffset);
|
||||
|
||||
// Verify time was updated on time offset button
|
||||
await expect(page.locator('data-testid=conductor-start-offset-button')).toContainText('00:30:23');
|
||||
|
||||
// Set end time offset
|
||||
await setEndOffset(page, endOffset);
|
||||
|
||||
// Verify time was updated on preceding time offset button
|
||||
await expect(page.locator('data-testid=conductor-end-offset-button')).toContainText('00:00:31');
|
||||
});
|
||||
|
||||
/**
|
||||
* Verify that offsets and url params are preserved when switching
|
||||
* between fixed timespan and real-time mode.
|
||||
*/
|
||||
test('preserve offsets and url params when switching between fixed and real-time mode', async ({ page }) => {
|
||||
const startOffset = {
|
||||
mins: '30',
|
||||
secs: '23'
|
||||
};
|
||||
|
||||
const endOffset = {
|
||||
secs: '01'
|
||||
};
|
||||
|
||||
// Convert offsets to milliseconds
|
||||
const startDelta = (30 * 60 * 1000) + (23 * 1000);
|
||||
const endDelta = (1 * 1000);
|
||||
|
||||
// Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
// Switch to real-time mode
|
||||
await setRealTimeMode(page);
|
||||
|
||||
// Set start time offset
|
||||
await setStartOffset(page, startOffset);
|
||||
|
||||
// Set end time offset
|
||||
await setEndOffset(page, endOffset);
|
||||
|
||||
// Switch to fixed timespan mode
|
||||
await setFixedTimeMode(page);
|
||||
|
||||
// Switch back to real-time mode
|
||||
await setRealTimeMode(page);
|
||||
|
||||
// Verify updated start time offset persists after mode switch
|
||||
await expect(page.locator('data-testid=conductor-start-offset-button')).toContainText('00:30:23');
|
||||
|
||||
// Verify updated end time offset persists after mode switch
|
||||
await expect(page.locator('data-testid=conductor-end-offset-button')).toContainText('00:00:01');
|
||||
|
||||
// Verify url parameters persist after mode switch
|
||||
await page.waitForNavigation();
|
||||
expect(page.url()).toContain(`startDelta=${startDelta}`);
|
||||
expect(page.url()).toContain(`endDelta=${endDelta}`);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @typedef {Object} OffsetValues
|
||||
* @property {string | undefined} hours
|
||||
* @property {string | undefined} mins
|
||||
* @property {string | undefined} secs
|
||||
*/
|
||||
|
||||
/**
|
||||
* Set the values (hours, mins, secs) for the start time offset when in realtime mode
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {OffsetValues} offset
|
||||
*/
|
||||
async function setStartOffset(page, offset) {
|
||||
const startOffsetButton = page.locator('data-testid=conductor-start-offset-button');
|
||||
await setTimeConductorOffset(page, offset, startOffsetButton);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the values (hours, mins, secs) for the end time offset when in realtime mode
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {OffsetValues} offset
|
||||
*/
|
||||
async function setEndOffset(page, offset) {
|
||||
const endOffsetButton = page.locator('data-testid=conductor-end-offset-button');
|
||||
await setTimeConductorOffset(page, offset, endOffsetButton);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the time conductor to fixed timespan mode
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function setFixedTimeMode(page) {
|
||||
await setTimeConductorMode(page, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the time conductor to realtime mode
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function setRealTimeMode(page) {
|
||||
await setTimeConductorMode(page, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the values (hours, mins, secs) for the TimeConductor offsets when in realtime mode
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {OffsetValues} offset
|
||||
* @param {import('@playwright/test').Locator} offsetButton
|
||||
*/
|
||||
async function setTimeConductorOffset(page, {hours, mins, secs}, offsetButton) {
|
||||
await offsetButton.click();
|
||||
|
||||
if (hours) {
|
||||
await page.fill('.pr-time-controls__hrs', hours);
|
||||
}
|
||||
|
||||
if (mins) {
|
||||
await page.fill('.pr-time-controls__mins', mins);
|
||||
}
|
||||
|
||||
if (secs) {
|
||||
await page.fill('.pr-time-controls__secs', secs);
|
||||
}
|
||||
|
||||
// Click the check button
|
||||
await page.locator('.icon-check').click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the time conductor mode to either fixed timespan or realtime mode.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {boolean} [isFixedTimespan=true] true for fixed timespan mode, false for realtime mode; default is true
|
||||
*/
|
||||
async function setTimeConductorMode(page, isFixedTimespan = true) {
|
||||
// Click 'mode' button
|
||||
await page.locator('.c-mode-button').click();
|
||||
|
||||
// Switch time conductor mode
|
||||
if (isFixedTimespan) {
|
||||
await page.locator('data-testid=conductor-modeOption-fixed').click();
|
||||
} else {
|
||||
await page.locator('data-testid=conductor-modeOption-realtime').click();
|
||||
}
|
||||
}
|
||||
|
185
e2e/tests/plugins/timer/timer.e2e.spec.js
Normal file
185
e2e/tests/plugins/timer/timer.e2e.spec.js
Normal file
@ -0,0 +1,185 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
const { test } = require('../../../fixtures.js');
|
||||
const { expect } = require('@playwright/test');
|
||||
|
||||
test.describe('Timer', () => {
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
//Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
//Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
// Click 'Timer'
|
||||
await page.click('text=Timer');
|
||||
|
||||
// Click text=OK
|
||||
await Promise.all([
|
||||
page.waitForNavigation({waitUntil: 'networkidle'}),
|
||||
page.click('text=OK')
|
||||
]);
|
||||
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Timer');
|
||||
});
|
||||
|
||||
test('Can perform actions on the Timer', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/4313'
|
||||
});
|
||||
|
||||
await test.step("From the tree context menu", async () => {
|
||||
await triggerTimerContextMenuAction(page, 'Start');
|
||||
await triggerTimerContextMenuAction(page, 'Pause');
|
||||
await triggerTimerContextMenuAction(page, 'Restart at 0');
|
||||
await triggerTimerContextMenuAction(page, 'Stop');
|
||||
});
|
||||
|
||||
await test.step("From the 3dot menu", async () => {
|
||||
await triggerTimer3dotMenuAction(page, 'Start');
|
||||
await triggerTimer3dotMenuAction(page, 'Pause');
|
||||
await triggerTimer3dotMenuAction(page, 'Restart at 0');
|
||||
await triggerTimer3dotMenuAction(page, 'Stop');
|
||||
});
|
||||
|
||||
await test.step("From the object view", async () => {
|
||||
await triggerTimerViewAction(page, 'Start');
|
||||
await triggerTimerViewAction(page, 'Pause');
|
||||
await triggerTimerViewAction(page, 'Restart at 0');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Actions that can be performed on a timer from context menus.
|
||||
* @typedef {'Start' | 'Stop' | 'Pause' | 'Restart at 0'} TimerAction
|
||||
*/
|
||||
|
||||
/**
|
||||
* Actions that can be performed on a timer from the object view.
|
||||
* @typedef {'Start' | 'Pause' | 'Restart at 0'} TimerViewAction
|
||||
*/
|
||||
|
||||
/**
|
||||
* Open the timer context menu from the object tree.
|
||||
* Expands the 'My Items' folder if it is not already expanded.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function openTimerContextMenu(page) {
|
||||
const myItemsFolder = page.locator('text=Open MCT My Items >> span').nth(3);
|
||||
const className = await myItemsFolder.getAttribute('class');
|
||||
if (!className.includes('c-disclosure-triangle--expanded')) {
|
||||
await myItemsFolder.click();
|
||||
}
|
||||
|
||||
await page.locator(`a:has-text("Unnamed Timer")`).click({
|
||||
button: 'right'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a timer action from the tree context menu
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {TimerAction} action
|
||||
*/
|
||||
async function triggerTimerContextMenuAction(page, action) {
|
||||
const menuAction = `.c-menu ul li >> text="${action}"`;
|
||||
await openTimerContextMenu(page);
|
||||
await page.locator(menuAction).click();
|
||||
assertTimerStateAfterAction(page, action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a timer action from the 3dot menu
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {TimerAction} action
|
||||
*/
|
||||
async function triggerTimer3dotMenuAction(page, action) {
|
||||
const menuAction = `.c-menu ul li >> text="${action}"`;
|
||||
const threeDotMenuButton = 'button[title="More options"]';
|
||||
let isActionAvailable = false;
|
||||
let iterations = 0;
|
||||
// Dismiss/open the 3dot menu until the action is available
|
||||
// or a maxiumum number of iterations is reached
|
||||
while (!isActionAvailable && iterations <= 20) {
|
||||
await page.click('.c-object-view');
|
||||
await page.click(threeDotMenuButton);
|
||||
isActionAvailable = await page.locator(menuAction).isVisible();
|
||||
iterations++;
|
||||
}
|
||||
|
||||
await page.locator(menuAction).click();
|
||||
assertTimerStateAfterAction(page, action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a timer action from the object view
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {TimerViewAction} action
|
||||
*/
|
||||
async function triggerTimerViewAction(page, action) {
|
||||
await page.locator('.c-timer').hover({trial: true});
|
||||
const buttonTitle = buttonTitleFromAction(action);
|
||||
await page.click(`button[title="${buttonTitle}"]`);
|
||||
assertTimerStateAfterAction(page, action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes in a TimerViewAction and returns the button title
|
||||
* @param {TimerViewAction} action
|
||||
*/
|
||||
function buttonTitleFromAction(action) {
|
||||
switch (action) {
|
||||
case 'Start':
|
||||
return 'Start';
|
||||
case 'Pause':
|
||||
return 'Pause';
|
||||
case 'Restart at 0':
|
||||
return 'Reset';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the timer state after a timer action has been performed.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
* @param {TimerAction} action
|
||||
*/
|
||||
async function assertTimerStateAfterAction(page, action) {
|
||||
let timerStateClass;
|
||||
switch (action) {
|
||||
case 'Start':
|
||||
case 'Restart at 0':
|
||||
timerStateClass = "is-started";
|
||||
break;
|
||||
case 'Stop':
|
||||
timerStateClass = 'is-stopped';
|
||||
break;
|
||||
case 'Pause':
|
||||
timerStateClass = 'is-paused';
|
||||
break;
|
||||
}
|
||||
|
||||
await expect.soft(page.locator('.c-timer')).toHaveClass(new RegExp(timerStateClass));
|
||||
}
|
@ -33,7 +33,8 @@ comfortable running this test during a live mission?" Avoid creating or deleting
|
||||
Make no assumptions about the order that elements appear in the DOM.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const { test } = require('../fixtures.js');
|
||||
const { expect } = require('@playwright/test');
|
||||
|
||||
test('Verify that the create button appears and that the Folder Domain Object is available for selection', async ({ page }) => {
|
||||
|
||||
@ -44,6 +45,15 @@ test('Verify that the create button appears and that the Folder Domain Object is
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
// Verify that Create Folder appears in the dropdown
|
||||
const locator = page.locator(':nth-match(:text("Folder"), 2)');
|
||||
await expect(locator).toBeEnabled();
|
||||
await expect(page.locator(':nth-match(:text("Folder"), 2)')).toBeEnabled();
|
||||
});
|
||||
|
||||
test('Verify that My Items Tree appears @ipad', async ({ page }) => {
|
||||
//Test.slow annotation is currently broken. Needs to be fixed in https://github.com/nasa/openmct/issues/5374
|
||||
test.slow();
|
||||
//Go to baseURL
|
||||
await page.goto('/');
|
||||
|
||||
//My Items to be visible
|
||||
await expect(page.locator('a:has-text("My Items")')).toBeEnabled();
|
||||
});
|
111
e2e/tests/ui/layout/search/grandsearch.e2e.spec.js
Normal file
111
e2e/tests/ui/layout/search/grandsearch.e2e.spec.js
Normal file
@ -0,0 +1,111 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
This test suite is dedicated to tests which verify search functionality.
|
||||
*/
|
||||
|
||||
const { expect } = require('@playwright/test');
|
||||
const { test } = require('../../../../fixtures');
|
||||
|
||||
/**
|
||||
* Creates a notebook object and adds an entry.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function createClockAndDisplayLayout(page) {
|
||||
//Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
// Click button:has-text("Create")
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
// Click li:has-text("Notebook")
|
||||
await page.locator('li:has-text("Clock")').click();
|
||||
// Click button:has-text("OK")
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('button:has-text("OK")').click()
|
||||
]);
|
||||
|
||||
// Click a:has-text("My Items")
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('a:has-text("My Items") >> nth=0').click()
|
||||
]);
|
||||
// Click button:has-text("Create")
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
// Click li:has-text("Notebook")
|
||||
await page.locator('li:has-text("Display Layout")').click();
|
||||
// Click button:has-text("OK")
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('button:has-text("OK")').click()
|
||||
]);
|
||||
}
|
||||
|
||||
test.describe('Grand Search', () => {
|
||||
test('Can search for objects, and subsequent search dropdown behaves properly', async ({ page }) => {
|
||||
await createClockAndDisplayLayout(page);
|
||||
|
||||
// Click [aria-label="OpenMCT Search"] input[type="search"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
|
||||
// Fill [aria-label="OpenMCT Search"] input[type="search"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Cl');
|
||||
await expect(page.locator('[aria-label="Search Result"]')).toContainText('Clock');
|
||||
// Click text=Elements >> nth=0
|
||||
await page.locator('text=Elements').first().click();
|
||||
await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible();
|
||||
|
||||
// Click [aria-label="OpenMCT Search"] [aria-label="Search Input"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click();
|
||||
// Click [aria-label="Unnamed Clock clock result"] >> text=Unnamed Clock
|
||||
await page.locator('[aria-label="Unnamed Clock clock result"] >> text=Unnamed Clock').click();
|
||||
await expect(page.locator('.js-preview-window')).toBeVisible();
|
||||
|
||||
// Click [aria-label="Close"]
|
||||
await page.locator('[aria-label="Close"]').click();
|
||||
await expect(page.locator('[aria-label="Search Result"]')).toBeVisible();
|
||||
await expect(page.locator('[aria-label="Search Result"]')).toContainText('Cloc');
|
||||
|
||||
// Click [aria-label="OpenMCT Search"] a >> nth=0
|
||||
await page.locator('[aria-label="OpenMCT Search"] a').first().click();
|
||||
await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible();
|
||||
|
||||
// Fill [aria-label="OpenMCT Search"] input[type="search"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('foo');
|
||||
await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible();
|
||||
|
||||
// Click text=Snapshot Save and Finish Editing Save and Continue Editing >> button >> nth=1
|
||||
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
|
||||
// Click text=Save and Finish Editing
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
// Click [aria-label="OpenMCT Search"] [aria-label="Search Input"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click();
|
||||
// Fill [aria-label="OpenMCT Search"] [aria-label="Search Input"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').fill('Cl');
|
||||
// Click text=Unnamed Clock
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=Unnamed Clock').click()
|
||||
]);
|
||||
await expect(page.locator('.is-object-type-clock')).toBeVisible();
|
||||
});
|
||||
});
|
76
e2e/tests/visual/addInit.visual.spec.js
Normal file
76
e2e/tests/visual/addInit.visual.spec.js
Normal file
@ -0,0 +1,76 @@
|
||||
/* eslint-disable no-undef */
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
Collection of Visual Tests set to run with modified init scripts to inject plugins not otherwise available in the default contexts.
|
||||
|
||||
These should only use functional expect statements to verify assumptions about the state
|
||||
in a test and not for functional verification of correctness. Visual tests are not supposed
|
||||
to "fail" on assertions. Instead, they should be used to detect changes between builds or branches.
|
||||
|
||||
Note: Larger testsuite sizes are OK due to the setup time associated with these tests.
|
||||
*/
|
||||
|
||||
const { test } = require('@playwright/test');
|
||||
const percySnapshot = require('@percy/playwright');
|
||||
const path = require('path');
|
||||
const sinon = require('sinon');
|
||||
|
||||
const VISUAL_GRACE_PERIOD = 5 * 1000; //Lets the application "simmer" before the snapshot is taken
|
||||
|
||||
const CUSTOM_NAME = 'CUSTOM_NAME';
|
||||
|
||||
// Snippet from https://github.com/microsoft/playwright/issues/6347#issuecomment-965887758
|
||||
// Will replace with cy.clock() equivalent
|
||||
test.beforeEach(async ({ context }) => {
|
||||
await context.addInitScript({
|
||||
path: path.join(__dirname, '../../..', './node_modules/sinon/pkg/sinon.js')
|
||||
});
|
||||
await context.addInitScript(() => {
|
||||
window.__clock = sinon.useFakeTimers({
|
||||
now: 0,
|
||||
shouldAdvanceTime: true
|
||||
}); //Set browser clock to UNIX Epoch
|
||||
});
|
||||
});
|
||||
|
||||
test('Visual - Restricted Notebook is visually correct @addInit', async ({ page }) => {
|
||||
// eslint-disable-next-line no-undef
|
||||
await page.addInitScript({ path: path.join(__dirname, '../plugins/notebook', './addInitRestrictedNotebook.js') });
|
||||
//Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
//Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
// Click text=CUSTOM_NAME
|
||||
await page.click(`text=${CUSTOM_NAME}`);
|
||||
// Click text=OK
|
||||
await Promise.all([
|
||||
page.waitForNavigation({waitUntil: 'networkidle'}),
|
||||
page.click('text=OK')
|
||||
]);
|
||||
|
||||
// Take a snapshot of the newly created CUSTOM_NAME notebook
|
||||
await page.waitForTimeout(VISUAL_GRACE_PERIOD);
|
||||
await percySnapshot(page, 'Restricted Notebook with CUSTOM_NAME');
|
||||
|
||||
});
|
70
e2e/tests/visual/controlledClock.visual.spec.js
Normal file
70
e2e/tests/visual/controlledClock.visual.spec.js
Normal file
@ -0,0 +1,70 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
Collection of Visual Tests set to run in a default context. The tests within this suite
|
||||
are only meant to run against openmct's app.js started by `npm run start` within the
|
||||
`./e2e/playwright-visual.config.js` file.
|
||||
|
||||
These should only use functional expect statements to verify assumptions about the state
|
||||
in a test and not for functional verification of correctness. Visual tests are not supposed
|
||||
to "fail" on assertions. Instead, they should be used to detect changes between builds or branches.
|
||||
|
||||
Note: Larger testsuite sizes are OK due to the setup time associated with these tests.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const percySnapshot = require('@percy/playwright');
|
||||
const path = require('path');
|
||||
const sinon = require('sinon');
|
||||
|
||||
// Snippet from https://github.com/microsoft/playwright/issues/6347#issuecomment-965887758
|
||||
// Will replace with cy.clock() equivalent
|
||||
test.beforeEach(async ({ context }) => {
|
||||
await context.addInitScript({
|
||||
// eslint-disable-next-line no-undef
|
||||
path: path.join(__dirname, '../../..', './node_modules/sinon/pkg/sinon.js')
|
||||
});
|
||||
await context.addInitScript(() => {
|
||||
window.__clock = sinon.useFakeTimers({
|
||||
now: 0, //Set browser clock to UNIX Epoch
|
||||
shouldAdvanceTime: false, //Don't advance the clock
|
||||
toFake: ["setTimeout", "nextTick"]
|
||||
});
|
||||
});
|
||||
});
|
||||
test.use({ storageState: './e2e/test-data/VisualTestData_storage.json' });
|
||||
|
||||
test('Visual - Overlay Plot Loading Indicator @localstorage', async ({ page }) => {
|
||||
// Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
await page.locator('a:has-text("Unnamed Overlay Plot Overlay Plot")').click();
|
||||
//Ensure that we're on the Unnamed Overlay Plot object
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Overlay Plot');
|
||||
|
||||
//Wait for canvas to be rendered and stop animating
|
||||
await page.locator('canvas >> nth=1').hover({trial: true});
|
||||
|
||||
//Take snapshot of Sine Wave Generator within Overlay Plot
|
||||
await percySnapshot(page, 'SineWaveInOverlayPlot');
|
||||
});
|
@ -32,7 +32,8 @@ to "fail" on assertions. Instead, they should be used to detect changes between
|
||||
Note: Larger testsuite sizes are OK due to the setup time associated with these tests.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const { test } = require('../../fixtures.js');
|
||||
const { expect } = require('@playwright/test');
|
||||
const percySnapshot = require('@percy/playwright');
|
||||
const path = require('path');
|
||||
const sinon = require('sinon');
|
||||
@ -47,7 +48,10 @@ test.beforeEach(async ({ context }) => {
|
||||
path: path.join(__dirname, '../../..', './node_modules/sinon/pkg/sinon.js')
|
||||
});
|
||||
await context.addInitScript(() => {
|
||||
window.__clock = sinon.useFakeTimers(); //Set browser clock to UNIX Epoch
|
||||
window.__clock = sinon.useFakeTimers({
|
||||
now: 0,
|
||||
shouldAdvanceTime: true
|
||||
}); //Set browser clock to UNIX Epoch
|
||||
});
|
||||
});
|
||||
|
||||
@ -56,8 +60,7 @@ test('Visual - Root and About', async ({ page }) => {
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
// Verify that Create button is actionable
|
||||
const createButtonLocator = page.locator('button:has-text("Create")');
|
||||
await expect(createButtonLocator).toBeEnabled();
|
||||
await expect(page.locator('button:has-text("Create")')).toBeEnabled();
|
||||
|
||||
// Take a snapshot of the Dashboard
|
||||
await page.waitForTimeout(VISUAL_GRACE_PERIOD);
|
||||
@ -94,7 +97,11 @@ test('Visual - Default Condition Set', async ({ page }) => {
|
||||
await percySnapshot(page, 'Default Condition Set');
|
||||
});
|
||||
|
||||
test('Visual - Default Condition Widget', async ({ page }) => {
|
||||
test.fixme('Visual - Default Condition Widget', async ({ page }) => {
|
||||
test.info().annotations.push({
|
||||
type: 'issue',
|
||||
description: 'https://github.com/nasa/openmct/issues/5349'
|
||||
});
|
||||
//Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
@ -171,3 +178,55 @@ test('Visual - Sine Wave Generator Form', async ({ page }) => {
|
||||
await page.waitForTimeout(VISUAL_GRACE_PERIOD);
|
||||
await percySnapshot(page, 'removed amplitude property value');
|
||||
});
|
||||
|
||||
test('Visual - Save Successful Banner', async ({ page }) => {
|
||||
//Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
//Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
//NOTE Something other than example imagery
|
||||
await page.click('text=Timer');
|
||||
|
||||
// Click text=OK
|
||||
await page.click('text=OK');
|
||||
await page.locator('.c-message-banner__message').hover({ trial: true });
|
||||
await percySnapshot(page, 'Banner message shown');
|
||||
|
||||
//Wait until Save Banner is gone
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
||||
await percySnapshot(page, 'Banner message gone');
|
||||
});
|
||||
|
||||
test('Visual - Display Layout Icon is correct', async ({ page }) => {
|
||||
//Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
//Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
//Hover on Display Layout option.
|
||||
await page.locator('text=Display Layout').hover();
|
||||
await percySnapshot(page, 'Display Layout Create Menu');
|
||||
|
||||
});
|
||||
|
||||
test('Visual - Default Gauge is correct', async ({ page }) => {
|
||||
|
||||
//Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
//Click the Create button
|
||||
await page.click('button:has-text("Create")');
|
||||
|
||||
await page.click('text=Gauge');
|
||||
|
||||
await page.click('text=OK');
|
||||
|
||||
// Take a snapshot of the newly created Gauge object
|
||||
await page.waitForTimeout(VISUAL_GRACE_PERIOD);
|
||||
await percySnapshot(page, 'Default Gauge');
|
||||
|
||||
});
|
||||
|
86
e2e/tests/visual/generateVisualTestData.e2e.spec.js
Normal file
86
e2e/tests/visual/generateVisualTestData.e2e.spec.js
Normal file
@ -0,0 +1,86 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
This test suite is dedicated to generating LocalStorage via Session Storage to be used
|
||||
in some visual test suites like controlledClock.visual.spec.js. This suite should run to completion
|
||||
and generate an artifact named ./e2e/test-data/VisualTestData_storage.json . This will run
|
||||
on every Commit to ensure that this object still loads into tests correctly and will retain the
|
||||
.e2e.spec.js suffix.
|
||||
|
||||
TODO: Provide additional validation of object properties as it grows.
|
||||
|
||||
*/
|
||||
|
||||
const { test } = require('../../fixtures.js');
|
||||
const { expect } = require('@playwright/test');
|
||||
|
||||
test('Generate Visual Test Data @localStorage', async ({ page, context }) => {
|
||||
//Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
|
||||
// add overlay plot with defaults
|
||||
await page.locator('li:has-text("Overlay Plot")').click();
|
||||
|
||||
// Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184
|
||||
await page.click('form[name="mctForm"] a:has-text("My Items")');
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=OK').click(),
|
||||
//Wait for Save Banner to appear1
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
|
||||
// save (exit edit mode)
|
||||
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
|
||||
// click create button
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
|
||||
// add sine wave generator with defaults
|
||||
await page.locator('li:has-text("Sine Wave Generator")').click();
|
||||
|
||||
//Add a 5000 ms Delay
|
||||
await page.locator('[aria-label="Loading Delay \\(ms\\)"]').fill('5000');
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=OK').click(),
|
||||
//Wait for Save Banner to appear1
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
|
||||
// focus the overlay plot
|
||||
await page.locator('text=Open MCT My Items >> span').nth(3).click();
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=Unnamed Overlay Plot').first().click()
|
||||
]);
|
||||
|
||||
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Overlay Plot');
|
||||
//Save localStorage for future test execution
|
||||
await context.storageState({ path: './e2e/test-data/VisualTestData_storage.json' });
|
||||
});
|
104
e2e/tests/visual/search.visual.spec.js
Normal file
104
e2e/tests/visual/search.visual.spec.js
Normal file
@ -0,0 +1,104 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
/*
|
||||
This test suite is dedicated to tests which verify search functionality.
|
||||
*/
|
||||
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const percySnapshot = require('@percy/playwright');
|
||||
|
||||
/**
|
||||
* Creates a notebook object and adds an entry.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function createClockAndDisplayLayout(page) {
|
||||
//Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
|
||||
// Click button:has-text("Create")
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
// Click li:has-text("Notebook")
|
||||
await page.locator('li:has-text("Clock")').click();
|
||||
// Click button:has-text("OK")
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('button:has-text("OK")').click()
|
||||
]);
|
||||
|
||||
// Click a:has-text("My Items")
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('a:has-text("My Items") >> nth=0').click()
|
||||
]);
|
||||
// Click button:has-text("Create")
|
||||
await page.locator('button:has-text("Create")').click();
|
||||
// Click li:has-text("Notebook")
|
||||
await page.locator('li:has-text("Display Layout")').click();
|
||||
// Click button:has-text("OK")
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('button:has-text("OK")').click()
|
||||
]);
|
||||
}
|
||||
|
||||
test.describe('Grand Search', () => {
|
||||
test('Can search for objects, and subsequent search dropdown behaves properly', async ({ page }) => {
|
||||
await createClockAndDisplayLayout(page);
|
||||
|
||||
// Click [aria-label="OpenMCT Search"] input[type="search"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
|
||||
// Fill [aria-label="OpenMCT Search"] input[type="search"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Cl');
|
||||
await expect(page.locator('[aria-label="Search Result"]')).toContainText('Clock');
|
||||
await percySnapshot(page, 'Searching for Clocks');
|
||||
// Click text=Elements >> nth=0
|
||||
await page.locator('text=Elements').first().click();
|
||||
await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible();
|
||||
|
||||
// Click [aria-label="OpenMCT Search"] [aria-label="Search Input"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click();
|
||||
// Click [aria-label="Unnamed Clock clock result"] >> text=Unnamed Clock
|
||||
await page.locator('[aria-label="Unnamed Clock clock result"] >> text=Unnamed Clock').click();
|
||||
await percySnapshot(page, 'Preview for clock should display when editing enabled and search item clicked');
|
||||
|
||||
// Click [aria-label="Close"]
|
||||
await page.locator('[aria-label="Close"]').click();
|
||||
await percySnapshot(page, 'Search should still be showing after preview closed');
|
||||
|
||||
// Click text=Snapshot Save and Finish Editing Save and Continue Editing >> button >> nth=1
|
||||
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
|
||||
// Click text=Save and Finish Editing
|
||||
await page.locator('text=Save and Finish Editing').click();
|
||||
// Click [aria-label="OpenMCT Search"] [aria-label="Search Input"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click();
|
||||
// Fill [aria-label="OpenMCT Search"] [aria-label="Search Input"]
|
||||
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').fill('Cl');
|
||||
// Click text=Unnamed Clock
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=Unnamed Clock').click()
|
||||
]);
|
||||
await percySnapshot(page, 'Clicking on search results should navigate to them if not editing');
|
||||
|
||||
});
|
||||
});
|
33
example/exampleTags/plugin.js
Normal file
33
example/exampleTags/plugin.js
Normal file
@ -0,0 +1,33 @@
|
||||
/*****************************************************************************
|
||||
* 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);
|
||||
});
|
||||
};
|
||||
}
|
19
example/exampleTags/tags.json
Normal file
19
example/exampleTags/tags.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"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,19 +21,56 @@
|
||||
*****************************************************************************/
|
||||
|
||||
import EventEmitter from 'EventEmitter';
|
||||
import uuid from 'uuid';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import createExampleUser from './exampleUserCreator';
|
||||
|
||||
const STATUSES = [{
|
||||
key: "NO_STATUS",
|
||||
label: "Not set",
|
||||
iconClass: "icon-question-mark",
|
||||
iconClassPoll: "icon-status-poll-question-mark"
|
||||
}, {
|
||||
key: "GO",
|
||||
label: "Go",
|
||||
iconClass: "icon-check",
|
||||
iconClassPoll: "icon-status-poll-question-mark",
|
||||
statusClass: "s-status-ok",
|
||||
statusBgColor: "#33cc33",
|
||||
statusFgColor: "#000"
|
||||
}, {
|
||||
key: "MAYBE",
|
||||
label: "Maybe",
|
||||
iconClass: "icon-alert-triangle",
|
||||
iconClassPoll: "icon-status-poll-question-mark",
|
||||
statusClass: "s-status-warning",
|
||||
statusBgColor: "#ffb66c",
|
||||
statusFgColor: "#000"
|
||||
}, {
|
||||
key: "NO_GO",
|
||||
label: "No go",
|
||||
iconClass: "icon-circle-slash",
|
||||
iconClassPoll: "icon-status-poll-question-mark",
|
||||
statusClass: "s-status-error",
|
||||
statusBgColor: "#9900cc",
|
||||
statusFgColor: "#fff"
|
||||
}];
|
||||
/**
|
||||
* @implements {StatusUserProvider}
|
||||
*/
|
||||
export default class ExampleUserProvider extends EventEmitter {
|
||||
constructor(openmct) {
|
||||
constructor(openmct, {defaultStatusRole} = {defaultStatusRole: undefined}) {
|
||||
super();
|
||||
|
||||
this.openmct = openmct;
|
||||
this.user = undefined;
|
||||
this.loggedIn = false;
|
||||
this.autoLoginUser = undefined;
|
||||
this.status = STATUSES[1];
|
||||
this.pollQuestion = undefined;
|
||||
this.defaultStatusRole = defaultStatusRole;
|
||||
|
||||
this.ExampleUser = createExampleUser(this.openmct.user.User);
|
||||
this.loginPromise = undefined;
|
||||
}
|
||||
|
||||
isLoggedIn() {
|
||||
@ -45,11 +82,19 @@ export default class ExampleUserProvider extends EventEmitter {
|
||||
}
|
||||
|
||||
getCurrentUser() {
|
||||
if (this.loggedIn) {
|
||||
return Promise.resolve(this.user);
|
||||
if (!this.loginPromise) {
|
||||
this.loginPromise = this._login().then(() => this.user);
|
||||
}
|
||||
|
||||
return this._login().then(() => this.user);
|
||||
return this.loginPromise;
|
||||
}
|
||||
|
||||
canProvideStatusForRole() {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
canSetPollQuestion() {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
hasRole(roleId) {
|
||||
@ -60,6 +105,55 @@ export default class ExampleUserProvider extends EventEmitter {
|
||||
return Promise.resolve(this.user.getRoles().includes(roleId));
|
||||
}
|
||||
|
||||
getStatusRoleForCurrentUser() {
|
||||
return Promise.resolve(this.defaultStatusRole);
|
||||
}
|
||||
|
||||
getAllStatusRoles() {
|
||||
return Promise.resolve([this.defaultStatusRole]);
|
||||
}
|
||||
|
||||
getStatusForRole(role) {
|
||||
return Promise.resolve(this.status);
|
||||
}
|
||||
|
||||
async getDefaultStatusForRole(role) {
|
||||
const allRoles = await this.getPossibleStatuses();
|
||||
|
||||
return allRoles?.[0];
|
||||
}
|
||||
|
||||
setStatusForRole(role, status) {
|
||||
this.status = status;
|
||||
this.emit('statusChange', {
|
||||
role,
|
||||
status
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
getPollQuestion() {
|
||||
return Promise.resolve({
|
||||
question: 'Set "GO" if your position is ready for a boarding action on the Klingon cruiser',
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
setPollQuestion(pollQuestion) {
|
||||
this.pollQuestion = {
|
||||
question: pollQuestion,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
this.emit("pollQuestionChange", this.pollQuestion);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
getPossibleStatuses() {
|
||||
return Promise.resolve(STATUSES);
|
||||
}
|
||||
|
||||
_login() {
|
||||
const id = uuid();
|
||||
|
||||
@ -108,3 +202,6 @@ export default class ExampleUserProvider extends EventEmitter {
|
||||
);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @typedef {import('@/api/user/StatusUserProvider').default} StatusUserProvider
|
||||
*/
|
||||
|
@ -22,8 +22,19 @@
|
||||
|
||||
import ExampleUserProvider from './ExampleUserProvider';
|
||||
|
||||
export default function ExampleUserPlugin() {
|
||||
export default function ExampleUserPlugin({autoLoginUser, defaultStatusRole} = {
|
||||
autoLoginUser: 'guest',
|
||||
defaultStatusRole: 'test-role'
|
||||
}) {
|
||||
return function install(openmct) {
|
||||
openmct.user.setProvider(new ExampleUserProvider(openmct));
|
||||
const userProvider = new ExampleUserProvider(openmct, {
|
||||
defaultStatusRole
|
||||
});
|
||||
|
||||
if (autoLoginUser !== undefined) {
|
||||
userProvider.autoLogin(autoLoginUser);
|
||||
}
|
||||
|
||||
openmct.user.setProvider(userProvider);
|
||||
};
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ import {
|
||||
} from '../../src/utils/testing';
|
||||
import ExampleUserProvider from './ExampleUserProvider';
|
||||
|
||||
xdescribe("The Example User Plugin", () => {
|
||||
describe("The Example User Plugin", () => {
|
||||
let openmct;
|
||||
|
||||
beforeEach(() => {
|
||||
@ -47,9 +47,4 @@ xdescribe("The Example User Plugin", () => {
|
||||
});
|
||||
openmct.install(openmct.plugins.example.ExampleUser());
|
||||
});
|
||||
|
||||
// The rest of the functionality of the ExampleUser Plugin is
|
||||
// tested in both the UserAPISpec.js and in the UserIndicatorPlugin spec.
|
||||
// If that changes, those tests can be moved here.
|
||||
|
||||
});
|
||||
|
83
example/faultManagment/exampleFaultSource.js
Normal file
83
example/faultManagment/exampleFaultSource.js
Normal file
@ -0,0 +1,83 @@
|
||||
/*****************************************************************************
|
||||
* 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
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
47
example/faultManagment/pluginSpec.js
Normal file
47
example/faultManagment/pluginSpec.js
Normal file
@ -0,0 +1,47 @@
|
||||
/*****************************************************************************
|
||||
* 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: "cos",
|
||||
name: "Cosine",
|
||||
unit: "deg",
|
||||
formatString: '%0.2f',
|
||||
key: "wavelengths",
|
||||
name: "Wavelength",
|
||||
unit: "nm",
|
||||
format: 'string[]',
|
||||
hints: {
|
||||
domain: 3
|
||||
range: 4
|
||||
}
|
||||
},
|
||||
// Need to enable "LocalTimeSystem" plugin to make use of this
|
||||
@ -64,6 +64,14 @@ define([
|
||||
hints: {
|
||||
range: 2
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "intensities",
|
||||
name: "Intensities",
|
||||
format: 'number[]',
|
||||
hints: {
|
||||
range: 3
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -32,7 +32,8 @@ define([
|
||||
offset: 0,
|
||||
dataRateInHz: 1,
|
||||
randomness: 0,
|
||||
phase: 0
|
||||
phase: 0,
|
||||
loadDelay: 0
|
||||
};
|
||||
|
||||
function GeneratorProvider(openmct) {
|
||||
@ -53,8 +54,9 @@ define([
|
||||
'period',
|
||||
'offset',
|
||||
'dataRateInHz',
|
||||
'randomness',
|
||||
'phase',
|
||||
'randomness'
|
||||
'loadDelay'
|
||||
];
|
||||
|
||||
request = request || {};
|
||||
|
@ -23,7 +23,7 @@
|
||||
define([
|
||||
'uuid'
|
||||
], function (
|
||||
uuid
|
||||
{ v4: uuid }
|
||||
) {
|
||||
function WorkerInterface(openmct) {
|
||||
// eslint-disable-next-line no-undef
|
||||
|
@ -77,7 +77,8 @@
|
||||
utc: nextStep,
|
||||
yesterday: nextStep - 60 * 60 * 24 * 1000,
|
||||
sin: sin(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness),
|
||||
wavelength: wavelength(start, nextStep),
|
||||
wavelengths: wavelengths(),
|
||||
intensities: intensities(),
|
||||
cos: cos(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness)
|
||||
}
|
||||
});
|
||||
@ -115,6 +116,7 @@
|
||||
var dataRateInHz = request.dataRateInHz;
|
||||
var phase = request.phase;
|
||||
var randomness = request.randomness;
|
||||
var loadDelay = Math.max(request.loadDelay, 0);
|
||||
|
||||
var step = 1000 / dataRateInHz;
|
||||
var nextStep = start - (start % step) + step;
|
||||
@ -126,11 +128,20 @@
|
||||
utc: nextStep,
|
||||
yesterday: nextStep - 60 * 60 * 24 * 1000,
|
||||
sin: sin(nextStep, period, amplitude, offset, phase, randomness),
|
||||
wavelength: wavelength(start, nextStep),
|
||||
wavelengths: wavelengths(),
|
||||
intensities: intensities(),
|
||||
cos: cos(nextStep, period, amplitude, offset, phase, randomness)
|
||||
});
|
||||
}
|
||||
|
||||
if (loadDelay === 0) {
|
||||
postOnRequest(message, request, data);
|
||||
} else {
|
||||
setTimeout(() => postOnRequest(message, request, data), loadDelay);
|
||||
}
|
||||
}
|
||||
|
||||
function postOnRequest(message, request, data) {
|
||||
self.postMessage({
|
||||
id: message.id,
|
||||
data: request.spectra ? {
|
||||
@ -154,8 +165,28 @@
|
||||
* Math.sin(phase + (timestamp / period / 1000 * Math.PI * 2)) + (amplitude * Math.random() * randomness) + offset;
|
||||
}
|
||||
|
||||
function wavelength(start, nextStep) {
|
||||
return (nextStep - start) / 10;
|
||||
function wavelengths() {
|
||||
let values = [];
|
||||
while (values.length < 5) {
|
||||
const randomValue = Math.random() * 100;
|
||||
if (!values.includes(randomValue)) {
|
||||
values.push(String(randomValue));
|
||||
}
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
function intensities() {
|
||||
let values = [];
|
||||
while (values.length < 5) {
|
||||
const randomValue = Math.random() * 10;
|
||||
if (!values.includes(randomValue)) {
|
||||
values.push(String(randomValue));
|
||||
}
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
function sendError(error, message) {
|
||||
|
@ -81,7 +81,7 @@ define([
|
||||
{
|
||||
name: "Amplitude",
|
||||
control: "numberfield",
|
||||
cssClass: "l-input-sm l-numeric",
|
||||
cssClass: "l-numeric",
|
||||
key: "amplitude",
|
||||
required: true,
|
||||
property: [
|
||||
@ -92,7 +92,7 @@ define([
|
||||
{
|
||||
name: "Offset",
|
||||
control: "numberfield",
|
||||
cssClass: "l-input-sm l-numeric",
|
||||
cssClass: "l-numeric",
|
||||
key: "offset",
|
||||
required: true,
|
||||
property: [
|
||||
@ -132,6 +132,17 @@ define([
|
||||
"telemetry",
|
||||
"randomness"
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Loading Delay (ms)",
|
||||
control: "numberfield",
|
||||
cssClass: "l-input-sm l-numeric",
|
||||
key: "loadDelay",
|
||||
required: true,
|
||||
property: [
|
||||
"telemetry",
|
||||
"loadDelay"
|
||||
]
|
||||
}
|
||||
],
|
||||
initialize: function (object) {
|
||||
@ -141,7 +152,8 @@ define([
|
||||
offset: 0,
|
||||
dataRateInHz: 1,
|
||||
phase: 0,
|
||||
randomness: 0
|
||||
randomness: 0,
|
||||
loadDelay: 0
|
||||
};
|
||||
}
|
||||
});
|
||||
|
@ -59,7 +59,8 @@ export default function () {
|
||||
object.configuration = {
|
||||
imageLocation: '',
|
||||
imageLoadDelayInMilliSeconds: DEFAULT_IMAGE_LOAD_DELAY_IN_MILISECONDS,
|
||||
imageSamples: []
|
||||
imageSamples: [],
|
||||
layers: []
|
||||
};
|
||||
|
||||
object.telemetry = {
|
||||
@ -90,7 +91,21 @@ export default function () {
|
||||
format: 'image',
|
||||
hints: {
|
||||
image: 1
|
||||
}
|
||||
},
|
||||
layers: [
|
||||
{
|
||||
source: 'dist/imagery/example-imagery-layer-16x9.png',
|
||||
name: '16:9'
|
||||
},
|
||||
{
|
||||
source: 'dist/imagery/example-imagery-layer-safe.png',
|
||||
name: 'Safe'
|
||||
},
|
||||
{
|
||||
source: 'dist/imagery/example-imagery-layer-scale.png',
|
||||
name: 'Scale'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Image Download Name',
|
||||
@ -153,7 +168,7 @@ function getImageUrlListFromConfig(configuration) {
|
||||
}
|
||||
|
||||
function getImageLoadDelay(domainObject) {
|
||||
const imageLoadDelay = domainObject.configuration.imageLoadDelayInMilliSeconds;
|
||||
const imageLoadDelay = Math.trunc(Number(domainObject.configuration.imageLoadDelayInMilliSeconds));
|
||||
if (!imageLoadDelay) {
|
||||
openmctInstance.objects.mutate(domainObject, 'configuration.imageLoadDelayInMilliSeconds', DEFAULT_IMAGE_LOAD_DELAY_IN_MILISECONDS);
|
||||
|
||||
@ -175,7 +190,9 @@ function getRealtimeProvider() {
|
||||
subscribe: (domainObject, callback) => {
|
||||
const delay = getImageLoadDelay(domainObject);
|
||||
const interval = setInterval(() => {
|
||||
callback(pointForTimestamp(Date.now(), domainObject.name, getImageSamples(domainObject.configuration), delay));
|
||||
const imageSamples = getImageSamples(domainObject.configuration);
|
||||
const datum = pointForTimestamp(Date.now(), domainObject.name, imageSamples, delay);
|
||||
callback(datum);
|
||||
}, delay);
|
||||
|
||||
return () => {
|
||||
@ -214,8 +231,9 @@ function getLadProvider() {
|
||||
},
|
||||
request: (domainObject, options) => {
|
||||
const delay = getImageLoadDelay(domainObject);
|
||||
const datum = pointForTimestamp(Date.now(), domainObject.name, getImageSamples(domainObject.configuration), delay);
|
||||
|
||||
return Promise.resolve([pointForTimestamp(Date.now(), domainObject.name, delay)]);
|
||||
return Promise.resolve([datum]);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -75,12 +75,12 @@
|
||||
const TWO_HOURS = ONE_HOUR * 2;
|
||||
const ONE_DAY = ONE_HOUR * 24;
|
||||
|
||||
|
||||
openmct.install(openmct.plugins.LocalStorage());
|
||||
|
||||
|
||||
openmct.install(openmct.plugins.example.Generator());
|
||||
openmct.install(openmct.plugins.example.EventGeneratorPlugin());
|
||||
openmct.install(openmct.plugins.example.ExampleImagery());
|
||||
openmct.install(openmct.plugins.example.ExampleTags());
|
||||
|
||||
openmct.install(openmct.plugins.Espresso());
|
||||
openmct.install(openmct.plugins.MyItems());
|
||||
@ -191,10 +191,13 @@
|
||||
openmct.install(openmct.plugins.ObjectMigration());
|
||||
openmct.install(openmct.plugins.ClearData(
|
||||
['table', 'telemetry.plot.overlay', 'telemetry.plot.stacked', 'example.imagery'],
|
||||
{indicator: true}
|
||||
{ indicator: true }
|
||||
));
|
||||
openmct.install(openmct.plugins.Clock({ enableClockIndicator: true }));
|
||||
openmct.install(openmct.plugins.Timer());
|
||||
openmct.install(openmct.plugins.Timelist());
|
||||
openmct.install(openmct.plugins.BarChart());
|
||||
openmct.install(openmct.plugins.ScatterPlot());
|
||||
openmct.start();
|
||||
</script>
|
||||
</html>
|
||||
|
@ -74,13 +74,8 @@ module.exports = (config) => {
|
||||
},
|
||||
coverageIstanbulReporter: {
|
||||
fixWebpackSourcePaths: true,
|
||||
dir: "dist/reports/coverage",
|
||||
reports: ['lcovonly', 'text-summary'],
|
||||
thresholds: {
|
||||
global: {
|
||||
lines: 52
|
||||
}
|
||||
}
|
||||
dir: "coverage/unit",
|
||||
reports: ['lcovonly']
|
||||
},
|
||||
specReporter: {
|
||||
maxLogLines: 5,
|
||||
|
86
package.json
86
package.json
@ -1,108 +1,113 @@
|
||||
{
|
||||
"name": "openmct",
|
||||
"version": "2.0.2-SNAPSHOT",
|
||||
"version": "2.0.5",
|
||||
"description": "The Open MCT core platform",
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "7.16.3",
|
||||
"@babel/eslint-parser": "7.18.2",
|
||||
"@braintree/sanitize-url": "6.0.0",
|
||||
"@percy/cli": "1.0.0-beta.76",
|
||||
"@percy/playwright": "1.0.1",
|
||||
"@playwright/test": "1.19.2",
|
||||
"@percy/cli": "1.2.1",
|
||||
"@percy/playwright": "1.0.4",
|
||||
"@playwright/test": "1.23.0",
|
||||
"@types/eventemitter3": "^1.0.0",
|
||||
"@types/jasmine": "^4.0.1",
|
||||
"@types/karma": "^6.3.2",
|
||||
"@types/lodash": "^4.14.178",
|
||||
"@types/mocha": "^9.1.0",
|
||||
"allure-playwright": "2.0.0-beta.15",
|
||||
"babel-loader": "8.2.3",
|
||||
"babel-plugin-istanbul": "6.1.1",
|
||||
"comma-separated-values": "3.6.4",
|
||||
"copy-webpack-plugin": "10.2.0",
|
||||
"core-js": "3.21.1",
|
||||
"codecov":"3.8.3",
|
||||
"copy-webpack-plugin": "11.0.0",
|
||||
"cross-env": "7.0.3",
|
||||
"css-loader": "4.0.0",
|
||||
"d3-axis": "1.0.x",
|
||||
"d3-scale": "1.0.x",
|
||||
"d3-selection": "1.3.x",
|
||||
"eslint": "8.11.0",
|
||||
"d3-axis": "3.0.0",
|
||||
"d3-scale": "3.3.0",
|
||||
"d3-selection": "3.0.0",
|
||||
"eslint": "8.13.0",
|
||||
"eslint-plugin-compat": "4.0.2",
|
||||
"eslint-plugin-playwright": "0.8.0",
|
||||
"eslint-plugin-vue": "8.5.0",
|
||||
"eslint-plugin-playwright": "0.9.0",
|
||||
"eslint-plugin-vue": "9.1.0",
|
||||
"eslint-plugin-you-dont-need-lodash-underscore": "6.12.0",
|
||||
"eventemitter3": "1.2.0",
|
||||
"exports-loader": "0.7.0",
|
||||
"express": "4.13.1",
|
||||
"file-saver": "2.0.5",
|
||||
"git-rev-sync": "3.0.2",
|
||||
"html2canvas": "1.4.1",
|
||||
"imports-loader": "0.8.0",
|
||||
"jasmine-core": "4.0.1",
|
||||
"jasmine-core": "4.1.1",
|
||||
"jsdoc": "3.5.5",
|
||||
"karma": "6.3.15",
|
||||
"karma": "6.3.20",
|
||||
"karma-chrome-launcher": "3.1.1",
|
||||
"karma-cli": "2.0.0",
|
||||
"karma-coverage": "2.1.1",
|
||||
"karma-coverage": "2.2.0",
|
||||
"karma-coverage-istanbul-reporter": "3.0.3",
|
||||
"karma-firefox-launcher": "2.1.2",
|
||||
"karma-jasmine": "4.0.1",
|
||||
"karma-junit-reporter": "2.0.1",
|
||||
"karma-sourcemap-loader": "0.3.8",
|
||||
"karma-spec-reporter": "0.0.33",
|
||||
"karma-spec-reporter": "0.0.34",
|
||||
"karma-webpack": "5.0.0",
|
||||
"lighthouse": "9.5.0",
|
||||
"lighthouse": "9.6.1",
|
||||
"location-bar": "3.0.1",
|
||||
"lodash": "4.17.21",
|
||||
"mini-css-extract-plugin": "2.6.0",
|
||||
"moment": "2.29.1",
|
||||
"moment-duration-format": "2.2.2",
|
||||
"moment": "2.29.3",
|
||||
"moment-duration-format": "2.3.2",
|
||||
"moment-timezone": "0.5.34",
|
||||
"node-bourbon": "4.2.3",
|
||||
"painterro": "1.2.56",
|
||||
"plotly.js-basic-dist": "2.5.0",
|
||||
"plotly.js-gl2d-dist": "2.5.0",
|
||||
"nyc":"15.1.0",
|
||||
"plotly.js-basic-dist": "2.12.0",
|
||||
"plotly.js-gl2d-dist": "2.12.0",
|
||||
"printj": "1.3.1",
|
||||
"request": "2.88.2",
|
||||
"resolve-url-loader": "4.0.0",
|
||||
"sass": "1.49.9",
|
||||
"resolve-url-loader": "5.0.0",
|
||||
"sass": "1.52.2",
|
||||
"sass-loader": "12.6.0",
|
||||
"sinon": "13.0.1",
|
||||
"sinon": "14.0.0",
|
||||
"style-loader": "^1.0.1",
|
||||
"uuid": "3.3.3",
|
||||
"uuid": "8.3.2",
|
||||
"vue": "2.6.14",
|
||||
"vue-eslint-parser": "8.2.0",
|
||||
"vue-eslint-parser": "9.0.2",
|
||||
"vue-loader": "15.9.8",
|
||||
"vue-template-compiler": "2.6.14",
|
||||
"webpack": "5.68.0",
|
||||
"webpack-cli": "4.9.2",
|
||||
"webpack-dev-middleware": "5.3.1",
|
||||
"webpack-dev-middleware": "5.3.3",
|
||||
"webpack-hot-middleware": "2.25.1",
|
||||
"webpack-merge": "5.8.0",
|
||||
"zepto": "1.2.0"
|
||||
"webpack-merge": "5.8.0"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rm -rf ./dist ./node_modules ./package-lock.json",
|
||||
"clean-test-lint": "npm run clean; npm install; npm run test; npm run lint",
|
||||
"start": "node app.js",
|
||||
"lint": "eslint example src --ext .js,.vue openmct.js",
|
||||
"lint:fix": "eslint example src --ext .js,.vue openmct.js --fix",
|
||||
"lint": "eslint example src e2e --ext .js,.vue openmct.js --max-warnings=0",
|
||||
"lint:fix": "eslint example src e2e --ext .js,.vue openmct.js --fix",
|
||||
"build:prod": "cross-env webpack --config webpack.prod.js",
|
||||
"build:dev": "webpack --config webpack.dev.js",
|
||||
"build:coverage": "webpack --config webpack.coverage.js",
|
||||
"build:watch": "webpack --config webpack.dev.js --watch",
|
||||
"info": "npx envinfo --system --browsers --npmPackages --binaries --languages --markdown",
|
||||
"test": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run",
|
||||
"test:firefox": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run --browsers=FirefoxHeadless",
|
||||
"test": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run",
|
||||
"test:firefox": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run --browsers=FirefoxHeadless",
|
||||
"test:debug": "cross-env NODE_ENV=debug karma start --no-single-run",
|
||||
"test:e2e:ci": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome smoke default condition timeConductor",
|
||||
"test:e2e": "npx playwright test",
|
||||
"test:e2e:ci": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome smoke branding default condition timeConductor clock exampleImagery persistence performance grandsearch notebook/tags",
|
||||
"test:e2e:local": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome",
|
||||
"test:e2e:visual": "percy exec --config ./e2e/.percy.yml -- npx playwright test --config=e2e/playwright-visual.config.js default",
|
||||
"test:e2e:updatesnapshots": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @snapshot --update-snapshots",
|
||||
"test:e2e:visual": "percy exec --config ./e2e/.percy.yml -- npx playwright test --config=e2e/playwright-visual.config.js",
|
||||
"test:e2e:full": "npx playwright test --config=e2e/playwright-ci.config.js",
|
||||
"test:watch": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --no-single-run",
|
||||
"test:perf": "npx playwright test --config=e2e/playwright-performance.config.js",
|
||||
"test:watch": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --no-single-run",
|
||||
"jsdoc": "jsdoc -c jsdoc.json -R API.md -r -d dist/docs/api",
|
||||
"update-about-dialog-copyright": "perl -pi -e 's/20\\d\\d\\-202\\d/2014\\-2022/gm' ./src/ui/layout/AboutDialog.vue",
|
||||
"update-copyright-date": "npm run update-about-dialog-copyright && grep -lr --null --include=*.{js,scss,vue,ts,sh,html,md,frag} 'Copyright (c) 20' . | xargs -r0 perl -pi -e 's/Copyright\\s\\(c\\)\\s20\\d\\d\\-20\\d\\d/Copyright \\(c\\)\\ 2014\\-2022/gm'",
|
||||
"otherdoc": "node docs/gendocs.js --in docs/src --out dist/docs --suppress-toc 'docs/src/index.md|docs/src/process/index.md'",
|
||||
"docs": "npm run jsdoc ; npm run otherdoc",
|
||||
"cov:e2e:report":"nyc report --reporter=lcovonly --report-dir=./coverage/e2e",
|
||||
"cov:e2e:full:publish":"codecov --disable=gcov -f ./coverage/e2e/lcov.info -F e2e-full",
|
||||
"cov:e2e:ci:publish":"codecov --disable=gcov -f ./coverage/e2e/lcov.info -F e2e-ci",
|
||||
"cov:unit:publish":"codecov --disable=gcov -f ./coverage/unit/lcov.info -F unit",
|
||||
"prepare": "npm run build:prod"
|
||||
},
|
||||
"repository": {
|
||||
@ -112,6 +117,9 @@
|
||||
"engines": {
|
||||
"node": ">=14.19.1"
|
||||
},
|
||||
"overrides": {
|
||||
"core-js": "3.21.1"
|
||||
},
|
||||
"browserslist": [
|
||||
"Firefox ESR",
|
||||
"not IE 11",
|
||||
|
270
src/MCT.js
270
src/MCT.js
@ -42,6 +42,7 @@ define([
|
||||
'./plugins/duplicate/plugin',
|
||||
'./plugins/importFromJSONAction/plugin',
|
||||
'./plugins/exportAsJSONAction/plugin',
|
||||
'./ui/components/components',
|
||||
'vue'
|
||||
], function (
|
||||
EventEmitter,
|
||||
@ -65,6 +66,7 @@ define([
|
||||
DuplicateActionPlugin,
|
||||
ImportFromJSONAction,
|
||||
ExportAsJSONAction,
|
||||
components,
|
||||
Vue
|
||||
) {
|
||||
/**
|
||||
@ -94,156 +96,170 @@ define([
|
||||
};
|
||||
|
||||
this.destroy = this.destroy.bind(this);
|
||||
/**
|
||||
* Tracks current selection state of the application.
|
||||
* @private
|
||||
*/
|
||||
this.selection = new Selection(this);
|
||||
[
|
||||
/**
|
||||
* Tracks current selection state of the application.
|
||||
* @private
|
||||
*/
|
||||
['selection', () => new Selection(this)],
|
||||
|
||||
/**
|
||||
* MCT's time conductor, which may be used to synchronize view contents
|
||||
* for telemetry- or time-based views.
|
||||
* @type {module:openmct.TimeConductor}
|
||||
* @memberof module:openmct.MCT#
|
||||
* @name conductor
|
||||
*/
|
||||
this.time = new api.TimeAPI(this);
|
||||
/**
|
||||
* MCT's time conductor, which may be used to synchronize view contents
|
||||
* for telemetry- or time-based views.
|
||||
* @type {module:openmct.TimeConductor}
|
||||
* @memberof module:openmct.MCT#
|
||||
* @name conductor
|
||||
*/
|
||||
['time', () => new api.TimeAPI(this)],
|
||||
|
||||
/**
|
||||
* An interface for interacting with the composition of domain objects.
|
||||
* The composition of a domain object is the list of other domain
|
||||
* objects it "contains" (for instance, that should be displayed
|
||||
* beneath it in the tree.)
|
||||
*
|
||||
* `composition` may be called as a function, in which case it acts
|
||||
* as [`composition.get`]{@link module:openmct.CompositionAPI#get}.
|
||||
*
|
||||
* @type {module:openmct.CompositionAPI}
|
||||
* @memberof module:openmct.MCT#
|
||||
* @name composition
|
||||
*/
|
||||
this.composition = new api.CompositionAPI(this);
|
||||
/**
|
||||
* An interface for interacting with the composition of domain objects.
|
||||
* The composition of a domain object is the list of other domain
|
||||
* objects it "contains" (for instance, that should be displayed
|
||||
* beneath it in the tree.)
|
||||
*
|
||||
* `composition` may be called as a function, in which case it acts
|
||||
* as [`composition.get`]{@link module:openmct.CompositionAPI#get}.
|
||||
*
|
||||
* @type {module:openmct.CompositionAPI}
|
||||
* @memberof module:openmct.MCT#
|
||||
* @name composition
|
||||
*/
|
||||
['composition', () => new api.CompositionAPI(this)],
|
||||
|
||||
/**
|
||||
* Registry for views of domain objects which should appear in the
|
||||
* main viewing area.
|
||||
*
|
||||
* @type {module:openmct.ViewRegistry}
|
||||
* @memberof module:openmct.MCT#
|
||||
* @name objectViews
|
||||
*/
|
||||
this.objectViews = new ViewRegistry();
|
||||
/**
|
||||
* Registry for views of domain objects which should appear in the
|
||||
* main viewing area.
|
||||
*
|
||||
* @type {module:openmct.ViewRegistry}
|
||||
* @memberof module:openmct.MCT#
|
||||
* @name objectViews
|
||||
*/
|
||||
['objectViews', () => new ViewRegistry()],
|
||||
|
||||
/**
|
||||
* Registry for views which should appear in the Inspector area.
|
||||
* These views will be chosen based on the selection state.
|
||||
*
|
||||
* @type {module:openmct.InspectorViewRegistry}
|
||||
* @memberof module:openmct.MCT#
|
||||
* @name inspectorViews
|
||||
*/
|
||||
this.inspectorViews = new InspectorViewRegistry();
|
||||
/**
|
||||
* Registry for views which should appear in the Inspector area.
|
||||
* These views will be chosen based on the selection state.
|
||||
*
|
||||
* @type {module:openmct.InspectorViewRegistry}
|
||||
* @memberof module:openmct.MCT#
|
||||
* @name inspectorViews
|
||||
*/
|
||||
['inspectorViews', () => new InspectorViewRegistry()],
|
||||
|
||||
/**
|
||||
* Registry for views which should appear in Edit Properties
|
||||
* dialogs, and similar user interface elements used for
|
||||
* modifying domain objects external to its regular views.
|
||||
*
|
||||
* @type {module:openmct.ViewRegistry}
|
||||
* @memberof module:openmct.MCT#
|
||||
* @name propertyEditors
|
||||
*/
|
||||
this.propertyEditors = new ViewRegistry();
|
||||
/**
|
||||
* Registry for views which should appear in Edit Properties
|
||||
* dialogs, and similar user interface elements used for
|
||||
* modifying domain objects external to its regular views.
|
||||
*
|
||||
* @type {module:openmct.ViewRegistry}
|
||||
* @memberof module:openmct.MCT#
|
||||
* @name propertyEditors
|
||||
*/
|
||||
['propertyEditors', () => new ViewRegistry()],
|
||||
|
||||
/**
|
||||
* Registry for views which should appear in the status indicator area.
|
||||
* @type {module:openmct.ViewRegistry}
|
||||
* @memberof module:openmct.MCT#
|
||||
* @name indicators
|
||||
*/
|
||||
this.indicators = new ViewRegistry();
|
||||
/**
|
||||
* Registry for views which should appear in the toolbar area while
|
||||
* editing. These views will be chosen based on the selection state.
|
||||
*
|
||||
* @type {module:openmct.ToolbarRegistry}
|
||||
* @memberof module:openmct.MCT#
|
||||
* @name toolbars
|
||||
*/
|
||||
['toolbars', () => new ToolbarRegistry()],
|
||||
|
||||
/**
|
||||
* Registry for views which should appear in the toolbar area while
|
||||
* editing. These views will be chosen based on the selection state.
|
||||
*
|
||||
* @type {module:openmct.ToolbarRegistry}
|
||||
* @memberof module:openmct.MCT#
|
||||
* @name toolbars
|
||||
*/
|
||||
this.toolbars = new ToolbarRegistry();
|
||||
/**
|
||||
* Registry for domain object types which may exist within this
|
||||
* instance of Open MCT.
|
||||
*
|
||||
* @type {module:openmct.TypeRegistry}
|
||||
* @memberof module:openmct.MCT#
|
||||
* @name types
|
||||
*/
|
||||
['types', () => new api.TypeRegistry()],
|
||||
|
||||
/**
|
||||
* Registry for domain object types which may exist within this
|
||||
* instance of Open MCT.
|
||||
*
|
||||
* @type {module:openmct.TypeRegistry}
|
||||
* @memberof module:openmct.MCT#
|
||||
* @name types
|
||||
*/
|
||||
this.types = new api.TypeRegistry();
|
||||
/**
|
||||
* An interface for interacting with domain objects and the domain
|
||||
* object hierarchy.
|
||||
*
|
||||
* @type {module:openmct.ObjectAPI}
|
||||
* @memberof module:openmct.MCT#
|
||||
* @name objects
|
||||
*/
|
||||
['objects', () => new api.ObjectAPI.default(this.types, this)],
|
||||
|
||||
/**
|
||||
* An interface for interacting with domain objects and the domain
|
||||
* object hierarchy.
|
||||
*
|
||||
* @type {module:openmct.ObjectAPI}
|
||||
* @memberof module:openmct.MCT#
|
||||
* @name objects
|
||||
*/
|
||||
this.objects = new api.ObjectAPI.default(this.types, this);
|
||||
/**
|
||||
* An interface for retrieving and interpreting telemetry data associated
|
||||
* with a domain object.
|
||||
*
|
||||
* @type {module:openmct.TelemetryAPI}
|
||||
* @memberof module:openmct.MCT#
|
||||
* @name telemetry
|
||||
*/
|
||||
['telemetry', () => new api.TelemetryAPI.default(this)],
|
||||
|
||||
/**
|
||||
* An interface for retrieving and interpreting telemetry data associated
|
||||
* with a domain object.
|
||||
*
|
||||
* @type {module:openmct.TelemetryAPI}
|
||||
* @memberof module:openmct.MCT#
|
||||
* @name telemetry
|
||||
*/
|
||||
this.telemetry = new api.TelemetryAPI(this);
|
||||
/**
|
||||
* An interface for creating new indicators and changing them dynamically.
|
||||
*
|
||||
* @type {module:openmct.IndicatorAPI}
|
||||
* @memberof module:openmct.MCT#
|
||||
* @name indicators
|
||||
*/
|
||||
['indicators', () => new api.IndicatorAPI(this)],
|
||||
|
||||
/**
|
||||
* An interface for creating new indicators and changing them dynamically.
|
||||
*
|
||||
* @type {module:openmct.IndicatorAPI}
|
||||
* @memberof module:openmct.MCT#
|
||||
* @name indicators
|
||||
*/
|
||||
this.indicators = new api.IndicatorAPI(this);
|
||||
/**
|
||||
* MCT's user awareness management, to enable user and
|
||||
* role specific functionality.
|
||||
* @type {module:openmct.UserAPI}
|
||||
* @memberof module:openmct.MCT#
|
||||
* @name user
|
||||
*/
|
||||
['user', () => new api.UserAPI(this)],
|
||||
|
||||
/**
|
||||
* MCT's user awareness management, to enable user and
|
||||
* role specific functionality.
|
||||
* @type {module:openmct.UserAPI}
|
||||
* @memberof module:openmct.MCT#
|
||||
* @name user
|
||||
*/
|
||||
this.user = new api.UserAPI(this);
|
||||
['notifications', () => new api.NotificationAPI()],
|
||||
|
||||
this.notifications = new api.NotificationAPI();
|
||||
['editor', () => new api.EditorAPI.default(this)],
|
||||
|
||||
this.editor = new api.EditorAPI.default(this);
|
||||
['overlays', () => new OverlayAPI.default()],
|
||||
|
||||
this.overlays = new OverlayAPI.default();
|
||||
['menus', () => new api.MenuAPI(this)],
|
||||
|
||||
this.menus = new api.MenuAPI(this);
|
||||
['actions', () => new api.ActionsAPI(this)],
|
||||
|
||||
this.actions = new api.ActionsAPI(this);
|
||||
['status', () => new api.StatusAPI(this)],
|
||||
|
||||
this.status = new api.StatusAPI(this);
|
||||
['priority', () => api.PriorityAPI],
|
||||
|
||||
this.priority = api.PriorityAPI;
|
||||
['router', () => new ApplicationRouter(this)],
|
||||
|
||||
this.router = new ApplicationRouter(this);
|
||||
this.forms = new api.FormsAPI.default(this);
|
||||
['faults', () => new api.FaultManagementAPI.default(this)],
|
||||
|
||||
this.branding = BrandingAPI.default;
|
||||
['forms', () => new api.FormsAPI.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
|
||||
*/
|
||||
['annotation', () => new api.AnnotationAPI(this)]
|
||||
].forEach(apiEntry => {
|
||||
const apiName = apiEntry[0];
|
||||
const apiObject = apiEntry[1]();
|
||||
|
||||
Object.defineProperty(this, apiName, {
|
||||
value: apiObject,
|
||||
enumerable: false,
|
||||
configurable: false,
|
||||
writable: true
|
||||
});
|
||||
});
|
||||
|
||||
// Plugins that are installed by default
|
||||
|
||||
this.install(this.plugins.Plot());
|
||||
this.install(this.plugins.Chart());
|
||||
this.install(this.plugins.TelemetryTable.default());
|
||||
this.install(PreviewPlugin.default());
|
||||
this.install(LicensesPlugin.default());
|
||||
@ -271,6 +287,7 @@ define([
|
||||
this.install(this.plugins.ObjectInterceptors());
|
||||
this.install(this.plugins.DeviceClassifier());
|
||||
this.install(this.plugins.UserIndicator());
|
||||
this.install(this.plugins.Gauge());
|
||||
}
|
||||
|
||||
MCT.prototype = Object.create(EventEmitter.prototype);
|
||||
@ -379,6 +396,7 @@ define([
|
||||
};
|
||||
|
||||
MCT.prototype.plugins = plugins;
|
||||
MCT.prototype.components = components.default;
|
||||
|
||||
return MCT;
|
||||
});
|
||||
|
@ -85,8 +85,6 @@ class ActionCollection extends EventEmitter {
|
||||
}
|
||||
|
||||
destroy() {
|
||||
super.removeAllListeners();
|
||||
|
||||
if (!this.skipEnvironmentObservers) {
|
||||
this.objectUnsubscribes.forEach(unsubscribe => {
|
||||
unsubscribe();
|
||||
@ -96,6 +94,7 @@ class ActionCollection extends EventEmitter {
|
||||
}
|
||||
|
||||
this.emit('destroy', this.view);
|
||||
this.removeAllListeners();
|
||||
}
|
||||
|
||||
getVisibleActions() {
|
||||
|
277
src/api/annotation/AnnotationAPI.js
Normal file
277
src/api/annotation/AnnotationAPI.js
Normal file
@ -0,0 +1,277 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import EventEmitter from 'EventEmitter';
|
||||
|
||||
/**
|
||||
* @readonly
|
||||
* @enum {String} AnnotationType
|
||||
* @property {String} NOTEBOOK The notebook annotation type
|
||||
* @property {String} GEOSPATIAL The geospatial annotation type
|
||||
* @property {String} PIXEL_SPATIAL The pixel-spatial annotation type
|
||||
* @property {String} TEMPORAL The temporal annotation type
|
||||
* @property {String} PLOT_SPATIAL The plot-spatial annotation type
|
||||
*/
|
||||
const ANNOTATION_TYPES = Object.freeze({
|
||||
NOTEBOOK: 'NOTEBOOK',
|
||||
GEOSPATIAL: 'GEOSPATIAL',
|
||||
PIXEL_SPATIAL: 'PIXEL_SPATIAL',
|
||||
TEMPORAL: 'TEMPORAL',
|
||||
PLOT_SPATIAL: 'PLOT_SPATIAL'
|
||||
});
|
||||
|
||||
/**
|
||||
* @typedef {Object} Tag
|
||||
* @property {String} key a unique identifier for the tag
|
||||
* @property {String} backgroundColor eg. "#cc0000"
|
||||
* @property {String} foregroundColor eg. "#ffffff"
|
||||
*/
|
||||
export default class AnnotationAPI extends EventEmitter {
|
||||
constructor(openmct) {
|
||||
super();
|
||||
this.openmct = openmct;
|
||||
this.availableTags = {};
|
||||
|
||||
this.ANNOTATION_TYPES = ANNOTATION_TYPES;
|
||||
|
||||
this.openmct.types.addType('annotation', {
|
||||
name: 'Annotation',
|
||||
description: 'A user created note or comment about time ranges, pixel space, and geospatial features.',
|
||||
creatable: false,
|
||||
cssClass: 'icon-notebook',
|
||||
initialize: function (domainObject) {
|
||||
domainObject.targets = domainObject.targets || {};
|
||||
domainObject.originalContextPath = domainObject.originalContextPath || '';
|
||||
domainObject.tags = domainObject.tags || [];
|
||||
domainObject.contentText = domainObject.contentText || '';
|
||||
domainObject.annotationType = domainObject.annotationType || 'plotspatial';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the a generic annotation
|
||||
* @typedef {Object} CreateAnnotationOptions
|
||||
* @property {String} name a name for the new parameter
|
||||
* @property {import('../objects/ObjectAPI').DomainObject} domainObject the domain object to create
|
||||
* @property {ANNOTATION_TYPES} annotationType the type of annotation to create
|
||||
* @property {Tag[]} tags
|
||||
* @property {String} contentText
|
||||
* @property {import('../objects/ObjectAPI').Identifier[]} targets
|
||||
*/
|
||||
/**
|
||||
* @method create
|
||||
* @param {CreateAnnotationOptions} options
|
||||
* @returns {Promise<import('../objects/ObjectAPI').DomainObject>} a promise which will resolve when the domain object
|
||||
* has been created, or be rejected if it cannot be saved
|
||||
*/
|
||||
async create({name, domainObject, annotationType, tags, contentText, targets}) {
|
||||
if (!Object.keys(this.ANNOTATION_TYPES).includes(annotationType)) {
|
||||
throw new Error(`Unknown annotation type: ${annotationType}`);
|
||||
}
|
||||
|
||||
if (!Object.keys(targets).length) {
|
||||
throw new Error(`At least one target is required to create an annotation`);
|
||||
}
|
||||
|
||||
const domainObjectKeyString = this.openmct.objects.makeKeyString(domainObject.identifier);
|
||||
const originalPathObjects = await this.openmct.objects.getOriginalPath(domainObjectKeyString);
|
||||
const originalContextPath = this.openmct.objects.getRelativePath(originalPathObjects);
|
||||
const namespace = domainObject.identifier.namespace;
|
||||
const type = 'annotation';
|
||||
const typeDefinition = this.openmct.types.get(type);
|
||||
const definition = typeDefinition.definition;
|
||||
|
||||
const createdObject = {
|
||||
name,
|
||||
type,
|
||||
identifier: {
|
||||
key: uuid(),
|
||||
namespace
|
||||
},
|
||||
tags,
|
||||
annotationType,
|
||||
contentText,
|
||||
originalContextPath
|
||||
};
|
||||
|
||||
if (definition.initialize) {
|
||||
definition.initialize(createdObject);
|
||||
}
|
||||
|
||||
createdObject.targets = targets;
|
||||
createdObject.originalContextPath = originalContextPath;
|
||||
|
||||
const success = await this.openmct.objects.save(createdObject);
|
||||
if (success) {
|
||||
this.emit('annotationCreated', createdObject);
|
||||
|
||||
return createdObject;
|
||||
} else {
|
||||
throw new Error('Failed to create object');
|
||||
}
|
||||
}
|
||||
|
||||
defineTag(tagKey, tagsDefinition) {
|
||||
this.availableTags[tagKey] = tagsDefinition;
|
||||
}
|
||||
|
||||
getAvailableTags() {
|
||||
if (this.availableTags) {
|
||||
const rearrangedToArray = Object.keys(this.availableTags).map(tagKey => {
|
||||
return {
|
||||
id: tagKey,
|
||||
...this.availableTags[tagKey]
|
||||
};
|
||||
});
|
||||
|
||||
return rearrangedToArray;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async getAnnotation(query, searchType) {
|
||||
let foundAnnotation = null;
|
||||
|
||||
const searchResults = (await Promise.all(this.openmct.objects.search(query, null, searchType))).flat();
|
||||
if (searchResults) {
|
||||
foundAnnotation = searchResults[0];
|
||||
}
|
||||
|
||||
return foundAnnotation;
|
||||
}
|
||||
|
||||
async addAnnotationTag(existingAnnotation, targetDomainObject, targetSpecificDetails, annotationType, tag) {
|
||||
if (!existingAnnotation) {
|
||||
const targets = {};
|
||||
const targetKeyString = this.openmct.objects.makeKeyString(targetDomainObject.identifier);
|
||||
targets[targetKeyString] = targetSpecificDetails;
|
||||
const contentText = `${annotationType} tag`;
|
||||
const annotationCreationArguments = {
|
||||
name: contentText,
|
||||
domainObject: targetDomainObject,
|
||||
annotationType,
|
||||
tags: [tag],
|
||||
contentText,
|
||||
targets
|
||||
};
|
||||
const newAnnotation = await this.create(annotationCreationArguments);
|
||||
|
||||
return newAnnotation;
|
||||
} else {
|
||||
const tagArray = [tag, ...existingAnnotation.tags];
|
||||
this.openmct.objects.mutate(existingAnnotation, 'tags', tagArray);
|
||||
|
||||
return existingAnnotation;
|
||||
}
|
||||
}
|
||||
|
||||
removeAnnotationTag(existingAnnotation, tagToRemove) {
|
||||
if (existingAnnotation && existingAnnotation.tags.includes(tagToRemove)) {
|
||||
const cleanedArray = existingAnnotation.tags.filter(extantTag => extantTag !== tagToRemove);
|
||||
this.openmct.objects.mutate(existingAnnotation, 'tags', cleanedArray);
|
||||
} else {
|
||||
throw new Error(`Asked to remove tag (${tagToRemove}) that doesn't exist`, existingAnnotation);
|
||||
}
|
||||
}
|
||||
|
||||
removeAnnotationTags(existingAnnotation) {
|
||||
// just removes tags on the annotation as we can't really delete objects
|
||||
if (existingAnnotation && existingAnnotation.tags) {
|
||||
this.openmct.objects.mutate(existingAnnotation, 'tags', []);
|
||||
}
|
||||
}
|
||||
|
||||
#getMatchingTags(query) {
|
||||
if (!query) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const matchingTags = Object.keys(this.availableTags).filter(tagKey => {
|
||||
if (this.availableTags[tagKey] && this.availableTags[tagKey].label) {
|
||||
return this.availableTags[tagKey].label.toLowerCase().includes(query.toLowerCase());
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
return matchingTags;
|
||||
}
|
||||
|
||||
#addTagMetaInformationToResults(results, matchingTagKeys) {
|
||||
const tagsAddedToResults = results.map(result => {
|
||||
const fullTagModels = result.tags.map(tagKey => {
|
||||
const tagModel = this.availableTags[tagKey];
|
||||
tagModel.tagID = tagKey;
|
||||
|
||||
return tagModel;
|
||||
});
|
||||
|
||||
return {
|
||||
fullTagModels,
|
||||
matchingTagKeys,
|
||||
...result
|
||||
};
|
||||
});
|
||||
|
||||
return tagsAddedToResults;
|
||||
}
|
||||
|
||||
async #addTargetModelsToResults(results) {
|
||||
const modelAddedToResults = await Promise.all(results.map(async result => {
|
||||
const targetModels = await Promise.all(Object.keys(result.targets).map(async (targetID) => {
|
||||
const targetModel = await this.openmct.objects.get(targetID);
|
||||
const targetKeyString = this.openmct.objects.makeKeyString(targetModel.identifier);
|
||||
const originalPathObjects = await this.openmct.objects.getOriginalPath(targetKeyString);
|
||||
|
||||
return {
|
||||
originalPath: originalPathObjects,
|
||||
...targetModel
|
||||
};
|
||||
}));
|
||||
|
||||
return {
|
||||
targetModels,
|
||||
...result
|
||||
};
|
||||
}));
|
||||
|
||||
return modelAddedToResults;
|
||||
}
|
||||
|
||||
/**
|
||||
* @method searchForTags
|
||||
* @param {String} query A query to match against tags. E.g., "dr" will match the tags "drilling" and "driving"
|
||||
* @param {Object} abortController An optional abort method to stop the query
|
||||
* @returns {Promise} returns a model of matching tags with their target domain objects attached
|
||||
*/
|
||||
async searchForTags(query, abortController) {
|
||||
const matchingTagKeys = this.#getMatchingTags(query);
|
||||
const searchResults = (await Promise.all(this.openmct.objects.search(matchingTagKeys, abortController, this.openmct.objects.SEARCH_TYPES.TAGS))).flat();
|
||||
const appliedTagSearchResults = this.#addTagMetaInformationToResults(searchResults, matchingTagKeys);
|
||||
const appliedTargetsModels = await this.#addTargetModelsToResults(appliedTagSearchResults);
|
||||
|
||||
return appliedTargetsModels;
|
||||
}
|
||||
}
|
176
src/api/annotation/AnnotationAPISpec.js
Normal file
176
src/api/annotation/AnnotationAPISpec.js
Normal file
@ -0,0 +1,176 @@
|
||||
/*****************************************************************************
|
||||
* 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,6 +24,7 @@ define([
|
||||
'./actions/ActionsAPI',
|
||||
'./composition/CompositionAPI',
|
||||
'./Editor',
|
||||
'./faultmanagement/FaultManagementAPI',
|
||||
'./forms/FormsAPI',
|
||||
'./indicators/IndicatorAPI',
|
||||
'./menu/MenuAPI',
|
||||
@ -34,11 +35,13 @@ define([
|
||||
'./telemetry/TelemetryAPI',
|
||||
'./time/TimeAPI',
|
||||
'./types/TypeRegistry',
|
||||
'./user/UserAPI'
|
||||
'./user/UserAPI',
|
||||
'./annotation/AnnotationAPI'
|
||||
], function (
|
||||
ActionsAPI,
|
||||
CompositionAPI,
|
||||
EditorAPI,
|
||||
FaultManagementAPI,
|
||||
FormsAPI,
|
||||
IndicatorAPI,
|
||||
MenuAPI,
|
||||
@ -49,14 +52,16 @@ define([
|
||||
TelemetryAPI,
|
||||
TimeAPI,
|
||||
TypeRegistry,
|
||||
UserAPI
|
||||
UserAPI,
|
||||
AnnotationAPI
|
||||
) {
|
||||
return {
|
||||
ActionsAPI: ActionsAPI.default,
|
||||
CompositionAPI: CompositionAPI,
|
||||
EditorAPI: EditorAPI,
|
||||
FaultManagementAPI: FaultManagementAPI,
|
||||
FormsAPI: FormsAPI,
|
||||
IndicatorAPI: IndicatorAPI,
|
||||
IndicatorAPI: IndicatorAPI.default,
|
||||
MenuAPI: MenuAPI.default,
|
||||
NotificationAPI: NotificationAPI.default,
|
||||
ObjectAPI: ObjectAPI,
|
||||
@ -65,6 +70,7 @@ define([
|
||||
TelemetryAPI: TelemetryAPI,
|
||||
TimeAPI: TimeAPI.default,
|
||||
TypeRegistry: TypeRegistry,
|
||||
UserAPI: UserAPI.default
|
||||
UserAPI: UserAPI.default,
|
||||
AnnotationAPI: AnnotationAPI.default
|
||||
};
|
||||
});
|
||||
|
106
src/api/faultmanagement/FaultManagementAPI.js
Normal file
106
src/api/faultmanagement/FaultManagementAPI.js
Normal file
@ -0,0 +1,106 @@
|
||||
/*****************************************************************************
|
||||
* 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": ""
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
*/
|
144
src/api/faultmanagement/FaultManagementAPISpec.js
Normal file
144
src/api/faultmanagement/FaultManagementAPISpec.js
Normal file
@ -0,0 +1,144 @@
|
||||
/*****************************************************************************
|
||||
* 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,5 +1,6 @@
|
||||
import AutoCompleteField from './components/controls/AutoCompleteField.vue';
|
||||
import ClockDisplayFormatField from './components/controls/ClockDisplayFormatField.vue';
|
||||
import CheckBoxField from './components/controls/CheckBoxField.vue';
|
||||
import Datetime from './components/controls/Datetime.vue';
|
||||
import FileInput from './components/controls/FileInput.vue';
|
||||
import Locator from './components/controls/Locator.vue';
|
||||
@ -7,11 +8,13 @@ import NumberField from './components/controls/NumberField.vue';
|
||||
import SelectField from './components/controls/SelectField.vue';
|
||||
import TextAreaField from './components/controls/TextAreaField.vue';
|
||||
import TextField from './components/controls/TextField.vue';
|
||||
import ToggleSwitchField from './components/controls/ToggleSwitchField.vue';
|
||||
|
||||
import Vue from 'vue';
|
||||
|
||||
export const DEFAULT_CONTROLS_MAP = {
|
||||
'autocomplete': AutoCompleteField,
|
||||
'checkbox': CheckBoxField,
|
||||
'composite': ClockDisplayFormatField,
|
||||
'datetime': Datetime,
|
||||
'file-input': FileInput,
|
||||
@ -19,7 +22,8 @@ export const DEFAULT_CONTROLS_MAP = {
|
||||
'numberfield': NumberField,
|
||||
'select': SelectField,
|
||||
'textarea': TextAreaField,
|
||||
'textfield': TextField
|
||||
'textfield': TextField,
|
||||
'toggleSwitch': ToggleSwitchField
|
||||
};
|
||||
|
||||
export default class FormControl {
|
||||
@ -94,4 +98,3 @@ export default class FormControl {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -23,10 +23,13 @@
|
||||
import FormController from './FormController';
|
||||
import FormProperties from './components/FormProperties.vue';
|
||||
|
||||
import EventEmitter from 'EventEmitter';
|
||||
import Vue from 'vue';
|
||||
|
||||
export default class FormsAPI {
|
||||
export default class FormsAPI extends EventEmitter {
|
||||
constructor(openmct) {
|
||||
super();
|
||||
|
||||
this.openmct = openmct;
|
||||
this.formController = new FormController(openmct);
|
||||
}
|
||||
@ -107,6 +110,8 @@ export default class FormsAPI {
|
||||
let onDismiss;
|
||||
let onSave;
|
||||
|
||||
const self = this;
|
||||
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
onSave = onFormSave(resolve);
|
||||
onDismiss = onFormDismiss(reject);
|
||||
@ -115,7 +120,7 @@ export default class FormsAPI {
|
||||
const vm = new Vue({
|
||||
components: { FormProperties },
|
||||
provide: {
|
||||
openmct: this.openmct
|
||||
openmct: self.openmct
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@ -132,7 +137,7 @@ export default class FormsAPI {
|
||||
if (element) {
|
||||
element.append(formElement);
|
||||
} else {
|
||||
overlay = this.openmct.overlays.overlay({
|
||||
overlay = self.openmct.overlays.overlay({
|
||||
element: vm.$el,
|
||||
size: 'small',
|
||||
onDestroy: () => vm.$destroy()
|
||||
@ -140,6 +145,7 @@ export default class FormsAPI {
|
||||
}
|
||||
|
||||
function onFormPropertyChange(data) {
|
||||
self.emit('onFormPropertyChange', data);
|
||||
if (onChange) {
|
||||
onChange(data);
|
||||
}
|
||||
|
157
src/api/forms/FormsAPISpec.js
Normal file
157
src/api/forms/FormsAPISpec.js
Normal file
@ -0,0 +1,157 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
import { createOpenMct, resetApplicationState } from '../../utils/testing';
|
||||
|
||||
describe('The Forms API', () => {
|
||||
let openmct;
|
||||
let element;
|
||||
|
||||
beforeEach((done) => {
|
||||
element = document.createElement('div');
|
||||
element.style.display = 'block';
|
||||
element.style.width = '1920px';
|
||||
element.style.height = '1080px';
|
||||
|
||||
openmct = createOpenMct();
|
||||
openmct.on('start', done);
|
||||
|
||||
openmct.startHeadless(element);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
return resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
it('openmct supports form API', () => {
|
||||
expect(openmct.forms).not.toBe(null);
|
||||
});
|
||||
|
||||
describe('check default form controls exists', () => {
|
||||
it('autocomplete', () => {
|
||||
const control = openmct.forms.getFormControl('autocomplete');
|
||||
expect(control).not.toBe(null);
|
||||
});
|
||||
|
||||
it('clock', () => {
|
||||
const control = openmct.forms.getFormControl('composite');
|
||||
expect(control).not.toBe(null);
|
||||
});
|
||||
|
||||
it('datetime', () => {
|
||||
const control = openmct.forms.getFormControl('datetime');
|
||||
expect(control).not.toBe(null);
|
||||
});
|
||||
|
||||
it('file-input', () => {
|
||||
const control = openmct.forms.getFormControl('file-input');
|
||||
expect(control).not.toBe(null);
|
||||
});
|
||||
|
||||
it('locator', () => {
|
||||
const control = openmct.forms.getFormControl('locator');
|
||||
expect(control).not.toBe(null);
|
||||
});
|
||||
|
||||
it('numberfield', () => {
|
||||
const control = openmct.forms.getFormControl('numberfield');
|
||||
expect(control).not.toBe(null);
|
||||
});
|
||||
|
||||
it('select', () => {
|
||||
const control = openmct.forms.getFormControl('select');
|
||||
expect(control).not.toBe(null);
|
||||
});
|
||||
|
||||
it('textarea', () => {
|
||||
const control = openmct.forms.getFormControl('textarea');
|
||||
expect(control).not.toBe(null);
|
||||
});
|
||||
|
||||
it('textfield', () => {
|
||||
const control = openmct.forms.getFormControl('textfield');
|
||||
expect(control).not.toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
it('supports user defined form controls', () => {
|
||||
const newFormControl = {
|
||||
show: () => {
|
||||
console.log('show new control');
|
||||
},
|
||||
destroy: () => {
|
||||
console.log('destroy');
|
||||
}
|
||||
};
|
||||
openmct.forms.addNewFormControl('newFormControl', newFormControl);
|
||||
const control = openmct.forms.getFormControl('newFormControl');
|
||||
expect(control).not.toBe(null);
|
||||
expect(control.show).not.toBe(null);
|
||||
expect(control.destroy).not.toBe(null);
|
||||
});
|
||||
|
||||
describe('show form on UI', () => {
|
||||
let formStructure;
|
||||
|
||||
beforeEach(() => {
|
||||
formStructure = {
|
||||
title: 'Test Show Form',
|
||||
sections: [
|
||||
{
|
||||
rows: [
|
||||
{
|
||||
key: 'name',
|
||||
control: 'textfield',
|
||||
name: 'Title',
|
||||
pattern: '\\S+',
|
||||
required: false,
|
||||
cssClass: 'l-input-lg',
|
||||
value: 'Test Name'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
it('when container element is provided', (done) => {
|
||||
openmct.forms.showForm(formStructure, { element }).catch(() => {
|
||||
done();
|
||||
});
|
||||
const titleElement = element.querySelector('.c-overlay__dialog-title');
|
||||
expect(titleElement.textContent).toBe(formStructure.title);
|
||||
|
||||
element.querySelector('.js-cancel-button').click();
|
||||
});
|
||||
|
||||
it('when container element is not provided', (done) => {
|
||||
openmct.forms.showForm(formStructure).catch(() => {
|
||||
done();
|
||||
});
|
||||
|
||||
const titleElement = document.querySelector('.c-overlay__dialog-title');
|
||||
const title = titleElement.textContent;
|
||||
|
||||
expect(title).toBe(formStructure.title);
|
||||
document.querySelector('.js-cancel-button').click();
|
||||
});
|
||||
});
|
||||
});
|
@ -21,9 +21,9 @@
|
||||
*****************************************************************************/
|
||||
|
||||
<template>
|
||||
<div class="c-form">
|
||||
<div class="c-form js-form">
|
||||
<div class="c-overlay__top-bar c-form__top-bar">
|
||||
<div class="c-overlay__dialog-title">{{ model.title }}</div>
|
||||
<div class="c-overlay__dialog-title js-form-title">{{ model.title }}</div>
|
||||
<div class="c-overlay__dialog-hint hint">All fields marked <span class="req icon-asterisk"></span> are required.</div>
|
||||
</div>
|
||||
<form
|
||||
@ -44,18 +44,14 @@
|
||||
>
|
||||
{{ section.name }}
|
||||
</h2>
|
||||
<div
|
||||
<FormRow
|
||||
v-for="(row, index) in section.rows"
|
||||
:key="row.id"
|
||||
class="u-contents"
|
||||
>
|
||||
<FormRow
|
||||
:css-class="section.cssClass"
|
||||
:first="index < 1"
|
||||
:row="row"
|
||||
@onChange="onChange"
|
||||
/>
|
||||
</div>
|
||||
:css-class="row.cssClass"
|
||||
:first="index < 1"
|
||||
:row="row"
|
||||
@onChange="onChange"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@ -64,13 +60,15 @@
|
||||
tabindex="0"
|
||||
:disabled="isInvalid"
|
||||
class="c-button c-button--major"
|
||||
aria-label="Save"
|
||||
@click="onSave"
|
||||
>
|
||||
{{ submitLabel }}
|
||||
</button>
|
||||
<button
|
||||
tabindex="0"
|
||||
class="c-button"
|
||||
class="c-button js-cancel-button"
|
||||
aria-label="Cancel"
|
||||
@click="onDismiss"
|
||||
>
|
||||
{{ cancelLabel }}
|
||||
@ -81,7 +79,7 @@
|
||||
|
||||
<script>
|
||||
import FormRow from "@/api/forms/components/FormRow.vue";
|
||||
import uuid from 'uuid';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -23,7 +23,10 @@
|
||||
<template>
|
||||
<div
|
||||
class="form-row c-form__row"
|
||||
:class="[{ 'first': first }]"
|
||||
:class="[
|
||||
{ 'first': first },
|
||||
cssClass
|
||||
]"
|
||||
@onChange="onChange"
|
||||
>
|
||||
<div
|
||||
@ -34,7 +37,7 @@
|
||||
</div>
|
||||
<div
|
||||
class="c-form-row__state-indicator"
|
||||
:class="rowClass"
|
||||
:class="reqClass"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
@ -76,22 +79,22 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
rowClass() {
|
||||
let cssClass = this.cssClass;
|
||||
reqClass() {
|
||||
let reqClass = 'req';
|
||||
|
||||
if (this.row.required) {
|
||||
cssClass = `${cssClass} req`;
|
||||
if (!this.row.required) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.visited && this.valid !== undefined) {
|
||||
if (this.valid === true) {
|
||||
cssClass = `${cssClass} valid`;
|
||||
reqClass = 'valid';
|
||||
} else {
|
||||
cssClass = `${cssClass} invalid`;
|
||||
reqClass = 'invalid';
|
||||
}
|
||||
}
|
||||
|
||||
return cssClass;
|
||||
return reqClass;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
@ -19,35 +19,47 @@
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
<template>
|
||||
<div class="form-control autocomplete">
|
||||
<span class="autocompleteInputAndArrow">
|
||||
<div
|
||||
ref="autoCompleteForm"
|
||||
class="form-control c-input--autocomplete js-autocomplete"
|
||||
>
|
||||
<div
|
||||
class="c-input--autocomplete__wrapper"
|
||||
>
|
||||
<input
|
||||
ref="autoCompleteInput"
|
||||
v-model="field"
|
||||
class="autocompleteInput"
|
||||
class="c-input--autocomplete__input js-autocomplete__input"
|
||||
type="text"
|
||||
:placeholder="placeHolderText"
|
||||
@click="inputClicked()"
|
||||
@keydown="keyDown($event)"
|
||||
>
|
||||
<span
|
||||
class="icon-arrow-down"
|
||||
<div
|
||||
class="icon-arrow-down c-icon-button c-input--autocomplete__afford-arrow js-autocomplete__afford-arrow"
|
||||
@click="arrowClicked()"
|
||||
></span>
|
||||
</span>
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
class="autocompleteOptions"
|
||||
v-if="!hideOptions"
|
||||
class="c-menu c-input--autocomplete__options"
|
||||
aria-label="Autocomplete Options"
|
||||
@blur="hideOptions = true"
|
||||
>
|
||||
<ul v-if="!hideOptions">
|
||||
<ul>
|
||||
<li
|
||||
v-for="opt in filteredOptions"
|
||||
:key="opt.optionId"
|
||||
:class="{'optionPreSelected': optionIndex === opt.optionId}"
|
||||
:class="[
|
||||
{'optionPreSelected': optionIndex === opt.optionId},
|
||||
itemCssClass
|
||||
]"
|
||||
:style="itemStyle(opt)"
|
||||
@click="fillInputWithString(opt.name)"
|
||||
@mouseover="optionMouseover(opt.optionId)"
|
||||
>
|
||||
<span class="optionText">{{ opt.name }}</span>
|
||||
{{ opt.name }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@ -65,7 +77,23 @@ export default {
|
||||
props: {
|
||||
model: {
|
||||
type: Object,
|
||||
required: true
|
||||
required: true,
|
||||
default() {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
placeHolderText: {
|
||||
type: String,
|
||||
default() {
|
||||
return "";
|
||||
}
|
||||
},
|
||||
itemCssClass: {
|
||||
type: String,
|
||||
required: false,
|
||||
default() {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@ -78,31 +106,40 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
filteredOptions() {
|
||||
const options = this.optionNames || [];
|
||||
const fullOptions = this.options || [];
|
||||
if (this.showFilteredOptions) {
|
||||
return options
|
||||
const optionsFiltered = fullOptions
|
||||
.filter(option => {
|
||||
return option.toLowerCase().indexOf(this.field.toLowerCase()) >= 0;
|
||||
if (option.name && this.field) {
|
||||
return option.name.toLowerCase().indexOf(this.field.toLowerCase()) >= 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
}).map((option, index) => {
|
||||
return {
|
||||
optionId: index,
|
||||
name: option
|
||||
name: option.name,
|
||||
color: option.color
|
||||
};
|
||||
});
|
||||
|
||||
return optionsFiltered;
|
||||
}
|
||||
|
||||
return options.map((option, index) => {
|
||||
const optionsFiltered = fullOptions.map((option, index) => {
|
||||
return {
|
||||
optionId: index,
|
||||
name: option
|
||||
name: option.name,
|
||||
color: option.color
|
||||
};
|
||||
});
|
||||
|
||||
return optionsFiltered;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
field(newValue, oldValue) {
|
||||
if (newValue !== oldValue) {
|
||||
|
||||
const data = {
|
||||
model: this.model,
|
||||
value: newValue
|
||||
@ -123,17 +160,17 @@ export default {
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.options = this.model.options;
|
||||
this.autocompleteInputAndArrow = this.$el.getElementsByClassName('autocompleteInputAndArrow')[0];
|
||||
this.autocompleteInputElement = this.$el.getElementsByClassName('autocompleteInput')[0];
|
||||
if (this.options[0].name) {
|
||||
// If "options" include name, value pair
|
||||
this.optionNames = this.options.map((opt) => {
|
||||
return opt.name;
|
||||
this.autocompleteInputAndArrow = this.$refs.autoCompleteForm;
|
||||
this.autocompleteInputElement = this.$refs.autoCompleteInput;
|
||||
if (this.model.options && this.model.options.length && !this.model.options[0].name) {
|
||||
// If options is only an array of string.
|
||||
this.options = this.model.options.map((option) => {
|
||||
return {
|
||||
name: option
|
||||
};
|
||||
});
|
||||
} else {
|
||||
// If options is only an array of string.
|
||||
this.optionNames = this.options;
|
||||
this.options = this.model.options;
|
||||
}
|
||||
},
|
||||
destroyed() {
|
||||
@ -222,6 +259,12 @@ export default {
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
itemStyle(option) {
|
||||
if (option.color) {
|
||||
|
||||
return { '--optionIconColor': option.color };
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
55
src/api/forms/components/controls/CheckBoxField.vue
Normal file
55
src/api/forms/components/controls/CheckBoxField.vue
Normal file
@ -0,0 +1,55 @@
|
||||
/*****************************************************************************
|
||||
* 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,6 +40,12 @@
|
||||
>
|
||||
{{ name }}
|
||||
</button>
|
||||
<button
|
||||
v-if="removable"
|
||||
class="c-button icon-trash"
|
||||
title="Remove file"
|
||||
@click="removeFile"
|
||||
></button>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
@ -63,6 +69,9 @@ export default {
|
||||
const fileInfo = this.fileInfo || this.model.value;
|
||||
|
||||
return fileInfo && fileInfo.name || this.model.text;
|
||||
},
|
||||
removable() {
|
||||
return (this.fileInfo || this.model.value) && this.model.removable;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@ -97,6 +106,15 @@ export default {
|
||||
},
|
||||
selectFile() {
|
||||
this.$refs.fileInput.click();
|
||||
},
|
||||
removeFile() {
|
||||
this.model.value = undefined;
|
||||
this.fileInfo = undefined;
|
||||
const data = {
|
||||
model: this.model,
|
||||
value: undefined
|
||||
};
|
||||
this.$emit('onChange', data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -28,6 +28,7 @@
|
||||
>
|
||||
<input
|
||||
v-model="field"
|
||||
:aria-label="model.name"
|
||||
type="number"
|
||||
:min="model.min"
|
||||
:max="model.max"
|
||||
@ -58,7 +59,6 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
updateText() {
|
||||
console.log('updateText', this.field);
|
||||
const data = {
|
||||
model: this.model,
|
||||
value: this.field
|
||||
|
62
src/api/forms/components/controls/ToggleSwitchField.vue
Normal file
62
src/api/forms/components/controls/ToggleSwitchField.vue
Normal file
@ -0,0 +1,62 @@
|
||||
/*****************************************************************************
|
||||
* 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>
|
19
src/api/forms/toggle-check-box-mixin.js
Normal file
19
src/api/forms/toggle-check-box-mixin.js
Normal file
@ -0,0 +1,19 @@
|
||||
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
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
define([
|
||||
'./SimpleIndicator',
|
||||
'lodash'
|
||||
], function (
|
||||
SimpleIndicator,
|
||||
_
|
||||
) {
|
||||
function IndicatorAPI(openmct) {
|
||||
|
||||
import EventEmitter from "EventEmitter";
|
||||
import SimpleIndicator from "./SimpleIndicator";
|
||||
|
||||
class IndicatorAPI extends EventEmitter {
|
||||
constructor(openmct) {
|
||||
super();
|
||||
|
||||
this.openmct = openmct;
|
||||
this.indicatorObjects = [];
|
||||
}
|
||||
|
||||
IndicatorAPI.prototype.getIndicatorObjectsByPriority = function () {
|
||||
getIndicatorObjectsByPriority() {
|
||||
const sortedIndicators = this.indicatorObjects.sort((a, b) => b.priority - a.priority);
|
||||
|
||||
return sortedIndicators;
|
||||
};
|
||||
}
|
||||
|
||||
IndicatorAPI.prototype.simpleIndicator = function () {
|
||||
simpleIndicator() {
|
||||
return new SimpleIndicator(this.openmct);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepts an indicator object, which is a simple object
|
||||
@ -62,14 +62,16 @@ define([
|
||||
* myIndicator.iconClass("icon-info");
|
||||
*
|
||||
*/
|
||||
IndicatorAPI.prototype.add = function (indicator) {
|
||||
add(indicator) {
|
||||
if (!indicator.priority) {
|
||||
indicator.priority = this.openmct.priority.DEFAULT;
|
||||
}
|
||||
|
||||
this.indicatorObjects.push(indicator);
|
||||
};
|
||||
|
||||
return IndicatorAPI;
|
||||
this.emit('addIndicator', indicator);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
export default IndicatorAPI;
|
||||
|
@ -20,82 +20,101 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
define(['zepto', './res/indicator-template.html'],
|
||||
function ($, indicatorTemplate) {
|
||||
const DEFAULT_ICON_CLASS = 'icon-info';
|
||||
import EventEmitter from 'EventEmitter';
|
||||
import indicatorTemplate from './res/indicator-template.html';
|
||||
import { convertTemplateToHTML } from '@/utils/template/templateHelpers';
|
||||
|
||||
function SimpleIndicator(openmct) {
|
||||
this.openmct = openmct;
|
||||
this.element = $(indicatorTemplate)[0];
|
||||
this.priority = openmct.priority.DEFAULT;
|
||||
const DEFAULT_ICON_CLASS = 'icon-info';
|
||||
|
||||
this.textElement = this.element.querySelector('.js-indicator-text');
|
||||
class SimpleIndicator extends EventEmitter {
|
||||
constructor(openmct) {
|
||||
super();
|
||||
|
||||
//Set defaults
|
||||
this.text('New Indicator');
|
||||
this.description('');
|
||||
this.iconClass(DEFAULT_ICON_CLASS);
|
||||
this.statusClass('');
|
||||
this.openmct = openmct;
|
||||
this.element = convertTemplateToHTML(indicatorTemplate)[0];
|
||||
this.priority = openmct.priority.DEFAULT;
|
||||
|
||||
this.textElement = this.element.querySelector('.js-indicator-text');
|
||||
|
||||
//Set defaults
|
||||
this.text('New Indicator');
|
||||
this.description('');
|
||||
this.iconClass(DEFAULT_ICON_CLASS);
|
||||
|
||||
this.click = this.click.bind(this);
|
||||
|
||||
this.element.addEventListener('click', this.click);
|
||||
openmct.once('destroy', () => {
|
||||
this.removeAllListeners();
|
||||
this.element.removeEventListener('click', this.click);
|
||||
});
|
||||
}
|
||||
|
||||
text(text) {
|
||||
if (text !== undefined && text !== this.textValue) {
|
||||
this.textValue = text;
|
||||
this.textElement.innerText = text;
|
||||
|
||||
if (!text) {
|
||||
this.element.classList.add('hidden');
|
||||
} else {
|
||||
this.element.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
SimpleIndicator.prototype.text = function (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.description = function (description) {
|
||||
if (description !== undefined && description !== this.descriptionValue) {
|
||||
this.descriptionValue = description;
|
||||
this.element.title = description;
|
||||
}
|
||||
|
||||
return this.descriptionValue;
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
if (iconClass) {
|
||||
this.element.classList.add(iconClass);
|
||||
}
|
||||
|
||||
this.iconClassValue = iconClass;
|
||||
}
|
||||
|
||||
return this.iconClassValue;
|
||||
};
|
||||
|
||||
SimpleIndicator.prototype.statusClass = function (statusClass) {
|
||||
if (statusClass !== undefined && statusClass !== this.statusClassValue) {
|
||||
if (this.statusClassValue) {
|
||||
this.element.classList.remove(this.statusClassValue);
|
||||
}
|
||||
|
||||
if (statusClass) {
|
||||
this.element.classList.add(statusClass);
|
||||
}
|
||||
|
||||
this.statusClassValue = statusClass;
|
||||
}
|
||||
|
||||
return this.statusClassValue;
|
||||
};
|
||||
|
||||
return SimpleIndicator;
|
||||
return this.textValue;
|
||||
}
|
||||
);
|
||||
|
||||
description(description) {
|
||||
if (description !== undefined && description !== this.descriptionValue) {
|
||||
this.descriptionValue = description;
|
||||
this.element.title = description;
|
||||
}
|
||||
|
||||
return this.descriptionValue;
|
||||
}
|
||||
|
||||
iconClass(iconClass) {
|
||||
if (iconClass !== undefined && iconClass !== this.iconClassValue) {
|
||||
// element.classList is precious and throws errors if you try and add
|
||||
// or remove empty strings
|
||||
if (this.iconClassValue) {
|
||||
this.element.classList.remove(this.iconClassValue);
|
||||
}
|
||||
|
||||
if (iconClass) {
|
||||
this.element.classList.add(iconClass);
|
||||
}
|
||||
|
||||
this.iconClassValue = iconClass;
|
||||
}
|
||||
|
||||
return this.iconClassValue;
|
||||
}
|
||||
|
||||
statusClass(statusClass) {
|
||||
if (arguments.length === 1 && statusClass !== this.statusClassValue) {
|
||||
if (this.statusClassValue) {
|
||||
this.element.classList.remove(this.statusClassValue);
|
||||
}
|
||||
|
||||
if (statusClass !== undefined) {
|
||||
this.element.classList.add(statusClass);
|
||||
}
|
||||
|
||||
this.statusClassValue = statusClass;
|
||||
}
|
||||
|
||||
return this.statusClassValue;
|
||||
}
|
||||
|
||||
click(event) {
|
||||
this.emit('click', event);
|
||||
}
|
||||
|
||||
getElement() {
|
||||
return this.element;
|
||||
}
|
||||
}
|
||||
|
||||
export default SimpleIndicator;
|
||||
|
@ -26,29 +26,31 @@ import { createOpenMct, createMouseEvent, resetApplicationState } from '../../ut
|
||||
|
||||
describe ('The Menu API', () => {
|
||||
let openmct;
|
||||
let element;
|
||||
let appHolder;
|
||||
let menuAPI;
|
||||
let actionsArray;
|
||||
let x;
|
||||
let y;
|
||||
let result;
|
||||
let onDestroy;
|
||||
let menuElement;
|
||||
|
||||
const x = 8;
|
||||
const y = 16;
|
||||
|
||||
const menuOptions = {
|
||||
onDestroy: () => {
|
||||
console.log('default onDestroy');
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach((done) => {
|
||||
const appHolder = document.createElement('div');
|
||||
appHolder = document.createElement('div');
|
||||
appHolder.style.display = 'block';
|
||||
appHolder.style.width = '1920px';
|
||||
appHolder.style.height = '1080px';
|
||||
|
||||
openmct = createOpenMct();
|
||||
|
||||
element = document.createElement('div');
|
||||
element.style.display = 'block';
|
||||
element.style.width = '1920px';
|
||||
element.style.height = '1080px';
|
||||
|
||||
openmct.on('start', done);
|
||||
openmct.startHeadless(appHolder);
|
||||
openmct.startHeadless();
|
||||
|
||||
menuAPI = new MenuAPI(openmct);
|
||||
actionsArray = [
|
||||
@ -56,7 +58,7 @@ describe ('The Menu API', () => {
|
||||
key: 'test-css-class-1',
|
||||
name: 'Test Action 1',
|
||||
cssClass: 'icon-clock',
|
||||
description: 'This is a test action',
|
||||
description: 'This is a test action 1',
|
||||
onItemClicked: () => {
|
||||
result = 'Test Action 1 Invoked';
|
||||
}
|
||||
@ -65,149 +67,165 @@ describe ('The Menu API', () => {
|
||||
key: 'test-css-class-2',
|
||||
name: 'Test Action 2',
|
||||
cssClass: 'icon-clock',
|
||||
description: 'This is a test action',
|
||||
description: 'This is a test action 2',
|
||||
onItemClicked: () => {
|
||||
result = 'Test Action 2 Invoked';
|
||||
}
|
||||
}
|
||||
];
|
||||
x = 8;
|
||||
y = 16;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
return resetApplicationState(openmct);
|
||||
});
|
||||
|
||||
describe("showMenu method", () => {
|
||||
it("creates an instance of Menu when invoked", () => {
|
||||
menuAPI.showMenu(x, y, actionsArray);
|
||||
|
||||
expect(menuAPI.menuComponent).toBeInstanceOf(Menu);
|
||||
describe('showMenu method', () => {
|
||||
beforeAll(() => {
|
||||
spyOn(menuOptions, 'onDestroy').and.callThrough();
|
||||
});
|
||||
|
||||
describe("creates a menu component", () => {
|
||||
let menuComponent;
|
||||
let vueComponent;
|
||||
it('creates an instance of Menu when invoked', (done) => {
|
||||
menuOptions.onDestroy = done;
|
||||
|
||||
beforeEach(() => {
|
||||
onDestroy = jasmine.createSpy('onDestroy');
|
||||
menuAPI.showMenu(x, y, actionsArray, menuOptions);
|
||||
|
||||
const menuOptions = {
|
||||
onDestroy
|
||||
};
|
||||
expect(menuAPI.menuComponent).toBeInstanceOf(Menu);
|
||||
document.body.click();
|
||||
});
|
||||
|
||||
describe('creates a menu component', () => {
|
||||
it('with all the actions passed in', (done) => {
|
||||
menuOptions.onDestroy = done;
|
||||
|
||||
menuAPI.showMenu(x, y, actionsArray, menuOptions);
|
||||
vueComponent = menuAPI.menuComponent.component;
|
||||
menuComponent = document.querySelector(".c-menu");
|
||||
menuElement = document.querySelector('.c-menu');
|
||||
expect(menuElement).toBeDefined();
|
||||
|
||||
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;
|
||||
const listItems = menuElement.children[0].children;
|
||||
|
||||
expect(listItems.length).toEqual(actionsArray.length);
|
||||
document.body.click();
|
||||
});
|
||||
|
||||
it("with click-able menu items, that will invoke the correct callBacks", () => {
|
||||
let listItem1 = menuComponent.children[0].children[0];
|
||||
it('with click-able menu items, that will invoke the correct callBack', (done) => {
|
||||
menuOptions.onDestroy = done;
|
||||
|
||||
menuAPI.showMenu(x, y, actionsArray, menuOptions);
|
||||
|
||||
menuElement = document.querySelector('.c-menu');
|
||||
const listItem1 = menuElement.children[0].children[0];
|
||||
|
||||
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", () => {
|
||||
let listItem1 = menuComponent.children[0].children[0];
|
||||
it('dismisses the menu when action is clicked on', (done) => {
|
||||
menuOptions.onDestroy = done;
|
||||
|
||||
menuAPI.showMenu(x, y, actionsArray, menuOptions);
|
||||
|
||||
menuElement = document.querySelector('.c-menu');
|
||||
const listItem1 = menuElement.children[0].children[0];
|
||||
listItem1.click();
|
||||
|
||||
let menu = document.querySelector('.c-menu');
|
||||
menuElement = document.querySelector('.c-menu');
|
||||
|
||||
expect(menu).toBeNull();
|
||||
expect(menuElement).toBeNull();
|
||||
});
|
||||
|
||||
it("invokes the destroy method when menu is dismissed", () => {
|
||||
it('invokes the destroy method when menu is dismissed', (done) => {
|
||||
menuOptions.onDestroy = done;
|
||||
|
||||
menuAPI.showMenu(x, y, actionsArray, menuOptions);
|
||||
|
||||
const vueComponent = menuAPI.menuComponent.component;
|
||||
spyOn(vueComponent, '$destroy');
|
||||
|
||||
document.body.click();
|
||||
|
||||
expect(vueComponent.$destroy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("invokes the onDestroy callback if passed in", () => {
|
||||
document.body.click();
|
||||
it('invokes the onDestroy callback if passed in', (done) => {
|
||||
let count = 0;
|
||||
menuOptions.onDestroy = () => {
|
||||
count++;
|
||||
expect(count).toEqual(1);
|
||||
done();
|
||||
};
|
||||
|
||||
expect(onDestroy).toHaveBeenCalled();
|
||||
menuAPI.showMenu(x, y, actionsArray, menuOptions);
|
||||
|
||||
document.body.click();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("superMenu method", () => {
|
||||
it("creates a superMenu", () => {
|
||||
menuAPI.showSuperMenu(x, y, actionsArray);
|
||||
describe('superMenu method', () => {
|
||||
it('creates a superMenu', (done) => {
|
||||
menuOptions.onDestroy = done;
|
||||
|
||||
const superMenu = document.querySelector('.c-super-menu__menu');
|
||||
menuAPI.showSuperMenu(x, y, actionsArray, menuOptions);
|
||||
menuElement = document.querySelector('.c-super-menu__menu');
|
||||
|
||||
expect(superMenu).not.toBeNull();
|
||||
expect(menuElement).not.toBeNull();
|
||||
document.body.click();
|
||||
});
|
||||
|
||||
it("Mouse over a superMenu shows correct description", (done) => {
|
||||
menuAPI.showSuperMenu(x, y, actionsArray);
|
||||
it('Mouse over a superMenu shows correct description', (done) => {
|
||||
menuOptions.onDestroy = done;
|
||||
|
||||
const superMenu = document.querySelector('.c-super-menu__menu');
|
||||
const superMenuItem = superMenu.querySelector('li');
|
||||
menuAPI.showSuperMenu(x, y, actionsArray, menuOptions);
|
||||
menuElement = document.querySelector('.c-super-menu__menu');
|
||||
|
||||
const superMenuItem = menuElement.querySelector('li');
|
||||
const mouseOverEvent = createMouseEvent('mouseover');
|
||||
|
||||
superMenuItem.dispatchEvent(mouseOverEvent);
|
||||
const itemDescription = document.querySelector('.l-item-description__description');
|
||||
|
||||
setTimeout(() => {
|
||||
menuAPI.menuComponent.component.$nextTick(() => {
|
||||
expect(menuElement).not.toBeNull();
|
||||
expect(itemDescription.innerText).toEqual(actionsArray[0].description);
|
||||
expect(superMenu).not.toBeNull();
|
||||
done();
|
||||
}, 300);
|
||||
|
||||
document.body.click();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Menu Placements", () => {
|
||||
it("default menu position BOTTOM_RIGHT", () => {
|
||||
menuAPI.showMenu(x, y, actionsArray);
|
||||
|
||||
const menu = document.querySelector('.c-menu');
|
||||
|
||||
const boundingClientRect = menu.getBoundingClientRect();
|
||||
const left = boundingClientRect.left;
|
||||
const top = boundingClientRect.top;
|
||||
|
||||
expect(left).toEqual(x);
|
||||
expect(top).toEqual(y);
|
||||
});
|
||||
|
||||
it("menu position BOTTOM_RIGHT", () => {
|
||||
const menuOptions = {
|
||||
placement: openmct.menus.menuPlacement.BOTTOM_RIGHT
|
||||
};
|
||||
describe('Menu Placements', () => {
|
||||
it('default menu position BOTTOM_RIGHT', (done) => {
|
||||
menuOptions.onDestroy = done;
|
||||
|
||||
menuAPI.showMenu(x, y, actionsArray, menuOptions);
|
||||
menuElement = document.querySelector('.c-menu');
|
||||
|
||||
const menu = document.querySelector('.c-menu');
|
||||
const boundingClientRect = menu.getBoundingClientRect();
|
||||
const boundingClientRect = menuElement.getBoundingClientRect();
|
||||
const left = boundingClientRect.left;
|
||||
const top = boundingClientRect.top;
|
||||
|
||||
expect(left).toEqual(x);
|
||||
expect(top).toEqual(y);
|
||||
|
||||
document.body.click();
|
||||
});
|
||||
|
||||
it('menu position BOTTOM_RIGHT', (done) => {
|
||||
menuOptions.onDestroy = done;
|
||||
menuOptions.placement = openmct.menus.menuPlacement.BOTTOM_RIGHT;
|
||||
|
||||
menuAPI.showMenu(x, y, actionsArray, menuOptions);
|
||||
menuElement = document.querySelector('.c-menu');
|
||||
|
||||
const boundingClientRect = menuElement.getBoundingClientRect();
|
||||
const left = boundingClientRect.left;
|
||||
const top = boundingClientRect.top;
|
||||
|
||||
expect(left).toEqual(x);
|
||||
expect(top).toEqual(y);
|
||||
|
||||
document.body.click();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -12,6 +12,7 @@
|
||||
:key="action.name"
|
||||
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
|
||||
:title="action.description"
|
||||
:data-testid="action.testId || false"
|
||||
@click="action.onItemClicked"
|
||||
>
|
||||
{{ action.name }}
|
||||
@ -35,8 +36,9 @@
|
||||
<li
|
||||
v-for="action in options.actions"
|
||||
:key="action.name"
|
||||
:class="action.cssClass"
|
||||
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
|
||||
:title="action.description"
|
||||
:data-testid="action.testId || false"
|
||||
@click="action.onItemClicked"
|
||||
>
|
||||
{{ action.name }}
|
||||
|
@ -15,6 +15,7 @@
|
||||
:key="action.name"
|
||||
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
|
||||
:title="action.description"
|
||||
:data-testid="action.testId || false"
|
||||
@click="action.onItemClicked"
|
||||
@mouseover="toggleItemDescription(action)"
|
||||
@mouseleave="toggleItemDescription()"
|
||||
@ -45,6 +46,7 @@
|
||||
:key="action.name"
|
||||
:class="action.cssClass"
|
||||
:title="action.description"
|
||||
:data-testid="action.testId || false"
|
||||
@click="action.onItemClicked"
|
||||
@mouseover="toggleItemDescription(action)"
|
||||
@mouseleave="toggleItemDescription()"
|
||||
|
@ -20,7 +20,7 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import uuid from 'uuid';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
class InMemorySearchProvider {
|
||||
/**
|
||||
@ -36,13 +36,13 @@ class InMemorySearchProvider {
|
||||
*/
|
||||
this.MAX_CONCURRENT_REQUESTS = 100;
|
||||
/**
|
||||
* If max results is not specified in query, use this as default.
|
||||
*/
|
||||
* If max results is not specified in query, use this as default.
|
||||
*/
|
||||
this.DEFAULT_MAX_RESULTS = 100;
|
||||
|
||||
this.openmct = openmct;
|
||||
|
||||
this.indexedIds = {};
|
||||
this.indexedCompositions = {};
|
||||
this.indexedTags = {};
|
||||
this.idsToIndex = [];
|
||||
this.pendingIndex = {};
|
||||
this.pendingRequests = 0;
|
||||
@ -51,14 +51,20 @@ class InMemorySearchProvider {
|
||||
/**
|
||||
* If we don't have SharedWorkers available (e.g., iOS)
|
||||
*/
|
||||
this.localIndexedItems = {};
|
||||
this.localIndexedDomainObjects = {};
|
||||
this.localIndexedAnnotationsByDomainObject = {};
|
||||
this.localIndexedAnnotationsByTag = {};
|
||||
|
||||
this.pendingQueries = {};
|
||||
this.onWorkerMessage = this.onWorkerMessage.bind(this);
|
||||
this.onWorkerMessageError = this.onWorkerMessageError.bind(this);
|
||||
this.localSearchForObjects = this.localSearchForObjects.bind(this);
|
||||
this.localSearchForAnnotations = this.localSearchForAnnotations.bind(this);
|
||||
this.localSearchForTags = this.localSearchForTags.bind(this);
|
||||
this.localSearchForNotebookAnnotations = this.localSearchForNotebookAnnotations.bind(this);
|
||||
this.onAnnotationCreation = this.onAnnotationCreation.bind(this);
|
||||
this.onerror = this.onWorkerError.bind(this);
|
||||
this.startIndexing = this.startIndexing.bind(this);
|
||||
this.onMutationOfIndexedObject = this.onMutationOfIndexedObject.bind(this);
|
||||
|
||||
this.openmct.on('start', this.startIndexing);
|
||||
this.openmct.on('destroy', () => {
|
||||
@ -68,18 +74,47 @@ class InMemorySearchProvider {
|
||||
this.worker.port.onmessageerror = null;
|
||||
this.worker.port.close();
|
||||
}
|
||||
|
||||
this.destroyObservers(this.indexedIds);
|
||||
this.destroyObservers(this.indexedCompositions);
|
||||
});
|
||||
}
|
||||
|
||||
startIndexing() {
|
||||
const rootObject = this.openmct.objects.rootProvider.rootObject;
|
||||
|
||||
this.searchTypes = this.openmct.objects.SEARCH_TYPES;
|
||||
|
||||
this.supportedSearchTypes = [this.searchTypes.OBJECTS, this.searchTypes.ANNOTATIONS, this.searchTypes.NOTEBOOK_ANNOTATIONS, this.searchTypes.TAGS];
|
||||
|
||||
this.scheduleForIndexing(rootObject.identifier);
|
||||
|
||||
this.indexAnnotations();
|
||||
|
||||
if (typeof SharedWorker !== 'undefined') {
|
||||
this.worker = this.startSharedWorker();
|
||||
} else {
|
||||
// we must be on iOS
|
||||
}
|
||||
|
||||
this.openmct.annotation.on('annotationCreated', this.onAnnotationCreation);
|
||||
|
||||
}
|
||||
|
||||
indexAnnotations() {
|
||||
const theInMemorySearchProvider = this;
|
||||
Object.values(this.openmct.objects.providers).forEach(objectProvider => {
|
||||
if (objectProvider.getAllObjects) {
|
||||
const allObjects = objectProvider.getAllObjects();
|
||||
if (allObjects) {
|
||||
Object.values(allObjects).forEach(domainObject => {
|
||||
if (domainObject.type === 'annotation') {
|
||||
theInMemorySearchProvider.scheduleForIndexing(domainObject.identifier);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -95,51 +130,60 @@ class InMemorySearchProvider {
|
||||
return intermediateResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
search(query, searchType) {
|
||||
const queryId = uuid();
|
||||
const pendingQuery = this.getIntermediateResponse();
|
||||
this.pendingQueries[queryId] = pendingQuery;
|
||||
const searchOptions = {
|
||||
queryId,
|
||||
searchType,
|
||||
query,
|
||||
maxResults: this.DEFAULT_MAX_RESULTS
|
||||
};
|
||||
|
||||
if (this.worker) {
|
||||
this.dispatchSearch(queryId, input, maxResults);
|
||||
this.#dispatchSearchToWorker(searchOptions);
|
||||
} else {
|
||||
this.localSearch(queryId, input, maxResults);
|
||||
this.#localQueryFallBack(searchOptions);
|
||||
}
|
||||
|
||||
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. Only really knows how to handle search
|
||||
* results, which are parsed, transformed into a modelResult object, which
|
||||
* is used to resolve the corresponding promise.
|
||||
* Handle messages from the worker.
|
||||
* @private
|
||||
*/
|
||||
async onWorkerMessage(event) {
|
||||
if (event.data.request !== 'search') {
|
||||
return;
|
||||
}
|
||||
|
||||
const pendingQuery = this.pendingQueries[event.data.queryId];
|
||||
const modelResults = {
|
||||
total: event.data.total
|
||||
};
|
||||
modelResults.hits = await Promise.all(event.data.results.map(async (hit) => {
|
||||
const identifier = this.openmct.objects.parseKeyString(hit.keyString);
|
||||
const domainObject = await this.openmct.objects.get(identifier.key);
|
||||
if (hit && hit.keyString) {
|
||||
const identifier = this.openmct.objects.parseKeyString(hit.keyString);
|
||||
const domainObject = await this.openmct.objects.get(identifier);
|
||||
|
||||
return domainObject;
|
||||
return domainObject;
|
||||
}
|
||||
}));
|
||||
|
||||
pendingQuery.resolve(modelResults);
|
||||
@ -180,7 +224,8 @@ class InMemorySearchProvider {
|
||||
|
||||
/**
|
||||
* Schedule an id to be indexed at a later date. If there are less
|
||||
* pending requests then allowed, will kick off an indexing request.
|
||||
* pending requests than the maximum allowed, this will kick off an indexing request.
|
||||
* This is done only when indexing first begins and we need to index a lot of objects.
|
||||
*
|
||||
* @private
|
||||
* @param {identifier} id to be indexed.
|
||||
@ -213,29 +258,75 @@ class InMemorySearchProvider {
|
||||
}
|
||||
}
|
||||
|
||||
onMutationOfIndexedObject(domainObject) {
|
||||
onAnnotationCreation(annotationObject) {
|
||||
|
||||
const objectProvider = this.openmct.objects.getProvider(annotationObject.identifier);
|
||||
if (objectProvider === undefined || objectProvider.search === undefined) {
|
||||
const provider = this;
|
||||
provider.index(annotationObject);
|
||||
}
|
||||
}
|
||||
|
||||
onNameMutation(domainObject, name) {
|
||||
const provider = this;
|
||||
provider.index(domainObject.identifier, domainObject);
|
||||
|
||||
domainObject.name = name;
|
||||
provider.index(domainObject);
|
||||
}
|
||||
|
||||
onTagMutation(domainObject, newTags) {
|
||||
domainObject.tags = newTags;
|
||||
const provider = this;
|
||||
|
||||
provider.index(domainObject);
|
||||
}
|
||||
|
||||
onCompositionMutation(domainObject, composition) {
|
||||
const provider = this;
|
||||
const indexedComposition = domainObject.composition;
|
||||
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 an id and model to the worker to be indexed. If the model has
|
||||
* composition, schedule those ids for later indexing.
|
||||
* Pass a domainObject to the worker to be indexed.
|
||||
* If the object has composition, schedule those ids for later indexing.
|
||||
* Watch for object changes and re-index object and children if so
|
||||
*
|
||||
* @private
|
||||
* @param id a model id
|
||||
* @param model a model
|
||||
* @param domainObject a domainObject
|
||||
*/
|
||||
async index(id, domainObject) {
|
||||
async index(domainObject) {
|
||||
const provider = this;
|
||||
const keyString = this.openmct.objects.makeKeyString(id);
|
||||
const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
|
||||
|
||||
if (!this.indexedIds[keyString]) {
|
||||
this.openmct.objects.observe(domainObject, `*`, this.onMutationOfIndexedObject);
|
||||
this.indexedIds[keyString] = this.openmct.objects.observe(
|
||||
domainObject,
|
||||
'name',
|
||||
this.onNameMutation.bind(this, domainObject)
|
||||
);
|
||||
this.indexedCompositions[keyString] = this.openmct.objects.observe(
|
||||
domainObject,
|
||||
'composition',
|
||||
this.onCompositionMutation.bind(this, domainObject)
|
||||
);
|
||||
if (domainObject.type === 'annotation') {
|
||||
this.indexedTags[keyString] = this.openmct.objects.observe(
|
||||
domainObject,
|
||||
'tags',
|
||||
this.onTagMutation.bind(this, domainObject)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.indexedIds[keyString] = true;
|
||||
|
||||
if ((id.key !== 'ROOT')) {
|
||||
if ((keyString !== 'ROOT')) {
|
||||
if (this.worker) {
|
||||
this.worker.port.postMessage({
|
||||
request: 'index',
|
||||
@ -247,15 +338,12 @@ class InMemorySearchProvider {
|
||||
}
|
||||
}
|
||||
|
||||
const composition = this.openmct.composition.registry.find(foundComposition => {
|
||||
return foundComposition.appliesTo(domainObject);
|
||||
});
|
||||
const composition = this.openmct.composition.get(domainObject);
|
||||
|
||||
if (composition) {
|
||||
const childIdentifiers = await composition.load(domainObject);
|
||||
childIdentifiers.forEach(function (childIdentifier) {
|
||||
provider.scheduleForIndexing(childIdentifier);
|
||||
});
|
||||
if (composition !== undefined) {
|
||||
const children = await composition.load();
|
||||
|
||||
children.forEach(child => provider.scheduleForIndexing(child.identifier));
|
||||
}
|
||||
}
|
||||
|
||||
@ -271,12 +359,12 @@ class InMemorySearchProvider {
|
||||
const provider = this;
|
||||
|
||||
this.pendingRequests += 1;
|
||||
const identifier = await this.openmct.objects.parseKeyString(keyString);
|
||||
const domainObject = await this.openmct.objects.get(identifier.key);
|
||||
const domainObject = await this.openmct.objects.get(keyString);
|
||||
delete provider.pendingIndex[keyString];
|
||||
|
||||
try {
|
||||
if (domainObject) {
|
||||
await provider.index(identifier, domainObject);
|
||||
await provider.index(domainObject);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to index domain object ' + keyString, error);
|
||||
@ -294,26 +382,83 @@ class InMemorySearchProvider {
|
||||
* @private
|
||||
* @returns {String} a unique query Id for the query.
|
||||
*/
|
||||
dispatchSearch(queryId, searchInput, maxResults) {
|
||||
#dispatchSearchToWorker({queryId, searchType, query, maxResults}) {
|
||||
const message = {
|
||||
request: 'search',
|
||||
input: searchInput,
|
||||
request: searchType.toString(),
|
||||
input: query,
|
||||
maxResults,
|
||||
queryId
|
||||
};
|
||||
this.worker.port.postMessage(message);
|
||||
}
|
||||
|
||||
localIndexTags(keyString, objectToIndex, model) {
|
||||
// add new tags
|
||||
model.tags.forEach(tagID => {
|
||||
if (!this.localIndexedAnnotationsByTag[tagID]) {
|
||||
this.localIndexedAnnotationsByTag[tagID] = [];
|
||||
}
|
||||
|
||||
const existsInIndex = this.localIndexedAnnotationsByTag[tagID].some(indexedObject => {
|
||||
return indexedObject.keyString === objectToIndex.keyString;
|
||||
});
|
||||
|
||||
if (!existsInIndex) {
|
||||
this.localIndexedAnnotationsByTag[tagID].push(objectToIndex);
|
||||
}
|
||||
|
||||
});
|
||||
const tagsToRemoveFromIndex = Object.keys(this.localIndexedAnnotationsByTag).filter(indexedTag => {
|
||||
return !(model.tags.includes(indexedTag));
|
||||
});
|
||||
tagsToRemoveFromIndex.forEach(tagToRemoveFromIndex => {
|
||||
this.localIndexedAnnotationsByTag[tagToRemoveFromIndex] = this.localIndexedAnnotationsByTag[tagToRemoveFromIndex].filter(indexedAnnotation => {
|
||||
const shouldKeep = indexedAnnotation.keyString !== keyString;
|
||||
|
||||
return shouldKeep;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
localIndexAnnotation(objectToIndex, model) {
|
||||
Object.keys(model.targets).forEach(targetID => {
|
||||
if (!this.localIndexedAnnotationsByDomainObject[targetID]) {
|
||||
this.localIndexedAnnotationsByDomainObject[targetID] = [];
|
||||
}
|
||||
|
||||
objectToIndex.targets = model.targets;
|
||||
objectToIndex.tags = model.tags;
|
||||
const existsInIndex = this.localIndexedAnnotationsByDomainObject[targetID].some(indexedObject => {
|
||||
return indexedObject.keyString === objectToIndex.keyString;
|
||||
});
|
||||
|
||||
if (!existsInIndex) {
|
||||
this.localIndexedAnnotationsByDomainObject[targetID].push(objectToIndex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A local version of the same SharedWorker function
|
||||
* if we don't have SharedWorkers available (e.g., iOS)
|
||||
*/
|
||||
* A local version of the same SharedWorker function
|
||||
* if we don't have SharedWorkers available (e.g., iOS)
|
||||
*/
|
||||
localIndexItem(keyString, model) {
|
||||
this.localIndexedItems[keyString] = {
|
||||
const objectToIndex = {
|
||||
type: model.type,
|
||||
name: model.name,
|
||||
keyString
|
||||
};
|
||||
if (model && (model.type === 'annotation')) {
|
||||
if (model.targets) {
|
||||
this.localIndexAnnotation(objectToIndex, model);
|
||||
}
|
||||
|
||||
if (model.tags) {
|
||||
this.localIndexTags(keyString, objectToIndex, model);
|
||||
}
|
||||
} else {
|
||||
this.localIndexedDomainObjects[keyString] = objectToIndex;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -323,21 +468,21 @@ class InMemorySearchProvider {
|
||||
* Gets search results from the indexedItems based on provided search
|
||||
* input. Returns matching results from indexedItems
|
||||
*/
|
||||
localSearch(queryId, searchInput, maxResults) {
|
||||
localSearchForObjects(queryId, searchInput, maxResults) {
|
||||
// This results dictionary will have domain object ID keys which
|
||||
// point to the value the domain object's score.
|
||||
let results;
|
||||
let results = [];
|
||||
const input = searchInput.trim().toLowerCase();
|
||||
const message = {
|
||||
request: 'search',
|
||||
results: {},
|
||||
request: 'searchForObjects',
|
||||
results: [],
|
||||
total: 0,
|
||||
queryId
|
||||
};
|
||||
|
||||
results = Object.values(this.localIndexedItems).filter((indexedItem) => {
|
||||
results = Object.values(this.localIndexedDomainObjects).filter((indexedItem) => {
|
||||
return indexedItem.name.toLowerCase().includes(input);
|
||||
});
|
||||
}) || [];
|
||||
|
||||
message.total = results.length;
|
||||
message.results = results
|
||||
@ -347,6 +492,117 @@ class InMemorySearchProvider {
|
||||
};
|
||||
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];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default InMemorySearchProvider;
|
||||
|
@ -26,16 +26,27 @@
|
||||
(function () {
|
||||
// An object composed of domain object IDs and models
|
||||
// {id: domainObject's ID, name: domainObject's name}
|
||||
const indexedItems = {};
|
||||
const indexedDomainObjects = {};
|
||||
const indexedAnnotationsByDomainObject = {};
|
||||
const indexedAnnotationsByTag = {};
|
||||
|
||||
self.onconnect = function (e) {
|
||||
const port = e.ports[0];
|
||||
|
||||
port.onmessage = function (event) {
|
||||
if (event.data.request === 'index') {
|
||||
const requestType = event.data.request;
|
||||
if (requestType === 'index') {
|
||||
indexItem(event.data.keyString, event.data.model);
|
||||
} else if (event.data.request === 'search') {
|
||||
port.postMessage(search(event.data));
|
||||
} else if (requestType === 'OBJECTS') {
|
||||
port.postMessage(searchForObjects(event.data));
|
||||
} else if (requestType === 'ANNOTATIONS') {
|
||||
port.postMessage(searchForAnnotations(event.data));
|
||||
} else if (requestType === 'TAGS') {
|
||||
port.postMessage(searchForTags(event.data));
|
||||
} else if (requestType === 'NOTEBOOK_ANNOTATIONS') {
|
||||
port.postMessage(searchForNotebookAnnotations(event.data));
|
||||
} else {
|
||||
throw new Error(`Unknown request ${event.data.request}`);
|
||||
}
|
||||
};
|
||||
|
||||
@ -48,12 +59,70 @@
|
||||
console.error('Error on feed', error);
|
||||
};
|
||||
|
||||
function indexAnnotation(objectToIndex, model) {
|
||||
Object.keys(model.targets).forEach(targetID => {
|
||||
if (!indexedAnnotationsByDomainObject[targetID]) {
|
||||
indexedAnnotationsByDomainObject[targetID] = [];
|
||||
}
|
||||
|
||||
objectToIndex.targets = model.targets;
|
||||
objectToIndex.tags = model.tags;
|
||||
const existsInIndex = indexedAnnotationsByDomainObject[targetID].some(indexedObject => {
|
||||
return indexedObject.keyString === objectToIndex.keyString;
|
||||
});
|
||||
|
||||
if (!existsInIndex) {
|
||||
indexedAnnotationsByDomainObject[targetID].push(objectToIndex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function indexTags(keyString, objectToIndex, model) {
|
||||
// add new tags
|
||||
model.tags.forEach(tagID => {
|
||||
if (!indexedAnnotationsByTag[tagID]) {
|
||||
indexedAnnotationsByTag[tagID] = [];
|
||||
}
|
||||
|
||||
const existsInIndex = indexedAnnotationsByTag[tagID].some(indexedObject => {
|
||||
return indexedObject.keyString === objectToIndex.keyString;
|
||||
});
|
||||
|
||||
if (!existsInIndex) {
|
||||
indexedAnnotationsByTag[tagID].push(objectToIndex);
|
||||
}
|
||||
|
||||
});
|
||||
// remove old tags
|
||||
const tagsToRemoveFromIndex = Object.keys(indexedAnnotationsByTag).filter(indexedTag => {
|
||||
return !(model.tags.includes(indexedTag));
|
||||
});
|
||||
tagsToRemoveFromIndex.forEach(tagToRemoveFromIndex => {
|
||||
indexedAnnotationsByTag[tagToRemoveFromIndex] = indexedAnnotationsByTag[tagToRemoveFromIndex].filter(indexedAnnotation => {
|
||||
const shouldKeep = indexedAnnotation.keyString !== keyString;
|
||||
|
||||
return shouldKeep;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function indexItem(keyString, model) {
|
||||
indexedItems[keyString] = {
|
||||
const objectToIndex = {
|
||||
type: model.type,
|
||||
name: model.name,
|
||||
keyString
|
||||
};
|
||||
if (model && (model.type === 'annotation')) {
|
||||
if (model.targets) {
|
||||
indexAnnotation(objectToIndex, model);
|
||||
}
|
||||
|
||||
if (model.tags) {
|
||||
indexTags(keyString, objectToIndex, model);
|
||||
}
|
||||
} else {
|
||||
indexedDomainObjects[keyString] = objectToIndex;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -65,21 +134,98 @@
|
||||
* * maxResults: The maximum number of search results desired
|
||||
* * queryId: an id identifying this query, will be returned.
|
||||
*/
|
||||
function search(data) {
|
||||
// This results dictionary will have domain object ID keys which
|
||||
// point to the value the domain object's score.
|
||||
let results;
|
||||
function searchForObjects(data) {
|
||||
let results = [];
|
||||
const input = data.input.trim().toLowerCase();
|
||||
const message = {
|
||||
request: 'search',
|
||||
request: 'searchForObjects',
|
||||
results: [],
|
||||
total: 0,
|
||||
queryId: data.queryId
|
||||
};
|
||||
|
||||
results = Object.values(indexedDomainObjects).filter((indexedItem) => {
|
||||
return indexedItem.name.toLowerCase().includes(input);
|
||||
}) || [];
|
||||
|
||||
message.total = results.length;
|
||||
message.results = results
|
||||
.slice(0, data.maxResults);
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
function searchForAnnotations(data) {
|
||||
let results = [];
|
||||
const message = {
|
||||
request: 'searchForAnnotations',
|
||||
results: [],
|
||||
total: 0,
|
||||
queryId: data.queryId
|
||||
};
|
||||
|
||||
results = indexedAnnotationsByDomainObject[data.input] || [];
|
||||
|
||||
message.total = results.length;
|
||||
message.results = results
|
||||
.slice(0, data.maxResults);
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
function searchForTags(data) {
|
||||
let results = [];
|
||||
const message = {
|
||||
request: 'searchForTags',
|
||||
results: [],
|
||||
total: 0,
|
||||
queryId: data.queryId
|
||||
};
|
||||
|
||||
if (data.input) {
|
||||
data.input.forEach(matchingTag => {
|
||||
const matchingAnnotations = indexedAnnotationsByTag[matchingTag];
|
||||
if (matchingAnnotations) {
|
||||
matchingAnnotations.forEach(matchingAnnotation => {
|
||||
const existsInResults = results.some(indexedObject => {
|
||||
return matchingAnnotation.keyString === indexedObject.keyString;
|
||||
});
|
||||
if (!existsInResults) {
|
||||
results.push(matchingAnnotation);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
message.total = results.length;
|
||||
message.results = results
|
||||
.slice(0, data.maxResults);
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
function searchForNotebookAnnotations(data) {
|
||||
let results = [];
|
||||
const message = {
|
||||
request: 'searchForNotebookAnnotations',
|
||||
results: {},
|
||||
total: 0,
|
||||
queryId: data.queryId
|
||||
};
|
||||
|
||||
results = Object.values(indexedItems).filter((indexedItem) => {
|
||||
return indexedItem.name.toLowerCase().includes(input);
|
||||
});
|
||||
const matchingAnnotations = indexedAnnotationsByDomainObject[data.input.targetKeyString];
|
||||
if (matchingAnnotations) {
|
||||
results = matchingAnnotations.filter(matchingAnnotation => {
|
||||
if (!matchingAnnotation.targets) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const target = matchingAnnotation.targets[data.input.targetKeyString];
|
||||
|
||||
return (target && target.entryId && (target.entryId === data.input.entryId));
|
||||
});
|
||||
}
|
||||
|
||||
message.total = results.length;
|
||||
message.results = results
|
||||
|
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user