Compare commits

..

6 Commits

Author SHA1 Message Date
Nikhil
1a9c845f84 Merge branch 'master' into temp-source-map-fix 2022-05-27 09:40:29 -07:00
Nikhil
dfa2e1ef1e Merge branch 'master' into temp-source-map-fix 2022-05-25 00:00:49 -07:00
Nikhil Mandlik
fa4c58a7cb wip 2022-05-24 23:05:35 -07:00
Nikhil Mandlik
6c642281e7 added extra devtool options 2022-05-24 17:27:40 -07:00
Nikhil Mandlik
68e46e45c7 mode -> production 2022-05-24 17:06:46 -07:00
Nikhil Mandlik
8cd87ff9d1 use dev mode for production 2022-05-24 15:39:23 -07:00
306 changed files with 4248 additions and 17199 deletions

View File

@@ -2,7 +2,7 @@ version: 2.1
executors: executors:
pw-focal-development: pw-focal-development:
docker: docker:
- image: mcr.microsoft.com/playwright:v1.23.0-focal - image: mcr.microsoft.com/playwright:v1.21.1-focal
environment: environment:
NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed
parameters: parameters:
@@ -12,7 +12,7 @@ parameters:
type: boolean type: boolean
commands: commands:
build_and_install: build_and_install:
description: "All steps used to build and install. Will use cache if found" description: "All steps used to build and install. Will not work on node10"
parameters: parameters:
node-version: node-version:
type: string type: string
@@ -58,14 +58,10 @@ commands:
ls -latR >> /tmp/artifacts/dir.txt ls -latR >> /tmp/artifacts/dir.txt
- store_artifacts: - store_artifacts:
path: /tmp/artifacts/ path: /tmp/artifacts/
generate_e2e_code_cov_report: upload_code_covio:
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" description: "Command to upload code coverage reports to codecov.io"
parameters:
suite:
type: string
steps: steps:
- run: npm run cov:e2e:report - run: curl -Os https://uploader.codecov.io/latest/linux/codecov;chmod +x codecov;./codecov
- run: npm run cov:e2e:<<parameters.suite>>:publish
orbs: orbs:
node: circleci/node@4.9.0 node: circleci/node@4.9.0
browser-tools: circleci/browser-tools@1.3.0 browser-tools: circleci/browser-tools@1.3.0
@@ -118,13 +114,12 @@ jobs:
- browser-tools/install-chrome: - browser-tools/install-chrome:
replace-existing: false replace-existing: false
- run: npm run test -- --browsers=<<parameters.browser>> - run: npm run test -- --browsers=<<parameters.browser>>
- run: npm run cov:unit:publish
- save_cache_cmd: - save_cache_cmd:
node-version: <<parameters.node-version>> node-version: <<parameters.node-version>>
- store_test_results: - store_test_results:
path: dist/reports/tests/ path: dist/reports/tests/
- store_artifacts: - store_artifacts:
path: coverage path: dist/reports/
- generate_and_store_version_and_filesystem_artifacts - generate_and_store_version_and_filesystem_artifacts
e2e-test: e2e-test:
parameters: parameters:
@@ -137,22 +132,11 @@ jobs:
steps: steps:
- build_and_install: - build_and_install:
node-version: <<parameters.node-version>> node-version: <<parameters.node-version>>
- when: #Only install chrome-beta when running the full suite to save $$$
condition:
equal: [ "full", <<parameters.suite>> ]
steps:
- run: npx playwright install chrome-beta
- run: SHARD="$((${CIRCLE_NODE_INDEX}+1))"; npm run test:e2e:<<parameters.suite>> -- --shard=${SHARD}/${CIRCLE_NODE_TOTAL} - 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: - store_test_results:
path: test-results/results.xml path: test-results/results.xml
- store_artifacts: - store_artifacts:
path: test-results path: test-results
- store_artifacts:
path: coverage
- store_artifacts:
path: html-test-results
- generate_and_store_version_and_filesystem_artifacts - generate_and_store_version_and_filesystem_artifacts
perf-test: perf-test:
parameters: parameters:
@@ -167,19 +151,19 @@ jobs:
path: test-results/results.xml path: test-results/results.xml
- store_artifacts: - store_artifacts:
path: test-results path: test-results
- store_artifacts:
path: html-test-results
- generate_and_store_version_and_filesystem_artifacts - generate_and_store_version_and_filesystem_artifacts
workflows: workflows:
overall-circleci-commit-status: #These jobs run on every commit overall-circleci-commit-status: #These jobs run on every commit
jobs: jobs:
- lint: - lint:
name: node14-lint name: node16-lint
node-version: lts/fermium
- unit-test:
name: node16-chrome
node-version: lts/gallium node-version: lts/gallium
- unit-test:
name: node14-chrome
node-version: lts/fermium
browser: ChromeHeadless browser: ChromeHeadless
post-steps:
- upload_code_covio
- unit-test: - unit-test:
name: node18-chrome name: node18-chrome
node-version: "18" node-version: "18"

20
.gitignore vendored
View File

@@ -15,6 +15,8 @@
*.idea *.idea
*.iml *.iml
# External dependencies
# Build output # Build output
target target
dist dist
@@ -22,24 +24,30 @@ dist
# Mac OS X Finder # Mac OS X Finder
.DS_Store .DS_Store
# Closed source libraries
closed-lib
# Node, Bower dependencies # Node, Bower dependencies
node_modules node_modules
bower_components bower_components
# Protractor logs
protractor/logs
# npm-debug log # npm-debug log
npm-debug.log npm-debug.log
# karma reports # karma reports
report.*.json report.*.json
# Lighthouse reports
.lighthouseci
# e2e test artifacts # e2e test artifacts
test-results test-results
html-test-results allure-results
package-lock.json
#codecov artifacts #codecov artifacts
.nyc_output
coverage
codecov codecov
# :(
package-lock.json

2
app.js
View File

@@ -49,7 +49,7 @@ class WatchRunPlugin {
} }
const webpack = require('webpack'); const webpack = require('webpack');
const webpackConfig = process.env.CI ? require('./webpack.coverage.js') : require('./webpack.dev.js'); const webpackConfig = require('./webpack.dev.js');
webpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin()); webpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin());
webpackConfig.plugins.push(new WatchRunPlugin()); webpackConfig.plugins.push(new WatchRunPlugin());

View File

@@ -13,16 +13,17 @@ coverage:
round: down round: down
range: "66...100" range: "66...100"
flags: ignore:
unit:
carryforward: true parsers:
e2e-ci: gcov:
carryforward: true branch_detection:
e2e-full: conditional: true
carryforward: true loop: true
method: false
macro: false
comment: comment:
layout: "reach,diff,flags,files,footer" layout: "reach,diff,flags,files,footer"
behavior: default behavior: default
require_changes: false require_changes: false
show_carryforward_flags: true

View File

@@ -1,57 +1,15 @@
/* This file extends the base functionality of the playwright test framework to enable /* eslint-disable no-undef */
* code coverage instrumentation, console log error detection and working with a 3rd
* party Chrome-as-a-service extension called Browserless.
*/
// This file extends the base functionality of the playwright test framework
const base = require('@playwright/test'); const base = require('@playwright/test');
const { expect } = 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({ 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) => { page: async ({ baseURL, page }, use) => {
const messages = []; const messages = [];
page.on('console', (msg) => messages.push(msg)); page.on('console', msg => messages.push(`[${msg.type()}] ${msg.text()}`));
await use(page); await use(page);
messages.forEach( await expect.soft(messages.toString()).not.toContain('[error]');
msg => expect.soft(msg.type(), `Console error detected: ${consoleMessageToString(msg)}`).not.toEqual('error')
);
}, },
browser: async ({ playwright, browser }, use, workerInfo) => { browser: async ({ playwright, browser }, use, workerInfo) => {
// Use browserless if configured // Use browserless if configured

View File

@@ -7,13 +7,13 @@ const { devices } = require('@playwright/test');
/** @type {import('@playwright/test').PlaywrightTestConfig} */ /** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = { const config = {
retries: 3, //Retries 3 times for a total of 4. When running sharded and with maxFailures = 5, this should ensure that flake is managed without failing the full suite retries: 1,
testDir: 'tests', testDir: 'tests',
testIgnore: '**/*.perf.spec.js', //Ignore performance tests and define in playwright-perfromance.config.js testIgnore: '**/*.perf.spec.js', //Ignore performance tests and define in playwright-perfromance.config.js
timeout: 60 * 1000, timeout: 60 * 1000,
webServer: { webServer: {
command: 'npm run start', command: 'npm run start',
url: 'http://localhost:8080/#', port: 8080,
timeout: 200 * 1000, timeout: 200 * 1000,
reuseExistingServer: !process.env.CI reuseExistingServer: !process.env.CI
}, },
@@ -36,7 +36,6 @@ const config = {
}, },
{ {
name: 'MMOC', name: 'MMOC',
testMatch: '**/*.e2e.spec.js', // only run e2e tests
grepInvert: /@snapshot/, grepInvert: /@snapshot/,
use: { use: {
browserName: 'chromium', browserName: 'chromium',
@@ -45,30 +44,20 @@ const config = {
height: 1440 height: 1440
} }
} }
}, }
{ /*{
name: 'firefox', name: 'ipad',
testMatch: '**/*.e2e.spec.js', // only run e2e tests
grepInvert: /@snapshot/,
use: { use: {
browserName: 'firefox' browserName: 'webkit',
} ...devices['iPad (gen 7) landscape'] // Complete List https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json
},
{
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: [ reporter: [
['list'], ['list'],
['html', { ['html', {
open: 'never', open: 'never',
outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840 outputFolder: '../test-results/html/'
}], }],
['junit', { outputFile: 'test-results/results.xml' }], ['junit', { outputFile: 'test-results/results.xml' }],
['github'] ['github']

View File

@@ -13,7 +13,7 @@ const config = {
timeout: 30 * 1000, timeout: 30 * 1000,
webServer: { webServer: {
command: 'npm run start', command: 'npm run start',
url: 'http://localhost:8080/#', port: 8080,
timeout: 120 * 1000, timeout: 120 * 1000,
reuseExistingServer: !process.env.CI reuseExistingServer: !process.env.CI
}, },
@@ -36,7 +36,6 @@ const config = {
}, },
{ {
name: 'MMOC', name: 'MMOC',
testMatch: '**/*.e2e.spec.js', // only run e2e tests
grepInvert: /@snapshot/, grepInvert: /@snapshot/,
use: { use: {
browserName: 'chromium', browserName: 'chromium',
@@ -45,58 +44,20 @@ const config = {
height: 1440 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', name: 'ipad',
testMatch: '**/*.e2e.spec.js', // only run e2e tests
grep: /@ipad/,
grepInvert: /@snapshot/,
use: { use: {
browserName: 'webkit', browserName: 'webkit',
...devices['iPad (gen 7) landscape'] // Complete List https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json ...devices['iPad (gen 7) landscape'] // Complete List https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json
} }
} }*/
], ],
reporter: [ reporter: [
['list'], ['list'],
['html', { ['html', {
open: 'on-failure', open: 'on-failure',
outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840 outputFolder: '../test-results'
}] }]
] ]
}; };

View File

@@ -4,13 +4,13 @@
/** @type {import('@playwright/test').PlaywrightTestConfig} */ /** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = { const config = {
retries: 1, //Only for debugging purposes because trace is enabled only on first retry retries: 0,
testDir: 'tests/performance/', testDir: 'tests/performance/',
timeout: 60 * 1000, timeout: 30 * 1000,
workers: 1, //Only run in serial with 1 worker workers: 1, //Only run in serial with 1 worker
webServer: { webServer: {
command: 'npm run start', command: 'npm run start',
url: 'http://localhost:8080/#', port: 8080,
timeout: 200 * 1000, timeout: 200 * 1000,
reuseExistingServer: !process.env.CI reuseExistingServer: !process.env.CI
}, },
@@ -20,7 +20,7 @@ const config = {
headless: Boolean(process.env.CI), //Only if running locally headless: Boolean(process.env.CI), //Only if running locally
ignoreHTTPSErrors: true, ignoreHTTPSErrors: true,
screenshot: 'off', screenshot: 'off',
trace: 'on-first-retry', trace: 'off',
video: 'off' video: 'off'
}, },
projects: [ projects: [

View File

@@ -10,7 +10,7 @@ const config = {
workers: 1, // visual tests should never run in parallel due to test pollution workers: 1, // visual tests should never run in parallel due to test pollution
webServer: { webServer: {
command: 'npm run start', command: 'npm run start',
url: 'http://localhost:8080/#', port: 8080,
timeout: 200 * 1000, timeout: 200 * 1000,
reuseExistingServer: !process.env.CI reuseExistingServer: !process.env.CI
}, },

View File

@@ -1 +1 @@
{"openmct":{"b3cee102-86dd-4c0a-8eec-4d5d276f8691":{"identifier":{"key":"b3cee102-86dd-4c0a-8eec-4d5d276f8691","namespace":""},"name":"Performance Display Layout","type":"layout","composition":[{"key":"9666e7b4-be0c-47a5-94b8-99accad7155e","namespace":""}],"configuration":{"items":[{"width":32,"height":18,"x":12,"y":9,"identifier":{"key":"9666e7b4-be0c-47a5-94b8-99accad7155e","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"23ca351d-a67d-46aa-a762-290eb742d2f1"}],"layoutGrid":[10,10]},"modified":1654299875432,"location":"mine","persisted":1654299878751},"9666e7b4-be0c-47a5-94b8-99accad7155e":{"identifier":{"key":"9666e7b4-be0c-47a5-94b8-99accad7155e","namespace":""},"name":"Performance Example Imagery","type":"example.imagery","configuration":{"imageLocation":"","imageLoadDelayInMilliSeconds":20000,"imageSamples":[],"layers":[{"source":"dist/imagery/example-imagery-layer-16x9.png","name":"16:9","visible":false},{"source":"dist/imagery/example-imagery-layer-safe.png","name":"Safe","visible":false},{"source":"dist/imagery/example-imagery-layer-scale.png","name":"Scale","visible":false}]},"telemetry":{"values":[{"name":"Name","key":"name"},{"name":"Time","key":"utc","format":"utc","hints":{"domain":2}},{"name":"Local Time","key":"local","format":"local-format","hints":{"domain":1}},{"name":"Image","key":"url","format":"image","hints":{"image":1},"layers":[{"source":"dist/imagery/example-imagery-layer-16x9.png","name":"16:9"},{"source":"dist/imagery/example-imagery-layer-safe.png","name":"Safe"},{"source":"dist/imagery/example-imagery-layer-scale.png","name":"Scale"}]},{"name":"Image Download Name","key":"imageDownloadName","format":"imageDownloadName","hints":{"imageDownloadName":1}}]},"modified":1654299840077,"location":"b3cee102-86dd-4c0a-8eec-4d5d276f8691","persisted":1654299840078}},"rootId":"b3cee102-86dd-4c0a-8eec-4d5d276f8691"} {"openmct":{"21338566-d472-4377-aed1-21b79272c8de":{"identifier":{"key":"21338566-d472-4377-aed1-21b79272c8de","namespace":""},"name":"Performance Display Layout","type":"layout","composition":[{"key":"644c2e47-2903-475f-8a4a-6be1588ee02f","namespace":""}],"configuration":{"items":[{"width":32,"height":18,"x":1,"y":1,"identifier":{"key":"644c2e47-2903-475f-8a4a-6be1588ee02f","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"5aeb5a71-3149-41ed-9d8a-d34b0a18b053"}],"layoutGrid":[10,10]},"modified":1652228997384,"location":"mine","persisted":1652228997384},"644c2e47-2903-475f-8a4a-6be1588ee02f":{"identifier":{"key":"644c2e47-2903-475f-8a4a-6be1588ee02f","namespace":""},"name":"Performance Example Imagery","type":"example.imagery","configuration":{"imageLocation":"","imageLoadDelayInMilliSeconds":20000,"imageSamples":[]},"telemetry":{"values":[{"name":"Name","key":"name"},{"name":"Time","key":"utc","format":"utc","hints":{"domain":2}},{"name":"Local Time","key":"local","format":"local-format","hints":{"domain":1}},{"name":"Image","key":"url","format":"image","hints":{"image":1}},{"name":"Image Download Name","key":"imageDownloadName","format":"imageDownloadName","hints":{"imageDownloadName":1}}]},"modified":1652228997375,"location":"21338566-d472-4377-aed1-21b79272c8de","persisted":1652228997375}},"rootId":"21338566-d472-4377-aed1-21b79272c8de"}

View File

@@ -1,22 +0,0 @@
{
"cookies": [],
"origins": [
{
"origin": "http://localhost:8080",
"localStorage": [
{
"name": "tcHistory",
"value": "{\"utc\":[{\"start\":1654548551471,\"end\":1654550351471}]}"
},
{
"name": "mct",
"value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"527856c0-cced-4b64-bb19-f943432326d0\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1654550352296,\"modified\":1654550352296},\"527856c0-cced-4b64-bb19-f943432326d0\":{\"identifier\":{\"key\":\"527856c0-cced-4b64-bb19-f943432326d0\",\"namespace\":\"\"},\"name\":\"Unnamed Overlay Plot\",\"type\":\"telemetry.plot.overlay\",\"composition\":[{\"key\":\"ce88ce37-8bb9-45e1-a85b-bb7e3c8453b9\",\"namespace\":\"\"}],\"configuration\":{\"series\":[{\"identifier\":{\"key\":\"ce88ce37-8bb9-45e1-a85b-bb7e3c8453b9\",\"namespace\":\"\"}}],\"yAxis\":{},\"xAxis\":{}},\"modified\":1654550353356,\"location\":\"mine\",\"persisted\":1654550353357},\"ce88ce37-8bb9-45e1-a85b-bb7e3c8453b9\":{\"name\":\"Unnamed Sine Wave Generator\",\"type\":\"generator\",\"identifier\":{\"key\":\"ce88ce37-8bb9-45e1-a85b-bb7e3c8453b9\",\"namespace\":\"\"},\"telemetry\":{\"period\":10,\"amplitude\":1,\"offset\":0,\"dataRateInHz\":1,\"phase\":0,\"randomness\":0,\"loadDelay\":\"5000\"},\"modified\":1654550353350,\"location\":\"527856c0-cced-4b64-bb19-f943432326d0\",\"persisted\":1654550353350}}"
},
{
"name": "mct-tree-expanded",
"value": "[\"/browse/mine\"]"
}
]
}
]
}

View File

@@ -1,22 +0,0 @@
{
"cookies": [],
"origins": [
{
"origin": "http://localhost:8080",
"localStorage": [
{
"name": "tcHistory",
"value": "{\"utc\":[{\"start\":1654537164464,\"end\":1654538964464},{\"start\":1652301954635,\"end\":1652303754635}]}"
},
{
"name": "mct",
"value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1654538965703,\"modified\":1654538965703},\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"73f2d9ae-d1f3-4561-b7fc-ecd5df557249\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1652303755999,\"location\":\"mine\",\"persisted\":1652303756002},\"2d02a680-eb7e-4645-bba2-dd298f76efb8\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"2d02a680-eb7e-4645-bba2-dd298f76efb8\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"4291d80c-303c-4d8d-85e1-10f012b864fb\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1654538965702,\"location\":\"mine\",\"persisted\":1654538965702}}"
},
{
"name": "mct-tree-expanded",
"value": "[]"
}
]
}
]
}

View File

@@ -58,7 +58,6 @@ test.describe('Branding tests', () => {
page.waitForEvent('popup'), page.waitForEvent('popup'),
page.locator('text=click here for third party licensing information').click() page.locator('text=click here for third party licensing information').click()
]); ]);
await page2.waitForLoadState('networkidle'); //Avoids timing issues with juggler/firefox
expect(page2.waitForURL('**/licenses**')).toBeTruthy(); expect(page2.waitForURL('**/licenses**')).toBeTruthy();
}); });
}); });

View File

@@ -28,9 +28,7 @@ const { test } = require('../../../fixtures.js');
const { expect } = require('@playwright/test'); const { expect } = require('@playwright/test');
test.describe('Sine Wave Generator', () => { test.describe('Sine Wave Generator', () => {
test('Create new Sine Wave Generator Object and validate create Form Logic', async ({ page, browserName }) => { test('Create new Sine Wave Generator Object and validate create Form Logic', async ({ page }) => {
test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox');
//Go to baseURL //Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' }); await page.goto('/', { waitUntil: 'networkidle' });
@@ -42,45 +40,44 @@ test.describe('Sine Wave Generator', () => {
// Verify that the each required field has required indicator // Verify that the each required field has required indicator
// Title // Title
await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(/req/); await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(['c-form-row__state-indicator req']);
// Verify that the Notes row does not have a required indicator // 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 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 // Period
await expect(page.locator('div:nth-child(4) .c-form-row__state-indicator')).toHaveClass(/req/); await expect(page.locator('.c-form__section div:nth-child(4) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator req']);
// Amplitude // Amplitude
await expect(page.locator('div:nth-child(5) .c-form-row__state-indicator')).toHaveClass(/req/); await expect(page.locator('.c-form__section div:nth-child(5) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator req']);
// Offset // Offset
await expect(page.locator('div:nth-child(6) .c-form-row__state-indicator')).toHaveClass(/req/); await expect(page.locator('.c-form__section div:nth-child(6) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator req']);
// Data Rate // Data Rate
await expect(page.locator('div:nth-child(7) .c-form-row__state-indicator')).toHaveClass(/req/); await expect(page.locator('.c-form__section div:nth-child(7) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator req']);
// Phase // Phase
await expect(page.locator('div:nth-child(8) .c-form-row__state-indicator')).toHaveClass(/req/); await expect(page.locator('.c-form__section div:nth-child(8) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator req']);
// Randomness // Randomness
await expect(page.locator('div:nth-child(9) .c-form-row__state-indicator')).toHaveClass(/req/); await expect(page.locator('.c-form__section div:nth-child(9) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator req']);
// Verify that by removing value from required text field shows invalid indicator // 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 page.locator('text=Properties Title Notes Period Amplitude Offset Data Rate (hz) Phase (radians) Ra >> input[type="text"]').fill('');
await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(/invalid/); await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(['c-form-row__state-indicator req invalid']);
// Verify that by adding value to empty required text field changes invalid to valid indicator // Verify that by adding value to empty required text field changes invalid to valid indicator
await page.locator('text=Properties Title Notes Period Amplitude Offset Data Rate (hz) Phase (radians) Ra >> input[type="text"]').fill('New Sine Wave Generator'); await 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(/valid/); await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(['c-form-row__state-indicator req valid']);
// Verify that by removing value from required number field shows invalid indicator // Verify that by removing value from required number field shows invalid indicator
await page.locator('.field.control.l-input-sm input').first().fill(''); await page.locator('.field.control.l-input-sm input').first().fill('');
await expect(page.locator('div:nth-child(4) .c-form-row__state-indicator')).toHaveClass(/invalid/); await expect(page.locator('.c-form__section div:nth-child(4) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator req invalid']);
// Verify that by adding value to empty required number field changes invalid to valid indicator // 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 page.locator('.field.control.l-input-sm input').first().fill('3');
await expect(page.locator('div:nth-child(4) .c-form-row__state-indicator')).toHaveClass(/valid/); await expect(page.locator('.c-form__section div:nth-child(4) .form-row .c-form-row__state-indicator')).toHaveClass(['c-form-row__state-indicator req valid']);
// Verify that can change value of number field by up/down arrows keys // Verify that can change value of number field by up/down arrows keys
// Click .field.control.l-input-sm input >> nth=0 // Click .field.control.l-input-sm input >> nth=0
@@ -93,6 +90,57 @@ test.describe('Sine Wave Generator', () => {
const value = await page.locator('.field.control.l-input-sm input').first().inputValue(); const value = await page.locator('.field.control.l-input-sm input').first().inputValue();
await expect(value).toBe('6'); 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 //Click text=OK
await Promise.all([ await Promise.all([
page.waitForNavigation(), page.waitForNavigation(),
@@ -103,7 +151,7 @@ test.describe('Sine Wave Generator', () => {
// Verify object properties // Verify object properties
await expect(page.locator('.l-browse-bar__object-name')).toContainText('New Sine Wave Generator'); await expect(page.locator('.l-browse-bar__object-name')).toContainText('New Sine Wave Generator');
// Verify canvas rendered and can be interacted with // Verify canvas rendered
await page.locator('canvas').nth(1).click({ await page.locator('canvas').nth(1).click({
position: { position: {
x: 341, x: 341,

View File

@@ -40,6 +40,9 @@ test.describe('Move item tests', () => {
await page.locator('text=Properties Title Notes >> input[type="text"]').click(); await page.locator('text=Properties Title Notes >> input[type="text"]').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder1); await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder1);
// Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184
await page.click('form[name="mctForm"] a:has-text("My Items")');
await Promise.all([ await Promise.all([
page.waitForNavigation(), page.waitForNavigation(),
page.locator('text=OK').click(), page.locator('text=OK').click(),
@@ -56,6 +59,9 @@ test.describe('Move item tests', () => {
await page.locator('text=Properties Title Notes >> input[type="text"]').click(); await page.locator('text=Properties Title Notes >> input[type="text"]').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder2); await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder2);
// Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184
await page.click('form[name="mctForm"] a:has-text("My Items")');
await Promise.all([ await Promise.all([
page.waitForNavigation(), page.waitForNavigation(),
page.locator('text=OK').click(), page.locator('text=OK').click(),
@@ -91,6 +97,9 @@ test.describe('Move item tests', () => {
await page.locator('text=Properties Title Notes >> input[type="text"]').click(); await page.locator('text=Properties Title Notes >> input[type="text"]').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(telemetryTable); await page.locator('text=Properties Title Notes >> input[type="text"]').fill(telemetryTable);
// Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184
await page.click('form[name="mctForm"] a:has-text("My Items")');
await page.locator('text=OK').click(); await page.locator('text=OK').click();
// Finish editing and save Telemetry Table // Finish editing and save Telemetry Table

View File

@@ -103,10 +103,10 @@ test.describe('Performance tests', () => {
await page.goto('/'); await page.goto('/');
// Search Available after Launch // Search Available after Launch
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); await page.locator('input[type="search"]').click();
await page.evaluate(() => window.performance.mark("search-available")); await page.evaluate(() => window.performance.mark("search-available"));
// Fill Search input // Fill Search input
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Performance Display Layout'); await page.locator('input[type="search"]').fill('Performance Display Layout');
await page.evaluate(() => window.performance.mark("search-entered")); await page.evaluate(() => window.performance.mark("search-entered"));
//Search Result Appears and is clicked //Search Result Appears and is clicked
await Promise.all([ await Promise.all([
@@ -164,7 +164,7 @@ test.describe('Performance tests', () => {
console.log('jpgResourceTiming ' + JSON.stringify(jpgResourceTiming)); console.log('jpgResourceTiming ' + JSON.stringify(jpgResourceTiming));
// Click Close Icon // Click Close Icon
await page.locator('[aria-label="Close"]').click(); await page.locator('.c-click-icon').click();
await page.evaluate(() => window.performance.mark("view-large-close-button")); await page.evaluate(() => window.performance.mark("view-large-close-button"));
//await client.send('HeapProfiler.enable'); //await client.send('HeapProfiler.enable');

View File

@@ -64,9 +64,9 @@ test.describe.skip('Memory Performance tests', () => {
await page.goto('/', {waitUntil: 'networkidle'}); await page.goto('/', {waitUntil: 'networkidle'});
// To to Search Available after Launch // To to Search Available after Launch
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); await page.locator('input[type="search"]').click();
// Fill Search input // Fill Search input
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Performance Display Layout'); await page.locator('input[type="search"]').fill('Performance Display Layout');
//Search Result Appears and is clicked //Search Result Appears and is clicked
await Promise.all([ await Promise.all([
page.waitForNavigation(), page.waitForNavigation(),

View File

@@ -98,10 +98,10 @@ test.describe('Performance tests', () => {
await page.goto('/'); await page.goto('/');
// To to Search Available after Launch // To to Search Available after Launch
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click(); await page.locator('input[type="search"]').click();
await page.evaluate(() => window.performance.mark("search-available")); await page.evaluate(() => window.performance.mark("search-available"));
// Fill Search input // Fill Search input
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Performance Notebook'); await page.locator('input[type="search"]').fill('Performance Notebook');
await page.evaluate(() => window.performance.mark("search-entered")); await page.evaluate(() => window.performance.mark("search-entered"));
//Search Result Appears and is clicked //Search Result Appears and is clicked
await Promise.all([ await Promise.all([

View File

@@ -28,7 +28,9 @@ const { test } = require('../../fixtures.js');
const { expect } = require('@playwright/test'); const { expect } = require('@playwright/test');
const path = require('path'); const path = require('path');
test.describe('Persistence operations @addInit', () => { // https://github.com/nasa/openmct/issues/4323#issuecomment-1067282651
test.describe('Persistence operations', () => {
// add non persistable root item // add non persistable root item
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
@@ -36,10 +38,6 @@ test.describe('Persistence operations @addInit', () => {
}); });
test('Persistability should be respected in the create form location field', async ({ page }) => { 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 // Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' }); await page.goto('/', { waitUntil: 'networkidle' });

View File

@@ -46,22 +46,22 @@ test.describe('Clock Generator', () => {
// Click .icon-arrow-down // Click .icon-arrow-down
await page.locator('.icon-arrow-down').click(); await page.locator('.icon-arrow-down').click();
//verify if the autocomplete dropdown is visible //verify if the autocomplete dropdown is visible
await expect(page.locator(".c-input--autocomplete__options")).toBeVisible(); await expect(page.locator(".optionPreSelected")).toBeVisible();
// Click .icon-arrow-down // Click .icon-arrow-down
await page.locator('.icon-arrow-down').click(); await page.locator('.icon-arrow-down').click();
// Verify clicking on the autocomplete arrow collapses the dropdown // Verify clicking on the autocomplete arrow collapses the dropdown
await expect(page.locator(".c-input--autocomplete__options")).not.toBeVisible(); await expect(page.locator(".optionPreSelected")).not.toBeVisible();
// Click timezone input to open dropdown // Click timezone input to open dropdown
await page.locator('.c-input--autocomplete__input').click(); await page.locator('.autocompleteInput').click();
//verify if the autocomplete dropdown is visible //verify if the autocomplete dropdown is visible
await expect(page.locator(".c-input--autocomplete__options")).toBeVisible(); await expect(page.locator(".optionPreSelected")).toBeVisible();
// Verify clicking outside the autocomplete dropdown collapses it // Verify clicking outside the autocomplete dropdown collapses it
await page.locator('text=Timezone').click(); await page.locator('text=Timezone').click();
// Verify clicking on the autocomplete arrow collapses the dropdown // Verify clicking on the autocomplete arrow collapses the dropdown
await expect(page.locator(".c-input--autocomplete__options")).not.toBeVisible(); await expect(page.locator(".optionPreSelected")).not.toBeVisible();
}); });
}); });

View File

@@ -32,10 +32,7 @@ const { expect } = require('@playwright/test');
let conditionSetUrl; let conditionSetUrl;
let getConditionSetIdentifierFromUrl; let getConditionSetIdentifierFromUrl;
test.describe.serial('Condition Set CRUD Operations on @localStorage', () => { test('Create new Condition Set object and store @localStorage', async ({ page, context }) => {
test.beforeAll(async ({ browser}) => {
const context = await browser.newContext();
const page = await context.newPage();
//Go to baseURL //Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' }); await page.goto('/', { waitUntil: 'networkidle' });
@@ -43,7 +40,10 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
await page.click('button:has-text("Create")'); await page.click('button:has-text("Create")');
// Click text=Condition Set // Click text=Condition Set
await page.locator('li:has-text("Condition Set")').click(); await page.click('text=Condition Set');
// Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184
await page.click('form[name="mctForm"] a:has-text("My Items")');
// Click text=OK // Click text=OK
await Promise.all([ await Promise.all([
@@ -51,28 +51,29 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
page.click('text=OK') page.click('text=OK')
]); ]);
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
//Save localStorage for future test execution //Save localStorage for future test execution
await context.storageState({ path: './e2e/test-data/recycled_local_storage.json' }); await context.storageState({ path: './e2e/tests/recycled_storage.json' });
//Set object identifier from url //Set object identifier from url
conditionSetUrl = await page.url(); conditionSetUrl = await page.url();
console.log('conditionSetUrl ' + conditionSetUrl); console.log('conditionSetUrl ' + conditionSetUrl);
getConditionSetIdentifierFromUrl = await conditionSetUrl.split('/').pop().split('?')[0]; getConditionSetIdentifierFromUrl = await conditionSetUrl.split('/').pop().split('?')[0];
console.debug('getConditionSetIdentifierFromUrl ' + getConditionSetIdentifierFromUrl); console.log('getConditionSetIdentifierFromUrl ' + getConditionSetIdentifierFromUrl);
await page.close();
});
test.afterAll(async ({ browser }) => {
await browser.close();
}); });
test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
//Load localStorage for subsequent tests //Load localStorage for subsequent tests
test.use({ storageState: './e2e/test-data/recycled_local_storage.json' }); test.use({ storageState: './e2e/tests/recycled_storage.json' });
//Begin suite of tests again localStorage //Begin suite of tests again localStorage
test('Condition set object properties persist in main view and inspector @localStorage', async ({ page }) => { test('Condition set object properties persist in main view and inspector', async ({ page }) => {
//Navigate to baseURL with injected localStorage //Navigate to baseURL with injected localStorage
await page.goto(conditionSetUrl, { waitUntil: 'networkidle' }); await page.goto(conditionSetUrl, { waitUntil: 'networkidle' });
//Assertions on loaded Condition Set in main view. This is a stateful transition step after page.goto() //Assertions on loaded Condition Set in main view
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set'); await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
//Assertions on loaded Condition Set in Inspector //Assertions on loaded Condition Set in Inspector
@@ -93,7 +94,7 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
test('condition set object can be modified on @localStorage', async ({ page }) => { test('condition set object can be modified on @localStorage', async ({ page }) => {
await page.goto(conditionSetUrl, { waitUntil: 'networkidle' }); await page.goto(conditionSetUrl, { waitUntil: 'networkidle' });
//Assertions on loaded Condition Set in main view. This is a stateful transition step after page.goto() //Assertions on loaded Condition Set in main view
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set'); await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
//Update the Condition Set properties //Update the Condition Set properties
@@ -123,7 +124,7 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
// Verify Condition Set Object is renamed in Tree // Verify Condition Set Object is renamed in Tree
await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
// Verify Search Tree reflects renamed Name property // Verify Search Tree reflects renamed Name property
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Renamed'); await page.locator('input[type="search"]').fill('Renamed');
await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
//Reload Page //Reload Page
@@ -147,33 +148,35 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
// Verify Condition Set Object is renamed in Tree // Verify Condition Set Object is renamed in Tree
await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
// Verify Search Tree reflects renamed Name property // Verify Search Tree reflects renamed Name property
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Renamed'); await page.locator('input[type="search"]').fill('Renamed');
await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy(); await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
}); });
test('condition set object can be deleted by Search Tree Actions menu on @localStorage', async ({ page }) => { test('condition set object can be deleted by Search Tree Actions menu on @localStorage', async ({ page }) => {
//Navigate to baseURL //Navigate to baseURL
await page.goto('/', { waitUntil: 'networkidle' }); await page.goto('/', { waitUntil: 'networkidle' });
//Assertions on loaded Condition Set in main view. This is a stateful transition step after page.goto() //Expect Unnamed Condition Set to be visible in Main View
await expect(page.locator('a:has-text("Unnamed Condition Set Condition Set") >> nth=0')).toBeVisible(); await expect(page.locator('a:has-text("Unnamed Condition Set Condition Set")')).toBeVisible();
const numberOfConditionSetsToStart = await page.locator('a:has-text("Unnamed Condition Set Condition Set")').count();
// Search for Unnamed Condition Set // Search for Unnamed Condition Set
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Unnamed Condition Set'); await page.locator('input[type="search"]').fill('Unnamed Condition Set');
// Click Search Result // Right Click to Open Actions Menu
await page.locator('[aria-label="OpenMCT Search"] >> text=Unnamed Condition Set').first().click(); await page.locator('a:has-text("Unnamed Condition Set")').click({
// Click hamburger button button: 'right'
await page.locator('[title="More options"]').click(); });
// Click Remove Action
// Click text=Remove
await page.locator('text=Remove').click(); await page.locator('text=Remove').click();
await page.locator('text=OK').click(); await page.locator('text=OK').click();
//Expect Unnamed Condition Set to be removed in Main View //Expect Unnamed Condition Set to be removed in Main View
const numberOfConditionSetsAtEnd = await page.locator('a:has-text("Unnamed Condition Set Condition Set")').count(); await expect(page.locator('a:has-text("Unnamed Condition Set Condition Set")')).not.toBeVisible();
expect(numberOfConditionSetsAtEnd).toEqual(numberOfConditionSetsToStart - 1); await page.locator('.c-search__clear-input').click();
// Search for Unnamed Condition Set
await page.locator('input[type="search"]').fill('Unnamed Condition Set');
// Expect Unnamed Condition Set to be removed
await expect(page.locator('a:has-text("Unnamed Condition Set")')).not.toBeVisible();
//Feature? //Feature?
//Domain Object is still available by direct URL after delete //Domain Object is still available by direct URL after delete

View File

@@ -29,12 +29,10 @@ but only assume that example imagery is present.
const { test } = require('../../../fixtures.js'); const { test } = require('../../../fixtures.js');
const { expect } = require('@playwright/test'); const { expect } = require('@playwright/test');
const backgroundImageSelector = '.c-imagery__main-image__background-image'; test.describe('Example Imagery', () => {
//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 }) => { test.beforeEach(async ({ page }) => {
page.on('console', msg => console.log(msg.text()));
//Go to baseURL //Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' }); await page.goto('/', { waitUntil: 'networkidle' });
@@ -44,6 +42,9 @@ test.describe('Example Imagery Object', () => {
// Click text=Example Imagery // Click text=Example Imagery
await page.click('text=Example Imagery'); await page.click('text=Example Imagery');
// Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184
await page.click('form[name="mctForm"] a:has-text("My Items")');
// Click text=OK // Click text=OK
await Promise.all([ await Promise.all([
page.waitForNavigation({waitUntil: 'networkidle'}), page.waitForNavigation({waitUntil: 'networkidle'}),
@@ -51,30 +52,28 @@ test.describe('Example Imagery Object', () => {
//Wait for Save Banner to appear //Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message') page.waitForSelector('.c-message-banner__message')
]); ]);
// Close Banner
await page.locator('.c-message-banner__close-button').click();
//Wait until Save Banner is gone //Wait until Save Banner is gone
await page.waitForSelector('.c-message-banner__message', { state: 'detached'}); await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery'); await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');
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 }) => { test('Can use Mouse Wheel to zoom in and out of latest image', async ({ page }) => {
const bgImageLocator = page.locator(backgroundImageSelector);
const deltaYStep = 100; //equivalent to 1x zoom const deltaYStep = 100; //equivalent to 1x zoom
await page.locator(backgroundImageSelector).hover({trial: true}); await bgImageLocator.hover();
const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox(); const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox();
// zoom in // zoom in
await page.locator(backgroundImageSelector).hover({trial: true}); await bgImageLocator.hover();
await page.mouse.wheel(0, deltaYStep * 2); await page.mouse.wheel(0, deltaYStep * 2);
// wait for zoom animation to finish // wait for zoom animation to finish
await page.locator(backgroundImageSelector).hover({trial: true}); await bgImageLocator.hover();
const imageMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox(); const imageMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox();
// zoom out // zoom out
await page.locator(backgroundImageSelector).hover({trial: true}); await bgImageLocator.hover();
await page.mouse.wheel(0, -deltaYStep); await page.mouse.wheel(0, -deltaYStep);
// wait for zoom animation to finish // wait for zoom animation to finish
await page.locator(backgroundImageSelector).hover({trial: true}); await bgImageLocator.hover();
const imageMouseZoomedOut = await page.locator(backgroundImageSelector).boundingBox(); const imageMouseZoomedOut = await page.locator(backgroundImageSelector).boundingBox();
expect(imageMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height); expect(imageMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height);
@@ -88,12 +87,13 @@ test.describe('Example Imagery Object', () => {
const deltaYStep = 100; //equivalent to 1x zoom const deltaYStep = 100; //equivalent to 1x zoom
const panHotkey = process.platform === 'linux' ? ['Control', 'Alt'] : ['Alt']; const panHotkey = process.platform === 'linux' ? ['Control', 'Alt'] : ['Alt'];
await page.locator(backgroundImageSelector).hover({trial: true}); const bgImageLocator = page.locator(backgroundImageSelector);
await bgImageLocator.hover();
// zoom in // zoom in
await page.mouse.wheel(0, deltaYStep * 2); await page.mouse.wheel(0, deltaYStep * 2);
await page.locator(backgroundImageSelector).hover({trial: true}); await bgImageLocator.hover();
const zoomedBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); const zoomedBoundingBox = await bgImageLocator.boundingBox();
const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2; const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;
const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2; const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
// move to the right // move to the right
@@ -116,7 +116,7 @@ test.describe('Example Imagery Object', () => {
await page.mouse.move(imageCenterX - 200, imageCenterY, 10); await page.mouse.move(imageCenterX - 200, imageCenterY, 10);
await page.mouse.up(); await page.mouse.up();
await Promise.all(panHotkey.map(x => page.keyboard.up(x))); await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
const afterRightPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); const afterRightPanBoundingBox = await bgImageLocator.boundingBox();
expect(zoomedBoundingBox.x).toBeGreaterThan(afterRightPanBoundingBox.x); expect(zoomedBoundingBox.x).toBeGreaterThan(afterRightPanBoundingBox.x);
// pan left // pan left
@@ -125,7 +125,7 @@ test.describe('Example Imagery Object', () => {
await page.mouse.move(imageCenterX, imageCenterY, 10); await page.mouse.move(imageCenterX, imageCenterY, 10);
await page.mouse.up(); await page.mouse.up();
await Promise.all(panHotkey.map(x => page.keyboard.up(x))); await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
const afterLeftPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); const afterLeftPanBoundingBox = await bgImageLocator.boundingBox();
expect(afterRightPanBoundingBox.x).toBeLessThan(afterLeftPanBoundingBox.x); expect(afterRightPanBoundingBox.x).toBeLessThan(afterLeftPanBoundingBox.x);
// pan up // pan up
@@ -135,7 +135,7 @@ test.describe('Example Imagery Object', () => {
await page.mouse.move(imageCenterX, imageCenterY + 200, 10); await page.mouse.move(imageCenterX, imageCenterY + 200, 10);
await page.mouse.up(); await page.mouse.up();
await Promise.all(panHotkey.map(x => page.keyboard.up(x))); await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
const afterUpPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); const afterUpPanBoundingBox = await bgImageLocator.boundingBox();
expect(afterUpPanBoundingBox.y).toBeGreaterThan(afterLeftPanBoundingBox.y); expect(afterUpPanBoundingBox.y).toBeGreaterThan(afterLeftPanBoundingBox.y);
// pan down // pan down
@@ -144,58 +144,60 @@ test.describe('Example Imagery Object', () => {
await page.mouse.move(imageCenterX, imageCenterY - 200, 10); await page.mouse.move(imageCenterX, imageCenterY - 200, 10);
await page.mouse.up(); await page.mouse.up();
await Promise.all(panHotkey.map(x => page.keyboard.up(x))); await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
const afterDownPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); const afterDownPanBoundingBox = await bgImageLocator.boundingBox();
expect(afterDownPanBoundingBox.y).toBeLessThan(afterUpPanBoundingBox.y); expect(afterDownPanBoundingBox.y).toBeLessThan(afterUpPanBoundingBox.y);
}); });
test('Can use + - buttons to zoom on the image', async ({ page }) => { test('Can use + - buttons to zoom on the image', async ({ page }) => {
await page.locator(backgroundImageSelector).hover({trial: true}); const bgImageLocator = page.locator(backgroundImageSelector);
const zoomInBtn = page.locator('.t-btn-zoom-in').nth(0); await bgImageLocator.hover();
const zoomOutBtn = page.locator('.t-btn-zoom-out').nth(0); const zoomInBtn = page.locator('.t-btn-zoom-in');
const initialBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); const zoomOutBtn = page.locator('.t-btn-zoom-out');
const initialBoundingBox = await bgImageLocator.boundingBox();
await zoomInBtn.click(); await zoomInBtn.click();
await zoomInBtn.click(); await zoomInBtn.click();
// wait for zoom animation to finish // wait for zoom animation to finish
await page.locator(backgroundImageSelector).hover({trial: true}); await bgImageLocator.hover();
const zoomedInBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); const zoomedInBoundingBox = await bgImageLocator.boundingBox();
expect(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height); expect(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
expect(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width); expect(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);
await zoomOutBtn.click(); await zoomOutBtn.click();
// wait for zoom animation to finish // wait for zoom animation to finish
await page.locator(backgroundImageSelector).hover({trial: true}); await bgImageLocator.hover();
const zoomedOutBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); const zoomedOutBoundingBox = await bgImageLocator.boundingBox();
expect(zoomedOutBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height); expect(zoomedOutBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height);
expect(zoomedOutBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width); expect(zoomedOutBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width);
}); });
test('Can use the reset button to reset the image', async ({ page }) => { test('Can use the reset button to reset the image', async ({ page }) => {
const bgImageLocator = page.locator(backgroundImageSelector);
// wait for zoom animation to finish // wait for zoom animation to finish
await page.locator(backgroundImageSelector).hover({trial: true}); await bgImageLocator.hover();
const zoomInBtn = page.locator('.t-btn-zoom-in').nth(0); const zoomInBtn = page.locator('.t-btn-zoom-in');
const zoomResetBtn = page.locator('.t-btn-zoom-reset').nth(0); const zoomResetBtn = page.locator('.t-btn-zoom-reset');
const initialBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); const initialBoundingBox = await bgImageLocator.boundingBox();
await zoomInBtn.click(); await zoomInBtn.click();
// wait for zoom animation to finish // wait for zoom animation to finish
await page.locator(backgroundImageSelector).hover({trial: true}); await bgImageLocator.hover();
await zoomInBtn.click(); await zoomInBtn.click();
// wait for zoom animation to finish // wait for zoom animation to finish
await page.locator(backgroundImageSelector).hover({trial: true}); await bgImageLocator.hover();
const zoomedInBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); const zoomedInBoundingBox = await bgImageLocator.boundingBox();
expect.soft(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height); expect.soft(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
expect.soft(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width); expect.soft(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);
await zoomResetBtn.click(); await zoomResetBtn.click();
// wait for zoom animation to finish // wait for zoom animation to finish
await page.locator(backgroundImageSelector).hover({trial: true}); await bgImageLocator.hover();
const resetBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); const resetBoundingBox = await bgImageLocator.boundingBox();
expect.soft(resetBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height); expect.soft(resetBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height);
expect.soft(resetBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width); expect.soft(resetBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width);
@@ -204,20 +206,21 @@ test.describe('Example Imagery Object', () => {
}); });
test('Using the zoom features does not pause telemetry', async ({ page }) => { test('Using the zoom features does not pause telemetry', async ({ page }) => {
const bgImageLocator = page.locator(backgroundImageSelector);
const pausePlayButton = page.locator('.c-button.pause-play'); const pausePlayButton = page.locator('.c-button.pause-play');
// wait for zoom animation to finish // wait for zoom animation to finish
await page.locator(backgroundImageSelector).hover({trial: true}); await bgImageLocator.hover();
// open the time conductor drop down // open the time conductor drop down
await page.locator('button:has-text("Fixed Timespan")').click(); await page.locator('.c-conductor__controls button.c-mode-button').click();
// Click local clock // Click local clock
await page.locator('[data-testid="conductor-modeOption-realtime"]').click(); await page.locator('.icon-clock >> text=Local Clock').click();
await expect.soft(pausePlayButton).not.toHaveClass(/is-paused/); await expect.soft(pausePlayButton).not.toHaveClass(/is-paused/);
const zoomInBtn = page.locator('.t-btn-zoom-in').nth(0); const zoomInBtn = page.locator('.t-btn-zoom-in');
await zoomInBtn.click(); await zoomInBtn.click();
// wait for zoom animation to finish // wait for zoom animation to finish
await page.locator(backgroundImageSelector).hover({trial: true}); await bgImageLocator.hover();
return expect(pausePlayButton).not.toHaveClass(/is-paused/); return expect(pausePlayButton).not.toHaveClass(/is-paused/);
}); });
@@ -230,8 +233,8 @@ test.describe('Example Imagery Object', () => {
// ('Clicking on the left arrow should pause the imagery and go to previous image'); // ('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 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'); // ('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 }) => { const backgroundImageSelector = '.c-imagery__main-image__background-image';
test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox'); test('Example Imagery in Display layout', async ({ page }) => {
test.info().annotations.push({ test.info().annotations.push({
type: 'issue', type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5265' description: 'https://github.com/nasa/openmct/issues/5265'
@@ -263,7 +266,8 @@ test('Example Imagery in Display layout', async ({ page, browserName }) => {
// Wait until Save Banner is gone // Wait until Save Banner is gone
await page.waitForSelector('.c-message-banner__message', { state: 'detached'}); await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery'); await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');
await page.locator(backgroundImageSelector).hover({trial: true}); const bgImageLocator = page.locator(backgroundImageSelector);
await bgImageLocator.hover();
// Click previous image button // Click previous image button
const previousImageButton = page.locator('.c-nav--prev'); const previousImageButton = page.locator('.c-nav--prev');
@@ -275,15 +279,15 @@ test('Example Imagery in Display layout', async ({ page, browserName }) => {
// Zoom in // Zoom in
const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox(); const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox();
await page.locator(backgroundImageSelector).hover({trial: true}); await bgImageLocator.hover();
const deltaYStep = 100; // equivalent to 1x zoom const deltaYStep = 100; // equivalent to 1x zoom
await page.mouse.wheel(0, deltaYStep * 2); await page.mouse.wheel(0, deltaYStep * 2);
const zoomedBoundingBox = await page.locator(backgroundImageSelector).boundingBox(); const zoomedBoundingBox = await bgImageLocator.boundingBox();
const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2; const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;
const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2; const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
// Wait for zoom animation to finish // Wait for zoom animation to finish
await page.locator(backgroundImageSelector).hover({trial: true}); await bgImageLocator.hover();
const imageMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox(); const imageMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox();
expect(imageMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height); expect(imageMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height);
expect(imageMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width); expect(imageMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width);
@@ -307,11 +311,11 @@ test('Example Imagery in Display layout', async ({ page, browserName }) => {
await page.locator('[data-testid=conductor-modeOption-realtime]').click(); await page.locator('[data-testid=conductor-modeOption-realtime]').click();
// Zoom in on next image // Zoom in on next image
await page.locator(backgroundImageSelector).hover({trial: true}); await bgImageLocator.hover();
await page.mouse.wheel(0, deltaYStep * 2); await page.mouse.wheel(0, deltaYStep * 2);
// Wait for zoom animation to finish // Wait for zoom animation to finish
await page.locator(backgroundImageSelector).hover({trial: true}); await bgImageLocator.hover();
const imageNextMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox(); const imageNextMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox();
expect(imageNextMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height); expect(imageNextMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height);
expect(imageNextMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width); expect(imageNextMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width);
@@ -328,15 +332,42 @@ test('Example Imagery in Display layout', async ({ page, browserName }) => {
return newImageCount; return newImageCount;
}, { }, {
message: "verify that old images are discarded", message: "verify that new images still stream in",
timeout: 6 * 1000 timeout: 6 * 1000
}).toBe(imageCount); }).toBeGreaterThan(imageCount);
// Verify selected image is still displayed // Verify selected image is still displayed
await expect(selectedImage).toBeVisible(); await expect(selectedImage).toBeVisible();
// Unpause imagery
await page.locator('.pause-play').click();
//Get background-image url from background-image css prop
const backgroundImage = page.locator('.c-imagery__main-image__background-image');
let backgroundImageUrl = await backgroundImage.evaluate((el) => {
return window.getComputedStyle(el).getPropertyValue('background-image').match(/url\(([^)]+)\)/)[1];
});
let backgroundImageUrl1 = backgroundImageUrl.slice(1, -1); //forgive me, padre
console.log('backgroundImageUrl1 ' + backgroundImageUrl1);
let backgroundImageUrl2;
await expect.poll(async () => {
// Verify next image has updated
let backgroundImageUrlNext = await backgroundImage.evaluate((el) => {
return window.getComputedStyle(el).getPropertyValue('background-image').match(/url\(([^)]+)\)/)[1];
});
backgroundImageUrl2 = backgroundImageUrlNext.slice(1, -1); //forgive me, padre
return backgroundImageUrl2;
}, {
message: "verify next image has updated",
timeout: 6 * 1000
}).not.toBe(backgroundImageUrl1);
console.log('backgroundImageUrl2 ' + backgroundImageUrl2);
}); });
test.describe('Example imagery thumbnails resize in display layouts', () => { test.describe('Example imagery thumbnails resize in display layouts', () => {
test('Resizing the layout changes thumbnail visibility and size', async ({ page }) => { test('Resizing the layout changes thumbnail visibility and size', async ({ page }) => {
await page.goto('/', { waitUntil: 'networkidle' }); await page.goto('/', { waitUntil: 'networkidle' });
@@ -425,137 +456,12 @@ test.describe('Example imagery thumbnails resize in display layouts', () => {
}); });
test.describe('Example Imagery in Flexible layout', () => { test.describe('Example Imagery in Flexible layout', () => {
test('Example Imagery in Flexible layout', async ({ page, browserName }) => { test.fixme('Can use Mouse Wheel to zoom in and out of previous image');
test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox'); test.fixme('Can use alt+drag to move around image once zoomed in');
test.info().annotations.push({ test.fixme('Can zoom into the latest image and the real-time/fixed-time imagery will pause');
type: 'issue', test.fixme('Clicking on the left arrow should pause the imagery and go to previous image');
description: 'https://github.com/nasa/openmct/issues/5326' 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');
// 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.describe('Example Imagery in Tabs view', () => {
@@ -567,185 +473,3 @@ test.describe('Example Imagery in Tabs view', () => {
test.fixme('If the imagery view is in pause mode, it should not be updated when new images come in'); test.fixme('If the imagery view is in pause mode, it should not be updated when new images come in');
test.fixme('If the imagery view is not in pause mode, it should be updated when new images come in'); test.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);
}

View File

@@ -1,30 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
// this will be called from the test suite with
// await page.addInitScript({ path: path.join(__dirname, 'addInitRestrictedNotebook.js') });
// it will install the RestrictedNotebook since it is not installed by default
document.addEventListener('DOMContentLoaded', () => {
const openmct = window.openmct;
openmct.install(openmct.plugins.RestrictedNotebook('CUSTOM_NAME'));
});

View File

@@ -1,198 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
This test suite is dedicated to tests which verify the basic operations surrounding Notebooks.
*/
const { test } = require('../../../fixtures');
test.describe('Notebook CRUD Operations', () => {
test.fixme('Can create a Notebook Object', async ({ page }) => {
//Create domain object
//Newly created notebook should have one Section and one page, 'Unnamed Section'/'Unnamed Page'
});
test.fixme('Can update a Notebook Object', async ({ page }) => {});
test.fixme('Can view a perviously created Notebook Object', async ({ page }) => {});
test.fixme('Can Delete a Notebook Object', async ({ page }) => {
// Other than non-persistible objects
});
});
test.describe('Default Notebook', () => {
// General Default Notebook statements
// ## Useful commands:
// 1. - To check default notebook:
// `JSON.parse(localStorage.getItem('notebook-storage'));`
// 1. - Clear default notebook:
// `localStorage.setItem('notebook-storage', null);`
test.fixme('A newly created Notebook is automatically set as the default notebook if no other notebooks exist', async ({ page }) => {
//Create new notebook
//Verify Default Notebook Characteristics
});
test.fixme('A newly created Notebook is automatically set as the default notebook if at least one other notebook exists', async ({ page }) => {
//Create new notebook A
//Create second notebook B
//Verify Non-Default Notebook A Characteristics
//Verify Default Notebook B Characteristics
});
test.fixme('If a default notebook is deleted, the second most recent notebook becomes the default', async ({ page }) => {
//Create new notebook A
//Create second notebook B
//Delete Notebook B
//Verify Default Notebook A Characteristics
});
});
test.describe('Notebook section tests', () => {
//The following test cases are associated with Notebook Sections
test.fixme('New sections are automatically named Unnamed Section with Unnamed Page', async ({ page }) => {
//Create new notebook A
//Add section
//Verify new section and new page details
});
test.fixme('Section selection operations and associated behavior', async ({ page }) => {
//Create new notebook A
//Add Sections until 6 total with no default section/page
//Select 3rd section
//Delete 4th section
//3rd section is still selected
//Delete 3rd section
//1st section is selected
//Set 3rd section as default
//Delete 2nd section
//3rd section is still default
//Delete 3rd section
//1st is selected and there is no default notebook
});
});
test.describe('Notebook page tests', () => {
//The following test cases are associated with Notebook Pages
test.fixme('Page selection operations and associated behavior', async ({ page }) => {
//Create new notebook A
//Delete existing Page
//New 'Unnamed Page' automatically created
//Create 6 total Pages without a default page
//Select 3rd
//Delete 3rd
//First is now selected
//Set 3rd as default
//Select 2nd page
//Delete 2nd page
//3rd (default) is now selected
//Set 3rd as default page
//Select 3rd (default) page
//Delete 3rd page
//First is now selected and there is no default notebook
});
});
test.describe('Notebook search tests', () => {
test.fixme('Can search for a single result', async ({ page }) => {});
test.fixme('Can search for many results', async ({ page }) => {});
test.fixme('Can search for new and recently modified entries', async ({ page }) => {});
test.fixme('Can search for section text', async ({ page }) => {});
test.fixme('Can search for page text', async ({ page }) => {});
test.fixme('Can search for entry text', async ({ page }) => {});
});
test.describe('Notebook entry tests', () => {
test.fixme('When a new entry is created, it should be focused', async ({ page }) => {});
test.fixme('When a telemetry object is dropped into a notebook, a new entry is created and it should be focused', async ({ page }) => {
// Drag and drop any telmetry object on 'drop object'
// new entry gets created with telemtry object
});
test.fixme('When a telemetry object is dropped into a notebooks existing entry, it should be focused', async ({ page }) => {
// Drag and drop any telemetry object onto existing entry
// Entry updated with object and snapshot
});
test.fixme('new entries persist through navigation events without save', async ({ page }) => {});
test.fixme('previous and new entries can be deleted', async ({ page }) => {});
});
test.describe('Snapshot Menu tests', () => {
test.fixme('When no default notebook is selected, Snapshot Menu dropdown should only have a single option', async ({ page }) => {
// There should be no default notebook
// Clear default notebook if exists using `localStorage.setItem('notebook-storage', null);`
// refresh page
// Click on 'Notebook Snaphot Menu'
// 'save to Notebook Snapshots' should be only option there
});
test.fixme('When default notebook is updated selected, Snapshot Menu dropdown should list it as the newest option', async ({ page }) => {
// Create 2a notebooks
// Set Notebook A as Default
// Open Snapshot Menu and note that Notebook A is listed
// Close Snapshot Menu
// Set Default Notebook to Notebook B
// Open Snapshot Notebook and note that Notebook B is listed
// Select Default Notebook Option and verify that Snapshot is added to Notebook B
});
test.fixme('Can add Snapshots via Snapshot Menu and details are correct', async ({ page }) => {
//Note this should be a visual test, too
// Create Telemetry object
// Create A notebook with many pages and sections.
// Set page and section defaults to be between first and last of many. i.e. 3 of 5
// Navigate to Telemetry object
// Select Default Notebook Option and verify that Snapshot is added to Notebook A
// Verify Snapshot Details appear correctly
});
test.fixme('Snapshots adjust time conductor', async ({ page }) => {
// Create Telemetry object
// Set Telemetry object's timeconductor to Fixed time with Start and Endtimes are recorded
// Embed Telemetry object into notebook
// Set Time Conductor to Local clock
// Click into embedded telemetry object and verify object appears with same fixed time from record
});
});
test.describe('Snapshot Container tests', () => {
test.fixme('5 Snapshots can be added to a container', async ({ page }) => {});
test.fixme('5 Snapshots can be added to a container and Deleted with Delete All action', async ({ page }) => {});
test.fixme('A snapshot can be Deleted from Container', async ({ page }) => {});
test.fixme('A snapshot can be Previewed from Container', async ({ page }) => {});
test.fixme('A snapshot Container can be open and closed', async ({ page }) => {});
test.fixme('Can add object to Snapshot container and pull into notebook and create a new entry', async ({ page }) => {
//Create Notebook
//Create Telemetry Object
//From Telemetry Object, use 'save to Notebook Snapshots'
//Snapshots indicator should blink, click on it to view snapshots
//Navigate to Notebook
//Drag and Drop onto droppable area for new entry
//New Entry created with given snapshot added
//Snapshot removed from container?
});
test.fixme('Can add object to Snapshot container and pull into notebook and existing entry', async ({ page }) => {
//Create Notebook
//Create Telemetry Object
//From Telemetry Object, use 'save to Notebook Snapshots'
//Snapshots indicator should blink, click on it to view snapshots
//Navigate to Notebook
//Drag and Drop into exiting entry
//Existing Entry updated with given snapshot
//Snapshot removed from container?
});
test.fixme('Verify Embedded options for PNG, JPG, and Annotate work correctly', async ({ page }) => {
//Add snapshot to container
//Verify PNG, JPG, and Annotate buttons work correctly
});
});

View File

@@ -1,262 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
const { test } = require('../../../fixtures');
const { expect } = require('@playwright/test');
const path = require('path');
const TEST_TEXT = 'Testing text for entries.';
const TEST_TEXT_NAME = 'Test Page';
const CUSTOM_NAME = 'CUSTOM_NAME';
const NOTEBOOK_DROP_AREA = '.c-notebook__drag-area';
test.describe('Restricted Notebook', () => {
test.beforeEach(async ({ page }) => {
await startAndAddRestrictedNotebookObject(page);
});
test('Can be renamed @addInit', async ({ page }) => {
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText(`Unnamed ${CUSTOM_NAME}`);
});
test('Can be deleted if there are no locked pages @addInit', async ({ page }) => {
await openContextMenuRestrictedNotebook(page);
const menuOptions = page.locator('.c-menu ul');
await expect.soft(menuOptions).toContainText('Remove');
const restrictedNotebookTreeObject = page.locator(`a:has-text("Unnamed ${CUSTOM_NAME}")`);
// notbook tree object exists
expect.soft(await restrictedNotebookTreeObject.count()).toEqual(1);
// Click Remove Text
await page.locator('text=Remove').click();
//Wait until Save Banner is gone
await Promise.all([
page.waitForNavigation(),
page.locator('text=OK').click(),
page.waitForSelector('.c-message-banner__message')
]);
await page.locator('.c-message-banner__close-button').click();
// has been deleted
expect.soft(await restrictedNotebookTreeObject.count()).toEqual(0);
});
test('Can be locked if at least one page has one entry @addInit', async ({ page }) => {
await enterTextEntry(page);
const commitButton = page.locator('button:has-text("Commit Entries")');
expect.soft(await commitButton.count()).toEqual(1);
});
});
test.describe('Restricted Notebook with at least one entry and with the page locked @addInit', () => {
test.beforeEach(async ({ page }) => {
await startAndAddRestrictedNotebookObject(page);
await enterTextEntry(page);
await lockPage(page);
// open sidebar
await page.locator('button.c-notebook__toggle-nav-button').click();
});
test('Locked page should now be in a locked state @addInit', async ({ page }) => {
// main lock message on page
const lockMessage = page.locator('text=This page has been committed and cannot be modified or removed');
expect.soft(await lockMessage.count()).toEqual(1);
// lock icon on page in sidebar
const pageLockIcon = page.locator('ul.c-notebook__pages li div.icon-lock');
expect.soft(await pageLockIcon.count()).toEqual(1);
// no way to remove a restricted notebook with a locked page
await openContextMenuRestrictedNotebook(page);
const menuOptions = page.locator('.c-menu ul');
await expect.soft(menuOptions).not.toContainText('Remove');
});
test('Can still: add page, rename, add entry, delete unlocked pages @addInit', async ({ page }) => {
// Click text=Page Add >> button
await Promise.all([
page.waitForNavigation(),
page.locator('text=Page Add >> button').click()
]);
// Click text=Unnamed Page >> nth=1
await page.locator('text=Unnamed Page').nth(1).click();
// Press a with modifiers
await page.locator('text=Unnamed Page').nth(1).fill(TEST_TEXT_NAME);
// expect to be able to rename unlocked pages
const newPageElement = page.locator(`text=${TEST_TEXT_NAME}`);
const newPageCount = await newPageElement.count();
await newPageElement.press('Enter'); // exit contenteditable state
expect.soft(newPageCount).toEqual(1);
// enter test text
await enterTextEntry(page);
// expect new page to be lockable
const commitButton = page.locator('BUTTON:HAS-TEXT("COMMIT ENTRIES")');
expect.soft(await commitButton.count()).toEqual(1);
// Click text=Unnamed PageTest Page >> button
await page.locator('text=Unnamed PageTest Page >> button').click();
// Click text=Delete Page
await page.locator('text=Delete Page').click();
// Click text=Ok
await Promise.all([
page.waitForNavigation(),
page.locator('text=Ok').click()
]);
// deleted page, should no longer exist
const deletedPageElement = page.locator(`text=${TEST_TEXT_NAME}`);
expect.soft(await deletedPageElement.count()).toEqual(0);
});
});
test.describe('Restricted Notebook with a page locked and with an embed @addInit', () => {
test.beforeEach(async ({ page }) => {
await startAndAddRestrictedNotebookObject(page);
await dragAndDropEmbed(page);
});
test('Allows embeds to be deleted if page unlocked @addInit', async ({ page }) => {
// Click .c-ne__embed__name .c-popup-menu-button
await page.locator('.c-ne__embed__name .c-popup-menu-button').click(); // embed popup menu
const embedMenu = page.locator('body >> .c-menu');
await expect.soft(embedMenu).toContainText('Remove This Embed');
});
test('Disallows embeds to be deleted if page locked @addInit', async ({ page }) => {
await lockPage(page);
// Click .c-ne__embed__name .c-popup-menu-button
await page.locator('.c-ne__embed__name .c-popup-menu-button').click(); // embed popup menu
const embedMenu = page.locator('body >> .c-menu');
await expect.soft(embedMenu).not.toContainText('Remove This Embed');
});
});
/**
* @param {import('@playwright/test').Page} page
*/
async function startAndAddRestrictedNotebookObject(page) {
// eslint-disable-next-line no-undef
await page.addInitScript({ path: path.join(__dirname, 'addInitRestrictedNotebook.js') });
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
//Click the Create button
await page.click('button:has-text("Create")');
// Click text=CUSTOME_NAME
await page.click(`text=${CUSTOM_NAME}`); // secondarily tests renamability also
// Click text=OK
await Promise.all([
page.waitForNavigation({waitUntil: 'networkidle'}),
page.click('text=OK')
]);
}
/**
* @param {import('@playwright/test').Page} page
*/
async function enterTextEntry(page) {
// Click .c-notebook__drag-area
await page.locator(NOTEBOOK_DROP_AREA).click();
// enter text
await page.locator('div.c-ne__text').click();
await page.locator('div.c-ne__text').fill(TEST_TEXT);
await page.locator('div.c-ne__text').press('Enter');
}
/**
* @param {import('@playwright/test').Page} page
*/
async function dragAndDropEmbed(page) {
// Click button:has-text("Create")
await page.locator('button:has-text("Create")').click();
// Click li:has-text("Sine Wave Generator")
await page.locator('li:has-text("Sine Wave Generator")').click();
// Click form[name="mctForm"] >> text=My Items
await page.locator('form[name="mctForm"] >> text=My Items').click();
// Click text=OK
await page.locator('text=OK').click();
// Click text=Open MCT My Items >> span >> nth=3
await page.locator('text=Open MCT My Items >> span').nth(3).click();
// Click text=Unnamed CUSTOM_NAME
await Promise.all([
page.waitForNavigation(),
page.locator('text=Unnamed CUSTOM_NAME').click()
]);
await page.dragAndDrop('text=UNNAMED SINE WAVE GENERATOR', NOTEBOOK_DROP_AREA);
}
/**
* @param {import('@playwright/test').Page} page
*/
async function lockPage(page) {
const commitButton = page.locator('button:has-text("Commit Entries")');
await commitButton.click();
//Wait until Lock Banner is visible
await Promise.all([
page.locator('text=Lock Page').click(),
page.waitForSelector('.c-message-banner__message')
]);
// Close Lock Banner
await page.locator('.c-message-banner__close-button').click();
//artifically wait to avoid mutation delay TODO: https://github.com/nasa/openmct/issues/5409
// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(1 * 1000);
}
/**
* @param {import('@playwright/test').Page} page
*/
async function openContextMenuRestrictedNotebook(page) {
// Click text=Open MCT My Items (This expands the My Items folder to show it's chilren in the tree)
await page.locator('text=Open MCT My Items >> span').nth(3).click();
//artifically wait to avoid mutation delay TODO: https://github.com/nasa/openmct/issues/5409
// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(1 * 1000);
// Click a:has-text("Unnamed CUSTOM_NAME")
await page.locator(`a:has-text("Unnamed ${CUSTOM_NAME}")`).click({
button: 'right'
});
}

View File

@@ -1,205 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
This test suite is dedicated to tests which verify form functionality.
*/
const { expect } = require('@playwright/test');
const { test } = require('../../../fixtures');
/**
* Creates a notebook object and adds an entry.
* @param {import('@playwright/test').Page} - page to load
* @param {number} [iterations = 1] - the number of entries to create
*/
async function createNotebookAndEntry(page, iterations = 1) {
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
// Click button:has-text("Create")
await page.locator('button:has-text("Create")').click();
await page.locator('[title="Create and save timestamped notes with embedded object snapshots."]').click();
// Click button:has-text("OK")
await Promise.all([
page.waitForNavigation(),
page.locator('[name="mctForm"] >> text=My Items').click(),
page.locator('button:has-text("OK")').click()
]);
for (let iteration = 0; iteration < iterations; iteration++) {
// Click text=To start a new entry, click here or drag and drop any object
await page.locator('text=To start a new entry, click here or drag and drop any object').click();
const entryLocator = `[aria-label="Notebook Entry Input"] >> nth = ${iteration}`;
await page.locator(entryLocator).click();
await page.locator(entryLocator).fill(`Entry ${iteration}`);
}
}
/**
* Creates a notebook object, adds an entry, and adds a tag.
* @param {import('@playwright/test').Page} page
* @param {number} [iterations = 1] - the number of entries (and tags) to create
*/
async function createNotebookEntryAndTags(page, iterations = 1) {
await createNotebookAndEntry(page, iterations);
for (let iteration = 0; iteration < iterations; iteration++) {
// Click text=To start a new entry, click here or drag and drop any object
await page.locator(`button:has-text("Add Tag") >> nth = ${iteration}`).click();
// Click [placeholder="Type to select tag"]
await page.locator('[placeholder="Type to select tag"]').click();
// Click text=Driving
await page.locator('[aria-label="Autocomplete Options"] >> text=Driving').click();
// Click button:has-text("Add Tag")
await page.locator(`button:has-text("Add Tag") >> nth = ${iteration}`).click();
// Click [placeholder="Type to select tag"]
await page.locator('[placeholder="Type to select tag"]').click();
// Click text=Science
await page.locator('[aria-label="Autocomplete Options"] >> text=Science').click();
}
}
test.describe('Tagging in Notebooks', () => {
test('Can load tags', async ({ page }) => {
await createNotebookAndEntry(page);
// Click text=To start a new entry, click here or drag and drop any object
await page.locator('button:has-text("Add Tag")').click();
// Click [placeholder="Type to select tag"]
await page.locator('[placeholder="Type to select tag"]').click();
await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Science");
await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Drilling");
await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Driving");
});
test('Can add tags', async ({ page }) => {
await createNotebookEntryAndTags(page);
await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Science");
await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Driving");
// Click button:has-text("Add Tag")
await page.locator('button:has-text("Add Tag")').click();
// Click [placeholder="Type to select tag"]
await page.locator('[placeholder="Type to select tag"]').click();
await expect(page.locator('[aria-label="Autocomplete Options"]')).not.toContainText("Science");
await expect(page.locator('[aria-label="Autocomplete Options"]')).not.toContainText("Driving");
await expect(page.locator('[aria-label="Autocomplete Options"]')).toContainText("Drilling");
});
test('Can search for tags', async ({ page }) => {
await createNotebookEntryAndTags(page);
// Click [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
// Fill [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc');
await expect(page.locator('[aria-label="Search Result"]')).toContainText("Science");
await expect(page.locator('[aria-label="Search Result"]')).toContainText("Driving");
// Click [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
// Fill [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Sc');
await expect(page.locator('[aria-label="Search Result"]')).toContainText("Science");
await expect(page.locator('[aria-label="Search Result"]')).toContainText("Driving");
// Click [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
// Fill [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Xq');
await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible();
await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible();
});
test('Can delete tags', async ({ page }) => {
await createNotebookEntryAndTags(page);
await page.locator('[aria-label="Notebook Entries"]').click();
// Delete Driving
await page.locator('text=Science Driving Add Tag >> button').nth(1).click();
await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Science");
await expect(page.locator('[aria-label="Notebook Entry"]')).not.toContainText("Driving");
// Fill [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('sc');
await expect(page.locator('[aria-label="Search Result"]')).not.toContainText("Driving");
});
test('Tags persist across reload', async ({ page }) => {
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
// Create a clock object we can navigate to
await page.click('button:has-text("Create")');
// Click Clock
await page.click('text=Clock');
// Click button:has-text("OK")
await Promise.all([
page.waitForNavigation(),
page.locator('[name="mctForm"] >> text=My Items').click(),
page.locator('button:has-text("OK")').click()
]);
await page.click('.c-disclosure-triangle');
const ITERATIONS = 4;
await createNotebookEntryAndTags(page, ITERATIONS);
for (let iteration = 0; iteration < ITERATIONS; iteration++) {
const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`;
await expect(page.locator(entryLocator)).toContainText("Science");
await expect(page.locator(entryLocator)).toContainText("Driving");
}
// Click Unnamed Clock
await page.click('text="Unnamed Clock"');
// Click Unnamed Notebook
await page.click('text="Unnamed Notebook"');
for (let iteration = 0; iteration < ITERATIONS; iteration++) {
const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`;
await expect(page.locator(entryLocator)).toContainText("Science");
await expect(page.locator(entryLocator)).toContainText("Driving");
}
//Reload Page
await Promise.all([
page.reload(),
page.waitForLoadState('networkidle')
]);
// Click Unnamed Notebook
await page.click('text="Unnamed Notebook"');
for (let iteration = 0; iteration < ITERATIONS; iteration++) {
const entryLocator = `[aria-label="Notebook Entry"] >> nth = ${iteration}`;
await expect(page.locator(entryLocator)).toContainText("Science");
await expect(page.locator(entryLocator)).toContainText("Driving");
}
});
});

View File

@@ -24,9 +24,22 @@
Testsuite for plot autoscale. Testsuite for plot autoscale.
*/ */
const { test } = require('../../../fixtures.js'); const { test: _test } = require('../../../fixtures.js');
const { expect } = require('@playwright/test'); const { expect } = require('@playwright/test');
// create a new `test` API that will not append platform details to snapshot
// file names, only for the tests in this file, so that the same snapshots will
// be used for all platforms.
const test = _test.extend({
_autoSnapshotSuffix: [
async ({}, use, testInfo) => {
testInfo.snapshotSuffix = '';
await use();
},
{ auto: true }
]
});
test.use({ test.use({
viewport: { viewport: {
width: 1280, width: 1280,
@@ -37,7 +50,7 @@ test.use({
test.describe('ExportAsJSON', () => { test.describe('ExportAsJSON', () => {
test('User can set autoscale with a valid range @snapshot', async ({ page }) => { test('User can set autoscale with a valid range @snapshot', async ({ page }) => {
//This is necessary due to the size of the test suite. //This is necessary due to the size of the test suite.
test.slow(); await test.setTimeout(120 * 1000);
await page.goto('/', { waitUntil: 'networkidle' }); await page.goto('/', { waitUntil: 'networkidle' });
@@ -49,16 +62,16 @@ test.describe('ExportAsJSON', () => {
await turnOffAutoscale(page); 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); const canvas = page.locator('canvas').nth(1);
await canvas.hover({trial: true}); // Make sure that after turning off autoscale, the user selected range values start at the same values the plot had prior.
await Promise.all([
testYTicks(page, ['-1.00', '-0.50', '0.00', '0.50', '1.00']),
new Promise(r => setTimeout(r, 100))
.then(() => canvas.screenshot())
.then(shot => expect(shot).toMatchSnapshot('autoscale-canvas-prepan.png', { maxDiffPixels: 40 }))
]);
expect(await canvas.screenshot()).toMatchSnapshot('autoscale-canvas-prepan');
//Alt Drag Start
await page.keyboard.down('Alt'); await page.keyboard.down('Alt');
await canvas.dragTo(canvas, { await canvas.dragTo(canvas, {
@@ -72,15 +85,15 @@ test.describe('ExportAsJSON', () => {
} }
}); });
//Alt Drag End
await page.keyboard.up('Alt'); await page.keyboard.up('Alt');
// Ensure the drag worked. // Ensure the drag worked.
await testYTicks(page, ['0.00', '0.50', '1.00', '1.50', '2.00']); await Promise.all([
testYTicks(page, ['0.00', '0.50', '1.00', '1.50', '2.00']),
await canvas.hover({trial: true}); new Promise(r => setTimeout(r, 100))
.then(() => canvas.screenshot())
expect(await canvas.screenshot()).toMatchSnapshot('autoscale-canvas-panned'); .then(shot => expect(shot).toMatchSnapshot('autoscale-canvas-panned.png', { maxDiffPixels: 40 }))
]);
}); });
}); });

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -30,8 +30,8 @@ const { expect } = require('@playwright/test');
test.describe('Log plot tests', () => { test.describe('Log plot tests', () => {
test('Log Plot ticks are functionally correct in regular and log mode and after refresh', async ({ page }) => { 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 //This is necessary due to the size of the test suite.
test.slow(); await test.setTimeout(120 * 1000);
await makeOverlayPlot(page); await makeOverlayPlot(page);
await testRegularTicks(page); await testRegularTicks(page);
@@ -44,6 +44,20 @@ test.describe('Log plot tests', () => {
await testLogTicks(page); await testLogTicks(page);
await saveOverlayPlot(page); await saveOverlayPlot(page);
await testLogTicks(page); await testLogTicks(page);
//await testLogPlotPixels(page);
// FIXME: Get rid of the waitForTimeout() and lint warning exception.
// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(1 * 1000);
// refresh page and wait for charts and ticks to load
await page.reload({ waitUntil: 'networkidle'});
await page.waitForSelector('.gl-plot-chart-area');
await page.waitForSelector('.gl-plot-y-tick-label');
// test log ticks hold up after refresh
await testLogTicks(page);
//await testLogPlotPixels(page);
}); });
// Leaving test as 'TODO' for now. // Leaving test as 'TODO' for now.
@@ -107,14 +121,14 @@ async function makeOverlayPlot(page) {
// set amplitude to 6, offset 4, period 2 // set amplitude to 6, offset 4, period 2
await page.locator('div:nth-child(5) .c-form-row__controls .form-control .field input').click(); await page.locator('div:nth-child(5) .form-row .c-form-row__controls .form-control .field input').click();
await page.locator('div:nth-child(5) .c-form-row__controls .form-control .field input').fill('6'); await page.locator('div:nth-child(5) .form-row .c-form-row__controls .form-control .field input').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) .form-row .c-form-row__controls .form-control .field input').click();
await page.locator('div:nth-child(6) .c-form-row__controls .form-control .field input').fill('4'); await page.locator('div:nth-child(6) .form-row .c-form-row__controls .form-control .field input').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) .form-row .c-form-row__controls .form-control .field input').click();
await page.locator('div:nth-child(7) .c-form-row__controls .form-control .field input').fill('2'); await page.locator('div:nth-child(7) .form-row .c-form-row__controls .form-control .field input').fill('2');
// Click OK to make generator // Click OK to make generator

View File

@@ -1,161 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
Tests to verify log plot functionality when objects are missing
*/
const { test } = require('../../../fixtures.js');
const { expect } = require('@playwright/test');
test.describe('Handle missing object for plots', () => {
test('Displays empty div for missing stacked plot item', async ({ page }) => {
const errorLogs = [];
page.on("console", (message) => {
if (message.type() === 'warning') {
errorLogs.push(message.text());
}
});
//Make stacked plot
await makeStackedPlot(page);
//Gets local storage and deletes the last sine wave generator in the stacked plot
const localStorage = await page.evaluate(() => window.localStorage);
const parsedData = JSON.parse(localStorage.mct);
const keys = Object.keys(parsedData);
const lastKey = keys[keys.length - 1];
delete parsedData[lastKey];
//Sets local storage with missing object
await page.evaluate(
`window.localStorage.setItem('mct', '${JSON.stringify(parsedData)}')`
);
//Reloads page and clicks on stacked plot
await Promise.all([
page.reload(),
page.waitForLoadState('networkidle')
]);
//Verify Main section is there on load
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Stacked Plot');
await page.locator('text=Open MCT My Items >> span').nth(3).click();
await Promise.all([
page.waitForNavigation(),
page.locator('text=Unnamed Stacked Plot').first().click()
]);
//Check that there is only one stacked item plot with a plot, the missing one will be empty
await expect(page.locator(".c-plot--stacked-container:has(.gl-plot)")).toHaveCount(1);
//Verify that console.warn is thrown
await expect(errorLogs).toHaveLength(1);
});
});
/**
* This is used the create a stacked plot object
* @private
*/
async function makeStackedPlot(page) {
// fresh page with time range from 2022-03-29 22:00:00.000Z to 2022-03-29 22:00:30.000Z
await page.goto('/', { waitUntil: 'networkidle' });
// create stacked plot
await page.locator('button.c-create-button').click();
await page.locator('li:has-text("Stacked Plot")').click();
await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle'}),
page.locator('text=OK').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
//Wait until Save Banner is gone
await page.locator('.c-message-banner__close-button').click();
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
// save the stacked plot
await saveStackedPlot(page);
// create a sinewave generator
await createSineWaveGenerator(page);
// click on stacked plot
await page.locator('text=Open MCT My Items >> span').nth(3).click();
await Promise.all([
page.waitForNavigation(),
page.locator('text=Unnamed Stacked Plot').first().click()
]);
// create a second sinewave generator
await createSineWaveGenerator(page);
// click on stacked plot
await page.locator('text=Open MCT My Items >> span').nth(3).click();
await Promise.all([
page.waitForNavigation(),
page.locator('text=Unnamed Stacked Plot').first().click()
]);
}
/**
* This is used to save a stacked plot object
* @private
*/
async function saveStackedPlot(page) {
// save stacked plot
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
await Promise.all([
page.locator('text=Save and Finish Editing').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
//Wait until Save Banner is gone
await page.locator('.c-message-banner__close-button').click();
await page.waitForSelector('.c-message-banner__message', { state: 'detached' });
}
/**
* This is used to create a sine wave generator object
* @private
*/
async function createSineWaveGenerator(page) {
//Create sine wave generator
await page.locator('button.c-create-button').click();
await page.locator('li:has-text("Sine Wave Generator")').click();
await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle'}),
page.locator('text=OK').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
//Wait until Save Banner is gone
await page.locator('.c-message-banner__close-button').click();
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
}

View File

@@ -1,41 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
const { test } = require('../../../fixtures.js');
// 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
});
});

View File

@@ -1,104 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
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);
});
});

View File

@@ -1,184 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
const { test } = require('../../../fixtures.js');
const { expect } = require('@playwright/test');
test.describe('Timer', () => {
test.beforeEach(async ({ page }) => {
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
//Click the Create button
await page.click('button:has-text("Create")');
// Click 'Timer'
await page.click('text=Timer');
// Click text=OK
await Promise.all([
page.waitForNavigation({waitUntil: 'networkidle'}),
page.click('text=OK')
]);
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Timer');
});
test('Can perform actions on the Timer', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/4313'
});
await test.step("From the tree context menu", async () => {
await triggerTimerContextMenuAction(page, 'Start');
await triggerTimerContextMenuAction(page, 'Pause');
await triggerTimerContextMenuAction(page, 'Restart at 0');
await triggerTimerContextMenuAction(page, 'Stop');
});
await test.step("From the 3dot menu", async () => {
await triggerTimer3dotMenuAction(page, 'Start');
await triggerTimer3dotMenuAction(page, 'Pause');
await triggerTimer3dotMenuAction(page, 'Restart at 0');
await triggerTimer3dotMenuAction(page, 'Stop');
});
await test.step("From the object view", async () => {
await triggerTimerViewAction(page, 'Start');
await triggerTimerViewAction(page, 'Pause');
await triggerTimerViewAction(page, 'Restart at 0');
});
});
});
/**
* Actions that can be performed on a timer from context menus.
* @typedef {'Start' | 'Stop' | 'Pause' | 'Restart at 0'} TimerAction
*/
/**
* Actions that can be performed on a timer from the object view.
* @typedef {'Start' | 'Pause' | 'Restart at 0'} TimerViewAction
*/
/**
* Open the timer context menu from the object tree.
* Expands the 'My Items' folder if it is not already expanded.
* @param {import('@playwright/test').Page} page
*/
async function openTimerContextMenu(page) {
const myItemsFolder = page.locator('text=Open MCT My Items >> span').nth(3);
const className = await myItemsFolder.getAttribute('class');
if (!className.includes('c-disclosure-triangle--expanded')) {
await myItemsFolder.click();
}
await page.locator(`a:has-text("Unnamed Timer")`).click({
button: 'right'
});
}
/**
* Trigger a timer action from the tree context menu
* @param {import('@playwright/test').Page} page
* @param {TimerAction} action
*/
async function triggerTimerContextMenuAction(page, action) {
const menuAction = `.c-menu ul li >> text="${action}"`;
await openTimerContextMenu(page);
await page.locator(menuAction).click();
assertTimerStateAfterAction(page, action);
}
/**
* Trigger a timer action from the 3dot menu
* @param {import('@playwright/test').Page} page
* @param {TimerAction} action
*/
async function triggerTimer3dotMenuAction(page, action) {
const menuAction = `.c-menu ul li >> text="${action}"`;
const threeDotMenuButton = 'button[title="More options"]';
let isActionAvailable = false;
let iterations = 0;
// Dismiss/open the 3dot menu until the action is available
// or a maxiumum number of iterations is reached
while (!isActionAvailable && iterations <= 20) {
await page.click('.c-object-view');
await page.click(threeDotMenuButton);
isActionAvailable = await page.locator(menuAction).isVisible();
iterations++;
}
await page.locator(menuAction).click();
assertTimerStateAfterAction(page, action);
}
/**
* Trigger a timer action from the object view
* @param {import('@playwright/test').Page} page
* @param {TimerViewAction} action
*/
async function triggerTimerViewAction(page, action) {
const buttonTitle = buttonTitleFromAction(action);
await page.click(`button[title="${buttonTitle}"]`);
assertTimerStateAfterAction(page, action);
}
/**
* Takes in a TimerViewAction and returns the button title
* @param {TimerViewAction} action
*/
function buttonTitleFromAction(action) {
switch (action) {
case 'Start':
return 'Start';
case 'Pause':
return 'Pause';
case 'Restart at 0':
return 'Reset';
}
}
/**
* Verify the timer state after a timer action has been performed.
* @param {import('@playwright/test').Page} page
* @param {TimerAction} action
*/
async function assertTimerStateAfterAction(page, action) {
let timerStateClass;
switch (action) {
case 'Start':
case 'Restart at 0':
timerStateClass = "is-started";
break;
case 'Stop':
timerStateClass = 'is-stopped';
break;
case 'Pause':
timerStateClass = 'is-paused';
break;
}
await expect.soft(page.locator('.c-timer')).toHaveClass(new RegExp(timerStateClass));
}

View File

@@ -0,0 +1,22 @@
{
"cookies": [],
"origins": [
{
"origin": "http://localhost:8080",
"localStorage": [
{
"name": "tcHistory",
"value": "{\"utc\":[{\"start\":1651513945533,\"end\":1651515745533}]}"
},
{
"name": "mct",
"value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"c0f99e39-85e7-4ef7-99b1-ef52d4ed69b2\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1651515746374,\"modified\":1651515746374},\"c0f99e39-85e7-4ef7-99b1-ef52d4ed69b2\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"c0f99e39-85e7-4ef7-99b1-ef52d4ed69b2\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"e35a066b-eb0e-4b05-a4c9-cc31dc202572\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1651515746373,\"location\":\"mine\",\"persisted\":1651515746373}}"
},
{
"name": "mct-tree-expanded",
"value": "[]"
}
]
}
]
}

View File

@@ -45,15 +45,6 @@ test('Verify that the create button appears and that the Folder Domain Object is
await page.click('button:has-text("Create")'); await page.click('button:has-text("Create")');
// Verify that Create Folder appears in the dropdown // Verify that Create Folder appears in the dropdown
await expect(page.locator(':nth-match(:text("Folder"), 2)')).toBeEnabled(); const locator = page.locator(':nth-match(:text("Folder"), 2)');
}); await expect(locator).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();
}); });

View File

@@ -1,111 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
This test suite is dedicated to tests which verify search functionality.
*/
const { expect } = require('@playwright/test');
const { test } = require('../../../../fixtures');
/**
* Creates a notebook object and adds an entry.
* @param {import('@playwright/test').Page} page
*/
async function createClockAndDisplayLayout(page) {
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
// Click button:has-text("Create")
await page.locator('button:has-text("Create")').click();
// Click li:has-text("Notebook")
await page.locator('li:has-text("Clock")').click();
// Click button:has-text("OK")
await Promise.all([
page.waitForNavigation(),
page.locator('button:has-text("OK")').click()
]);
// Click a:has-text("My Items")
await Promise.all([
page.waitForNavigation(),
page.locator('a:has-text("My Items") >> nth=0').click()
]);
// Click button:has-text("Create")
await page.locator('button:has-text("Create")').click();
// Click li:has-text("Notebook")
await page.locator('li:has-text("Display Layout")').click();
// Click button:has-text("OK")
await Promise.all([
page.waitForNavigation(),
page.locator('button:has-text("OK")').click()
]);
}
test.describe('Grand Search', () => {
test('Can search for objects, and subsequent search dropdown behaves properly', async ({ page }) => {
await createClockAndDisplayLayout(page);
// Click [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
// Fill [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Cl');
await expect(page.locator('[aria-label="Search Result"]')).toContainText('Clock');
// Click text=Elements >> nth=0
await page.locator('text=Elements').first().click();
await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible();
// Click [aria-label="OpenMCT Search"] [aria-label="Search Input"]
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click();
// Click [aria-label="Unnamed Clock clock result"] >> text=Unnamed Clock
await page.locator('[aria-label="Unnamed Clock clock result"] >> text=Unnamed Clock').click();
await expect(page.locator('.js-preview-window')).toBeVisible();
// Click [aria-label="Close"]
await page.locator('[aria-label="Close"]').click();
await expect(page.locator('[aria-label="Search Result"]')).toBeVisible();
await expect(page.locator('[aria-label="Search Result"]')).toContainText('Cloc');
// Click [aria-label="OpenMCT Search"] a >> nth=0
await page.locator('[aria-label="OpenMCT Search"] a').first().click();
await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible();
// Fill [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('foo');
await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible();
// Click text=Snapshot Save and Finish Editing Save and Continue Editing >> button >> nth=1
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
// Click text=Save and Finish Editing
await page.locator('text=Save and Finish Editing').click();
// Click [aria-label="OpenMCT Search"] [aria-label="Search Input"]
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click();
// Fill [aria-label="OpenMCT Search"] [aria-label="Search Input"]
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').fill('Cl');
// Click text=Unnamed Clock
await Promise.all([
page.waitForNavigation(),
page.locator('text=Unnamed Clock').click()
]);
await expect(page.locator('.is-object-type-clock')).toBeVisible();
});
});

View File

@@ -1,76 +0,0 @@
/* eslint-disable no-undef */
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
Collection of Visual Tests set to run with modified init scripts to inject plugins not otherwise available in the default contexts.
These should only use functional expect statements to verify assumptions about the state
in a test and not for functional verification of correctness. Visual tests are not supposed
to "fail" on assertions. Instead, they should be used to detect changes between builds or branches.
Note: Larger testsuite sizes are OK due to the setup time associated with these tests.
*/
const { test } = require('@playwright/test');
const percySnapshot = require('@percy/playwright');
const path = require('path');
const sinon = require('sinon');
const VISUAL_GRACE_PERIOD = 5 * 1000; //Lets the application "simmer" before the snapshot is taken
const CUSTOM_NAME = 'CUSTOM_NAME';
// Snippet from https://github.com/microsoft/playwright/issues/6347#issuecomment-965887758
// Will replace with cy.clock() equivalent
test.beforeEach(async ({ context }) => {
await context.addInitScript({
path: path.join(__dirname, '../../..', './node_modules/sinon/pkg/sinon.js')
});
await context.addInitScript(() => {
window.__clock = sinon.useFakeTimers({
now: 0,
shouldAdvanceTime: true
}); //Set browser clock to UNIX Epoch
});
});
test('Visual - Restricted Notebook is visually correct @addInit', async ({ page }) => {
// eslint-disable-next-line no-undef
await page.addInitScript({ path: path.join(__dirname, '../plugins/notebook', './addInitRestrictedNotebook.js') });
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
//Click the Create button
await page.click('button:has-text("Create")');
// Click text=CUSTOM_NAME
await page.click(`text=${CUSTOM_NAME}`);
// Click text=OK
await Promise.all([
page.waitForNavigation({waitUntil: 'networkidle'}),
page.click('text=OK')
]);
// Take a snapshot of the newly created CUSTOM_NAME notebook
await page.waitForTimeout(VISUAL_GRACE_PERIOD);
await percySnapshot(page, 'Restricted Notebook with CUSTOM_NAME');
});

View File

@@ -1,70 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
Collection of Visual Tests set to run in a default context. The tests within this suite
are only meant to run against openmct's app.js started by `npm run start` within the
`./e2e/playwright-visual.config.js` file.
These should only use functional expect statements to verify assumptions about the state
in a test and not for functional verification of correctness. Visual tests are not supposed
to "fail" on assertions. Instead, they should be used to detect changes between builds or branches.
Note: Larger testsuite sizes are OK due to the setup time associated with these tests.
*/
const { test, expect } = require('@playwright/test');
const percySnapshot = require('@percy/playwright');
const path = require('path');
const sinon = require('sinon');
// Snippet from https://github.com/microsoft/playwright/issues/6347#issuecomment-965887758
// Will replace with cy.clock() equivalent
test.beforeEach(async ({ context }) => {
await context.addInitScript({
// eslint-disable-next-line no-undef
path: path.join(__dirname, '../../..', './node_modules/sinon/pkg/sinon.js')
});
await context.addInitScript(() => {
window.__clock = sinon.useFakeTimers({
now: 0, //Set browser clock to UNIX Epoch
shouldAdvanceTime: false, //Don't advance the clock
toFake: ["setTimeout", "nextTick"]
});
});
});
test.use({ storageState: './e2e/test-data/VisualTestData_storage.json' });
test('Visual - Overlay Plot Loading Indicator @localstorage', async ({ page }) => {
// Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
await page.locator('a:has-text("Unnamed Overlay Plot Overlay Plot")').click();
//Ensure that we're on the Unnamed Overlay Plot object
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Overlay Plot');
//Wait for canvas to be rendered and stop animating
await page.locator('canvas >> nth=1').hover({trial: true});
//Take snapshot of Sine Wave Generator within Overlay Plot
await percySnapshot(page, 'SineWaveInOverlayPlot');
});

View File

@@ -32,8 +32,7 @@ to "fail" on assertions. Instead, they should be used to detect changes between
Note: Larger testsuite sizes are OK due to the setup time associated with these tests. Note: Larger testsuite sizes are OK due to the setup time associated with these tests.
*/ */
const { test } = require('../../fixtures.js'); const { test, expect } = require('@playwright/test');
const { expect } = require('@playwright/test');
const percySnapshot = require('@percy/playwright'); const percySnapshot = require('@percy/playwright');
const path = require('path'); const path = require('path');
const sinon = require('sinon'); const sinon = require('sinon');
@@ -97,11 +96,7 @@ test('Visual - Default Condition Set', async ({ page }) => {
await percySnapshot(page, 'Default Condition Set'); await percySnapshot(page, 'Default Condition Set');
}); });
test.fixme('Visual - Default Condition Widget', async ({ page }) => { test('Visual - Default Condition Widget', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5349'
});
//Go to baseURL //Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' }); await page.goto('/', { waitUntil: 'networkidle' });
@@ -197,36 +192,5 @@ test('Visual - Save Successful Banner', async ({ page }) => {
//Wait until Save Banner is gone //Wait until Save Banner is gone
await page.waitForSelector('.c-message-banner__message', { state: 'detached'}); await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
await percySnapshot(page, 'Banner message gone'); 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');
});

View File

@@ -1,95 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
This test suite is dedicated to generating LocalStorage via Session Storage to be used
in some visual test suites like controlledClock.visual.spec.js. This suite should run to completion
and generate an artifact named ./e2e/test-data/VisualTestData_storage.json . This will run
on every Commit to ensure that this object still loads into tests correctly and will retain the
.e2e.spec.js suffix.
TODO: Provide additional validation of object properties as it grows.
*/
const { test } = require('../../fixtures.js');
const { expect } = require('@playwright/test');
test('Generate Visual Test Data @localStorage', async ({ page, context }) => {
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
await page.locator('button:has-text("Create")').click();
// add overlay plot with defaults
await page.locator('li:has-text("Overlay Plot")').click();
// Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184
await page.click('form[name="mctForm"] a:has-text("My Items")');
await Promise.all([
page.waitForNavigation(),
page.locator('text=OK').click(),
//Wait for Save Banner to appear1
page.waitForSelector('.c-message-banner__message')
]);
//Wait until Save Banner is gone
await page.locator('.c-message-banner__close-button').click();
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
// save (exit edit mode)
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
await page.locator('text=Save and Finish Editing').click();
// click create button
await page.locator('button:has-text("Create")').click();
// add sine wave generator with defaults
await page.locator('li:has-text("Sine Wave Generator")').click();
//Add a 5000 ms Delay
await page.locator('[aria-label="Loading Delay \\(ms\\)"]').fill('5000');
// Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184
await page.click('form[name="mctForm"] a:has-text("Overlay Plot")');
await Promise.all([
page.waitForNavigation(),
page.locator('text=OK').click(),
//Wait for Save Banner to appear1
page.waitForSelector('.c-message-banner__message')
]);
//Wait until Save Banner is gone
await page.locator('.c-message-banner__close-button').click();
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
// focus the overlay plot
await page.locator('text=Open MCT My Items >> span').nth(3).click();
await Promise.all([
page.waitForNavigation(),
page.locator('text=Unnamed Overlay Plot').first().click()
]);
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Overlay Plot');
//Save localStorage for future test execution
await context.storageState({ path: './e2e/test-data/VisualTestData_storage.json' });
});

View File

@@ -1,104 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
This test suite is dedicated to tests which verify search functionality.
*/
const { test, expect } = require('@playwright/test');
const percySnapshot = require('@percy/playwright');
/**
* Creates a notebook object and adds an entry.
* @param {import('@playwright/test').Page} page
*/
async function createClockAndDisplayLayout(page) {
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
// Click button:has-text("Create")
await page.locator('button:has-text("Create")').click();
// Click li:has-text("Notebook")
await page.locator('li:has-text("Clock")').click();
// Click button:has-text("OK")
await Promise.all([
page.waitForNavigation(),
page.locator('button:has-text("OK")').click()
]);
// Click a:has-text("My Items")
await Promise.all([
page.waitForNavigation(),
page.locator('a:has-text("My Items") >> nth=0').click()
]);
// Click button:has-text("Create")
await page.locator('button:has-text("Create")').click();
// Click li:has-text("Notebook")
await page.locator('li:has-text("Display Layout")').click();
// Click button:has-text("OK")
await Promise.all([
page.waitForNavigation(),
page.locator('button:has-text("OK")').click()
]);
}
test.describe('Grand Search', () => {
test('Can search for objects, and subsequent search dropdown behaves properly', async ({ page }) => {
await createClockAndDisplayLayout(page);
// Click [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').click();
// Fill [aria-label="OpenMCT Search"] input[type="search"]
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Cl');
await expect(page.locator('[aria-label="Search Result"]')).toContainText('Clock');
await percySnapshot(page, 'Searching for Clocks');
// Click text=Elements >> nth=0
await page.locator('text=Elements').first().click();
await expect(page.locator('[aria-label="Search Result"]')).not.toBeVisible();
// Click [aria-label="OpenMCT Search"] [aria-label="Search Input"]
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click();
// Click [aria-label="Unnamed Clock clock result"] >> text=Unnamed Clock
await page.locator('[aria-label="Unnamed Clock clock result"] >> text=Unnamed Clock').click();
await percySnapshot(page, 'Preview for clock should display when editing enabled and search item clicked');
// Click [aria-label="Close"]
await page.locator('[aria-label="Close"]').click();
await percySnapshot(page, 'Search should still be showing after preview closed');
// Click text=Snapshot Save and Finish Editing Save and Continue Editing >> button >> nth=1
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
// Click text=Save and Finish Editing
await page.locator('text=Save and Finish Editing').click();
// Click [aria-label="OpenMCT Search"] [aria-label="Search Input"]
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').click();
// Fill [aria-label="OpenMCT Search"] [aria-label="Search Input"]
await page.locator('[aria-label="OpenMCT Search"] [aria-label="Search Input"]').fill('Cl');
// Click text=Unnamed Clock
await Promise.all([
page.waitForNavigation(),
page.locator('text=Unnamed Clock').click()
]);
await percySnapshot(page, 'Clicking on search results should navigate to them if not editing');
});
});

View File

@@ -1,33 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import availableTags from './tags.json';
/**
* @returns {function} The plugin install function
*/
export default function exampleTagsPlugin() {
return function install(openmct) {
Object.keys(availableTags.tags).forEach(tagKey => {
const tagDefinition = availableTags.tags[tagKey];
openmct.annotation.defineTag(tagKey, tagDefinition);
});
};
}

View File

@@ -1,19 +0,0 @@
{
"tags": {
"46a62ad1-bb86-4f88-9a17-2a029e12669d": {
"label": "Science",
"backgroundColor": "#cc0000",
"foregroundColor": "#ffffff"
},
"65f150ef-73b7-409a-b2e8-258cbd8b7323": {
"label": "Driving",
"backgroundColor": "#ffad32",
"foregroundColor": "#333333"
},
"f156b038-c605-46db-88a6-67cf2489a371": {
"label": "Drilling",
"backgroundColor": "#b0ac4e",
"foregroundColor": "#FFFFFF"
}
}
}

View File

@@ -24,53 +24,16 @@ import EventEmitter from 'EventEmitter';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import createExampleUser from './exampleUserCreator'; import createExampleUser from './exampleUserCreator';
const STATUSES = [{
key: "NO_STATUS",
label: "Not set",
iconClass: "icon-question-mark",
iconClassPoll: "icon-status-poll-question-mark"
}, {
key: "GO",
label: "GO",
iconClass: "icon-check",
iconClassPoll: "icon-status-poll-question-mark",
statusClass: "s-status-ok",
statusBgColor: "#33cc33",
statusFgColor: "#000"
}, {
key: "MAYBE",
label: "MAYBE",
iconClass: "icon-alert-triangle",
iconClassPoll: "icon-status-poll-question-mark",
statusClass: "s-status-warning",
statusBgColor: "#ffb66c",
statusFgColor: "#000"
}, {
key: "NO_GO",
label: "NO GO",
iconClass: "icon-circle-slash",
iconClassPoll: "icon-status-poll-question-mark",
statusClass: "s-status-error",
statusBgColor: "#9900cc",
statusFgColor: "#fff"
}];
/**
* @implements {StatusUserProvider}
*/
export default class ExampleUserProvider extends EventEmitter { export default class ExampleUserProvider extends EventEmitter {
constructor(openmct, {defaultStatusRole} = {defaultStatusRole: undefined}) { constructor(openmct) {
super(); super();
this.openmct = openmct; this.openmct = openmct;
this.user = undefined; this.user = undefined;
this.loggedIn = false; this.loggedIn = false;
this.autoLoginUser = undefined; this.autoLoginUser = undefined;
this.status = STATUSES[1];
this.pollQuestion = undefined;
this.defaultStatusRole = defaultStatusRole;
this.ExampleUser = createExampleUser(this.openmct.user.User); this.ExampleUser = createExampleUser(this.openmct.user.User);
this.loginPromise = undefined;
} }
isLoggedIn() { isLoggedIn() {
@@ -82,19 +45,11 @@ export default class ExampleUserProvider extends EventEmitter {
} }
getCurrentUser() { getCurrentUser() {
if (!this.loginPromise) { if (this.loggedIn) {
this.loginPromise = this._login().then(() => this.user); return Promise.resolve(this.user);
} }
return this.loginPromise; return this._login().then(() => this.user);
}
canProvideStatusForRole() {
return Promise.resolve(true);
}
canSetPollQuestion() {
return Promise.resolve(true);
} }
hasRole(roleId) { hasRole(roleId) {
@@ -105,55 +60,6 @@ export default class ExampleUserProvider extends EventEmitter {
return Promise.resolve(this.user.getRoles().includes(roleId)); return Promise.resolve(this.user.getRoles().includes(roleId));
} }
getStatusRoleForCurrentUser() {
return Promise.resolve(this.defaultStatusRole);
}
getAllStatusRoles() {
return Promise.resolve([this.defaultStatusRole]);
}
getStatusForRole(role) {
return Promise.resolve(this.status);
}
async getDefaultStatusForRole(role) {
const allRoles = await this.getPossibleStatuses();
return allRoles?.[0];
}
setStatusForRole(role, status) {
this.status = status;
this.emit('statusChange', {
role,
status
});
return true;
}
getPollQuestion() {
return Promise.resolve({
question: 'Set "GO" if your position is ready for a boarding action on the Klingon cruiser',
timestamp: Date.now()
});
}
setPollQuestion(pollQuestion) {
this.pollQuestion = {
question: pollQuestion,
timestamp: Date.now()
};
this.emit("pollQuestionChange", this.pollQuestion);
return true;
}
getPossibleStatuses() {
return Promise.resolve(STATUSES);
}
_login() { _login() {
const id = uuid(); const id = uuid();
@@ -202,6 +108,3 @@ export default class ExampleUserProvider extends EventEmitter {
); );
} }
} }
/**
* @typedef {import('@/api/user/StatusUserProvider').default} StatusUserProvider
*/

View File

@@ -22,19 +22,8 @@
import ExampleUserProvider from './ExampleUserProvider'; import ExampleUserProvider from './ExampleUserProvider';
export default function ExampleUserPlugin({autoLoginUser, defaultStatusRole} = { export default function ExampleUserPlugin() {
autoLoginUser: 'guest',
defaultStatusRole: 'test-role'
}) {
return function install(openmct) { return function install(openmct) {
const userProvider = new ExampleUserProvider(openmct, { openmct.user.setProvider(new ExampleUserProvider(openmct));
defaultStatusRole
});
if (autoLoginUser !== undefined) {
userProvider.autoLogin(autoLoginUser);
}
openmct.user.setProvider(userProvider);
}; };
} }

View File

@@ -26,7 +26,7 @@ import {
} from '../../src/utils/testing'; } from '../../src/utils/testing';
import ExampleUserProvider from './ExampleUserProvider'; import ExampleUserProvider from './ExampleUserProvider';
describe("The Example User Plugin", () => { xdescribe("The Example User Plugin", () => {
let openmct; let openmct;
beforeEach(() => { beforeEach(() => {
@@ -47,4 +47,9 @@ describe("The Example User Plugin", () => {
}); });
openmct.install(openmct.plugins.example.ExampleUser()); openmct.install(openmct.plugins.example.ExampleUser());
}); });
// The rest of the functionality of the ExampleUser Plugin is
// tested in both the UserAPISpec.js and in the UserIndicatorPlugin spec.
// If that changes, those tests can be moved here.
}); });

View File

@@ -1,83 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
export default function () {
return function install(openmct) {
openmct.install(openmct.plugins.FaultManagement());
openmct.faults.addProvider({
request(domainObject, options) {
const faults = JSON.parse(localStorage.getItem('faults'));
return Promise.resolve(faults.alarms);
},
subscribe(domainObject, callback) {
const faultsData = JSON.parse(localStorage.getItem('faults')).alarms;
function getRandomIndex(start, end) {
return Math.floor(start + (Math.random() * (end - start + 1)));
}
let id = setInterval(() => {
const index = getRandomIndex(0, faultsData.length - 1);
const randomFaultData = faultsData[index];
const randomFault = randomFaultData.fault;
randomFault.currentValueInfo.value = Math.random();
callback({
fault: randomFault,
type: 'alarms'
});
}, 300);
return () => {
clearInterval(id);
};
},
supportsRequest(domainObject) {
const faults = localStorage.getItem('faults');
return faults && domainObject.type === 'faultManagement';
},
supportsSubscribe(domainObject) {
const faults = localStorage.getItem('faults');
return faults && domainObject.type === 'faultManagement';
},
acknowledgeFault(fault, { comment = '' }) {
console.log('acknowledgeFault', fault);
console.log('comment', comment);
return Promise.resolve({
success: true
});
},
shelveFault(fault, shelveData) {
console.log('shelveFault', fault);
console.log('shelveData', shelveData);
return Promise.resolve({
success: true
});
}
});
};
}

View File

@@ -1,47 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import {
createOpenMct,
resetApplicationState
} from '../../src/utils/testing';
describe("The Example Fault Source Plugin", () => {
let openmct;
beforeEach(() => {
openmct = createOpenMct();
});
afterEach(() => {
return resetApplicationState(openmct);
});
it('is not installed by default', () => {
expect(openmct.faults.provider).toBeUndefined();
});
it('can be installed', () => {
openmct.install(openmct.plugins.example.ExampleFaultSource());
expect(openmct.faults.provider).not.toBeUndefined();
});
});

View File

@@ -29,12 +29,12 @@ define([
} }
}, },
{ {
key: "wavelengths", key: "cos",
name: "Wavelength", name: "Cosine",
unit: "nm", unit: "deg",
format: 'string[]', formatString: '%0.2f',
hints: { hints: {
range: 4 domain: 3
} }
}, },
// Need to enable "LocalTimeSystem" plugin to make use of this // Need to enable "LocalTimeSystem" plugin to make use of this
@@ -64,14 +64,6 @@ define([
hints: { hints: {
range: 2 range: 2
} }
},
{
key: "intensities",
name: "Intensities",
format: 'number[]',
hints: {
range: 3
}
} }
] ]
}, },

View File

@@ -32,8 +32,7 @@ define([
offset: 0, offset: 0,
dataRateInHz: 1, dataRateInHz: 1,
randomness: 0, randomness: 0,
phase: 0, phase: 0
loadDelay: 0
}; };
function GeneratorProvider(openmct) { function GeneratorProvider(openmct) {
@@ -54,9 +53,8 @@ define([
'period', 'period',
'offset', 'offset',
'dataRateInHz', 'dataRateInHz',
'randomness',
'phase', 'phase',
'loadDelay' 'randomness'
]; ];
request = request || {}; request = request || {};

View File

@@ -23,7 +23,7 @@
define([ define([
'uuid' 'uuid'
], function ( ], function (
{ v4: uuid } uuid
) { ) {
function WorkerInterface(openmct) { function WorkerInterface(openmct) {
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef

View File

@@ -77,8 +77,7 @@
utc: nextStep, utc: nextStep,
yesterday: nextStep - 60 * 60 * 24 * 1000, yesterday: nextStep - 60 * 60 * 24 * 1000,
sin: sin(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness), sin: sin(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness),
wavelengths: wavelengths(), wavelength: wavelength(start, nextStep),
intensities: intensities(),
cos: cos(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness) cos: cos(nextStep, data.period, data.amplitude, data.offset, data.phase, data.randomness)
} }
}); });
@@ -116,7 +115,6 @@
var dataRateInHz = request.dataRateInHz; var dataRateInHz = request.dataRateInHz;
var phase = request.phase; var phase = request.phase;
var randomness = request.randomness; var randomness = request.randomness;
var loadDelay = Math.max(request.loadDelay, 0);
var step = 1000 / dataRateInHz; var step = 1000 / dataRateInHz;
var nextStep = start - (start % step) + step; var nextStep = start - (start % step) + step;
@@ -128,20 +126,11 @@
utc: nextStep, utc: nextStep,
yesterday: nextStep - 60 * 60 * 24 * 1000, yesterday: nextStep - 60 * 60 * 24 * 1000,
sin: sin(nextStep, period, amplitude, offset, phase, randomness), sin: sin(nextStep, period, amplitude, offset, phase, randomness),
wavelengths: wavelengths(), wavelength: wavelength(start, nextStep),
intensities: intensities(),
cos: cos(nextStep, period, amplitude, offset, phase, randomness) cos: cos(nextStep, period, amplitude, offset, phase, randomness)
}); });
} }
if (loadDelay === 0) {
postOnRequest(message, request, data);
} else {
setTimeout(() => postOnRequest(message, request, data), loadDelay);
}
}
function postOnRequest(message, request, data) {
self.postMessage({ self.postMessage({
id: message.id, id: message.id,
data: request.spectra ? { data: request.spectra ? {
@@ -165,28 +154,8 @@
* Math.sin(phase + (timestamp / period / 1000 * Math.PI * 2)) + (amplitude * Math.random() * randomness) + offset; * Math.sin(phase + (timestamp / period / 1000 * Math.PI * 2)) + (amplitude * Math.random() * randomness) + offset;
} }
function wavelengths() { function wavelength(start, nextStep) {
let values = []; return (nextStep - start) / 10;
while (values.length < 5) {
const randomValue = Math.random() * 100;
if (!values.includes(randomValue)) {
values.push(String(randomValue));
}
}
return values;
}
function intensities() {
let values = [];
while (values.length < 5) {
const randomValue = Math.random() * 10;
if (!values.includes(randomValue)) {
values.push(String(randomValue));
}
}
return values;
} }
function sendError(error, message) { function sendError(error, message) {

View File

@@ -81,7 +81,7 @@ define([
{ {
name: "Amplitude", name: "Amplitude",
control: "numberfield", control: "numberfield",
cssClass: "l-numeric", cssClass: "l-input-sm l-numeric",
key: "amplitude", key: "amplitude",
required: true, required: true,
property: [ property: [
@@ -92,7 +92,7 @@ define([
{ {
name: "Offset", name: "Offset",
control: "numberfield", control: "numberfield",
cssClass: "l-numeric", cssClass: "l-input-sm l-numeric",
key: "offset", key: "offset",
required: true, required: true,
property: [ property: [
@@ -132,17 +132,6 @@ define([
"telemetry", "telemetry",
"randomness" "randomness"
] ]
},
{
name: "Loading Delay (ms)",
control: "numberfield",
cssClass: "l-input-sm l-numeric",
key: "loadDelay",
required: true,
property: [
"telemetry",
"loadDelay"
]
} }
], ],
initialize: function (object) { initialize: function (object) {
@@ -152,8 +141,7 @@ define([
offset: 0, offset: 0,
dataRateInHz: 1, dataRateInHz: 1,
phase: 0, phase: 0,
randomness: 0, randomness: 0
loadDelay: 0
}; };
} }
}); });

View File

@@ -59,8 +59,7 @@ export default function () {
object.configuration = { object.configuration = {
imageLocation: '', imageLocation: '',
imageLoadDelayInMilliSeconds: DEFAULT_IMAGE_LOAD_DELAY_IN_MILISECONDS, imageLoadDelayInMilliSeconds: DEFAULT_IMAGE_LOAD_DELAY_IN_MILISECONDS,
imageSamples: [], imageSamples: []
layers: []
}; };
object.telemetry = { object.telemetry = {
@@ -91,21 +90,7 @@ export default function () {
format: 'image', format: 'image',
hints: { hints: {
image: 1 image: 1
},
layers: [
{
source: 'dist/imagery/example-imagery-layer-16x9.png',
name: '16:9'
},
{
source: 'dist/imagery/example-imagery-layer-safe.png',
name: 'Safe'
},
{
source: 'dist/imagery/example-imagery-layer-scale.png',
name: 'Scale'
} }
]
}, },
{ {
name: 'Image Download Name', name: 'Image Download Name',
@@ -168,7 +153,7 @@ function getImageUrlListFromConfig(configuration) {
} }
function getImageLoadDelay(domainObject) { function getImageLoadDelay(domainObject) {
const imageLoadDelay = Math.trunc(Number(domainObject.configuration.imageLoadDelayInMilliSeconds)); const imageLoadDelay = domainObject.configuration.imageLoadDelayInMilliSeconds;
if (!imageLoadDelay) { if (!imageLoadDelay) {
openmctInstance.objects.mutate(domainObject, 'configuration.imageLoadDelayInMilliSeconds', DEFAULT_IMAGE_LOAD_DELAY_IN_MILISECONDS); openmctInstance.objects.mutate(domainObject, 'configuration.imageLoadDelayInMilliSeconds', DEFAULT_IMAGE_LOAD_DELAY_IN_MILISECONDS);
@@ -190,9 +175,7 @@ function getRealtimeProvider() {
subscribe: (domainObject, callback) => { subscribe: (domainObject, callback) => {
const delay = getImageLoadDelay(domainObject); const delay = getImageLoadDelay(domainObject);
const interval = setInterval(() => { const interval = setInterval(() => {
const imageSamples = getImageSamples(domainObject.configuration); callback(pointForTimestamp(Date.now(), domainObject.name, getImageSamples(domainObject.configuration), delay));
const datum = pointForTimestamp(Date.now(), domainObject.name, imageSamples, delay);
callback(datum);
}, delay); }, delay);
return () => { return () => {
@@ -231,9 +214,8 @@ function getLadProvider() {
}, },
request: (domainObject, options) => { request: (domainObject, options) => {
const delay = getImageLoadDelay(domainObject); const delay = getImageLoadDelay(domainObject);
const datum = pointForTimestamp(Date.now(), domainObject.name, getImageSamples(domainObject.configuration), delay);
return Promise.resolve([datum]); return Promise.resolve([pointForTimestamp(Date.now(), domainObject.name, delay)]);
} }
}; };
} }

View File

@@ -75,12 +75,12 @@
const TWO_HOURS = ONE_HOUR * 2; const TWO_HOURS = ONE_HOUR * 2;
const ONE_DAY = ONE_HOUR * 24; const ONE_DAY = ONE_HOUR * 24;
openmct.install(openmct.plugins.LocalStorage()); openmct.install(openmct.plugins.LocalStorage());
openmct.install(openmct.plugins.example.Generator()); openmct.install(openmct.plugins.example.Generator());
openmct.install(openmct.plugins.example.EventGeneratorPlugin()); openmct.install(openmct.plugins.example.EventGeneratorPlugin());
openmct.install(openmct.plugins.example.ExampleImagery()); openmct.install(openmct.plugins.example.ExampleImagery());
openmct.install(openmct.plugins.example.ExampleTags());
openmct.install(openmct.plugins.Espresso()); openmct.install(openmct.plugins.Espresso());
openmct.install(openmct.plugins.MyItems()); openmct.install(openmct.plugins.MyItems());

View File

@@ -74,8 +74,13 @@ module.exports = (config) => {
}, },
coverageIstanbulReporter: { coverageIstanbulReporter: {
fixWebpackSourcePaths: true, fixWebpackSourcePaths: true,
dir: "coverage/unit", dir: "dist/reports/coverage",
reports: ['lcovonly'] reports: ['lcovonly', 'text-summary'],
thresholds: {
global: {
lines: 52
}
}
}, },
specReporter: { specReporter: {
maxLogLines: 5, maxLogLines: 5,

View File

@@ -1,13 +1,13 @@
{ {
"name": "openmct", "name": "openmct",
"version": "2.0.5", "version": "2.0.4-SNAPSHOT",
"description": "The Open MCT core platform", "description": "The Open MCT core platform",
"devDependencies": { "devDependencies": {
"@babel/eslint-parser": "7.18.2", "@babel/eslint-parser": "7.16.3",
"@braintree/sanitize-url": "6.0.0", "@braintree/sanitize-url": "6.0.0",
"@percy/cli": "1.2.1", "@percy/cli": "1.2.1",
"@percy/playwright": "1.0.4", "@percy/playwright": "1.0.4",
"@playwright/test": "1.23.0", "@playwright/test": "1.21.1",
"@types/eventemitter3": "^1.0.0", "@types/eventemitter3": "^1.0.0",
"@types/jasmine": "^4.0.1", "@types/jasmine": "^4.0.1",
"@types/karma": "^6.3.2", "@types/karma": "^6.3.2",
@@ -16,7 +16,6 @@
"babel-loader": "8.2.3", "babel-loader": "8.2.3",
"babel-plugin-istanbul": "6.1.1", "babel-plugin-istanbul": "6.1.1",
"comma-separated-values": "3.6.4", "comma-separated-values": "3.6.4",
"codecov":"3.8.3",
"copy-webpack-plugin": "11.0.0", "copy-webpack-plugin": "11.0.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"css-loader": "4.0.0", "css-loader": "4.0.0",
@@ -26,9 +25,10 @@
"eslint": "8.13.0", "eslint": "8.13.0",
"eslint-plugin-compat": "4.0.2", "eslint-plugin-compat": "4.0.2",
"eslint-plugin-playwright": "0.9.0", "eslint-plugin-playwright": "0.9.0",
"eslint-plugin-vue": "9.1.0", "eslint-plugin-vue": "8.5.0",
"eslint-plugin-you-dont-need-lodash-underscore": "6.12.0", "eslint-plugin-you-dont-need-lodash-underscore": "6.12.0",
"eventemitter3": "1.2.0", "eventemitter3": "1.2.0",
"exports-loader": "0.7.0",
"express": "4.13.1", "express": "4.13.1",
"file-saver": "2.0.5", "file-saver": "2.0.5",
"git-rev-sync": "3.0.2", "git-rev-sync": "3.0.2",
@@ -56,26 +56,26 @@
"moment-timezone": "0.5.34", "moment-timezone": "0.5.34",
"node-bourbon": "4.2.3", "node-bourbon": "4.2.3",
"painterro": "1.2.56", "painterro": "1.2.56",
"nyc":"15.1.0",
"plotly.js-basic-dist": "2.12.0", "plotly.js-basic-dist": "2.12.0",
"plotly.js-gl2d-dist": "2.12.0", "plotly.js-gl2d-dist": "2.12.0",
"printj": "1.3.1", "printj": "1.3.1",
"request": "2.88.2", "request": "2.88.2",
"resolve-url-loader": "5.0.0", "resolve-url-loader": "5.0.0",
"sass": "1.52.2", "sass": "1.49.9",
"sass-loader": "12.6.0", "sass-loader": "12.6.0",
"sinon": "14.0.0", "sinon": "14.0.0",
"style-loader": "^1.0.1", "style-loader": "^1.0.1",
"uuid": "8.3.2", "uuid": "8.3.2",
"vue": "2.6.14", "vue": "2.6.14",
"vue-eslint-parser": "9.0.2", "vue-eslint-parser": "8.3.0",
"vue-loader": "15.9.8", "vue-loader": "15.9.8",
"vue-template-compiler": "2.6.14", "vue-template-compiler": "2.6.14",
"webpack": "5.68.0", "webpack": "5.68.0",
"webpack-cli": "4.9.2", "webpack-cli": "4.9.2",
"webpack-dev-middleware": "5.3.3", "webpack-dev-middleware": "5.3.3",
"webpack-hot-middleware": "2.25.1", "webpack-hot-middleware": "2.25.1",
"webpack-merge": "5.8.0" "webpack-merge": "5.8.0",
"zepto": "1.2.0"
}, },
"scripts": { "scripts": {
"clean": "rm -rf ./dist ./node_modules ./package-lock.json", "clean": "rm -rf ./dist ./node_modules ./package-lock.json",
@@ -91,10 +91,9 @@
"test": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run", "test": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run",
"test:firefox": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run --browsers=FirefoxHeadless", "test:firefox": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run --browsers=FirefoxHeadless",
"test:debug": "cross-env NODE_ENV=debug karma start --no-single-run", "test:debug": "cross-env NODE_ENV=debug karma start --no-single-run",
"test:e2e": "npx playwright test", "test:e2e:ci": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome smoke branding default condition timeConductor clock exampleImagery notebook persistence performance",
"test: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:local": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome",
"test:e2e:updatesnapshots": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome --grep @snapshot --update-snapshots", "test:e2e:updatesnapshots": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome --grep @snapshot --update-snapshots",
"test:e2e:visual": "percy exec --config ./e2e/.percy.yml -- npx playwright test --config=e2e/playwright-visual.config.js", "test:e2e:visual": "percy exec --config ./e2e/.percy.yml -- npx playwright test --config=e2e/playwright-visual.config.js",
"test:e2e:full": "npx playwright test --config=e2e/playwright-ci.config.js", "test:e2e:full": "npx playwright test --config=e2e/playwright-ci.config.js",
"test:perf": "npx playwright test --config=e2e/playwright-performance.config.js", "test:perf": "npx playwright test --config=e2e/playwright-performance.config.js",
@@ -104,10 +103,6 @@
"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'", "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'", "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", "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" "prepare": "npm run build:prod"
}, },
"repository": { "repository": {

View File

@@ -42,7 +42,6 @@ define([
'./plugins/duplicate/plugin', './plugins/duplicate/plugin',
'./plugins/importFromJSONAction/plugin', './plugins/importFromJSONAction/plugin',
'./plugins/exportAsJSONAction/plugin', './plugins/exportAsJSONAction/plugin',
'./ui/components/components',
'vue' 'vue'
], function ( ], function (
EventEmitter, EventEmitter,
@@ -66,7 +65,6 @@ define([
DuplicateActionPlugin, DuplicateActionPlugin,
ImportFromJSONAction, ImportFromJSONAction,
ExportAsJSONAction, ExportAsJSONAction,
components,
Vue Vue
) { ) {
/** /**
@@ -96,12 +94,11 @@ define([
}; };
this.destroy = this.destroy.bind(this); this.destroy = this.destroy.bind(this);
[
/** /**
* Tracks current selection state of the application. * Tracks current selection state of the application.
* @private * @private
*/ */
['selection', () => new Selection(this)], this.selection = new Selection(this);
/** /**
* MCT's time conductor, which may be used to synchronize view contents * MCT's time conductor, which may be used to synchronize view contents
@@ -110,7 +107,7 @@ define([
* @memberof module:openmct.MCT# * @memberof module:openmct.MCT#
* @name conductor * @name conductor
*/ */
['time', () => new api.TimeAPI(this)], this.time = new api.TimeAPI(this);
/** /**
* An interface for interacting with the composition of domain objects. * An interface for interacting with the composition of domain objects.
@@ -125,7 +122,7 @@ define([
* @memberof module:openmct.MCT# * @memberof module:openmct.MCT#
* @name composition * @name composition
*/ */
['composition', () => new api.CompositionAPI(this)], this.composition = new api.CompositionAPI(this);
/** /**
* Registry for views of domain objects which should appear in the * Registry for views of domain objects which should appear in the
@@ -135,7 +132,7 @@ define([
* @memberof module:openmct.MCT# * @memberof module:openmct.MCT#
* @name objectViews * @name objectViews
*/ */
['objectViews', () => new ViewRegistry()], this.objectViews = new ViewRegistry();
/** /**
* Registry for views which should appear in the Inspector area. * Registry for views which should appear in the Inspector area.
@@ -145,7 +142,7 @@ define([
* @memberof module:openmct.MCT# * @memberof module:openmct.MCT#
* @name inspectorViews * @name inspectorViews
*/ */
['inspectorViews', () => new InspectorViewRegistry()], this.inspectorViews = new InspectorViewRegistry();
/** /**
* Registry for views which should appear in Edit Properties * Registry for views which should appear in Edit Properties
@@ -156,7 +153,15 @@ define([
* @memberof module:openmct.MCT# * @memberof module:openmct.MCT#
* @name propertyEditors * @name propertyEditors
*/ */
['propertyEditors', () => new ViewRegistry()], this.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 * Registry for views which should appear in the toolbar area while
@@ -166,7 +171,7 @@ define([
* @memberof module:openmct.MCT# * @memberof module:openmct.MCT#
* @name toolbars * @name toolbars
*/ */
['toolbars', () => new ToolbarRegistry()], this.toolbars = new ToolbarRegistry();
/** /**
* Registry for domain object types which may exist within this * Registry for domain object types which may exist within this
@@ -176,7 +181,7 @@ define([
* @memberof module:openmct.MCT# * @memberof module:openmct.MCT#
* @name types * @name types
*/ */
['types', () => new api.TypeRegistry()], this.types = new api.TypeRegistry();
/** /**
* An interface for interacting with domain objects and the domain * An interface for interacting with domain objects and the domain
@@ -186,7 +191,7 @@ define([
* @memberof module:openmct.MCT# * @memberof module:openmct.MCT#
* @name objects * @name objects
*/ */
['objects', () => new api.ObjectAPI.default(this.types, this)], this.objects = new api.ObjectAPI.default(this.types, this);
/** /**
* An interface for retrieving and interpreting telemetry data associated * An interface for retrieving and interpreting telemetry data associated
@@ -196,7 +201,7 @@ define([
* @memberof module:openmct.MCT# * @memberof module:openmct.MCT#
* @name telemetry * @name telemetry
*/ */
['telemetry', () => new api.TelemetryAPI.default(this)], this.telemetry = new api.TelemetryAPI(this);
/** /**
* An interface for creating new indicators and changing them dynamically. * An interface for creating new indicators and changing them dynamically.
@@ -205,7 +210,7 @@ define([
* @memberof module:openmct.MCT# * @memberof module:openmct.MCT#
* @name indicators * @name indicators
*/ */
['indicators', () => new api.IndicatorAPI(this)], this.indicators = new api.IndicatorAPI(this);
/** /**
* MCT's user awareness management, to enable user and * MCT's user awareness management, to enable user and
@@ -214,49 +219,26 @@ define([
* @memberof module:openmct.MCT# * @memberof module:openmct.MCT#
* @name user * @name user
*/ */
['user', () => new api.UserAPI(this)], 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 // Plugins that are installed by default
this.install(this.plugins.Plot()); this.install(this.plugins.Plot());
@@ -287,7 +269,6 @@ define([
this.install(this.plugins.ObjectInterceptors()); this.install(this.plugins.ObjectInterceptors());
this.install(this.plugins.DeviceClassifier()); this.install(this.plugins.DeviceClassifier());
this.install(this.plugins.UserIndicator()); this.install(this.plugins.UserIndicator());
this.install(this.plugins.Gauge());
} }
MCT.prototype = Object.create(EventEmitter.prototype); MCT.prototype = Object.create(EventEmitter.prototype);
@@ -396,7 +377,6 @@ define([
}; };
MCT.prototype.plugins = plugins; MCT.prototype.plugins = plugins;
MCT.prototype.components = components.default;
return MCT; return MCT;
}); });

View File

@@ -85,6 +85,8 @@ class ActionCollection extends EventEmitter {
} }
destroy() { destroy() {
super.removeAllListeners();
if (!this.skipEnvironmentObservers) { if (!this.skipEnvironmentObservers) {
this.objectUnsubscribes.forEach(unsubscribe => { this.objectUnsubscribes.forEach(unsubscribe => {
unsubscribe(); unsubscribe();
@@ -94,7 +96,6 @@ class ActionCollection extends EventEmitter {
} }
this.emit('destroy', this.view); this.emit('destroy', this.view);
this.removeAllListeners();
} }
getVisibleActions() { getVisibleActions() {

View File

@@ -1,277 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { v4 as uuid } from 'uuid';
import EventEmitter from 'EventEmitter';
/**
* @readonly
* @enum {String} AnnotationType
* @property {String} NOTEBOOK The notebook annotation type
* @property {String} GEOSPATIAL The geospatial annotation type
* @property {String} PIXEL_SPATIAL The pixel-spatial annotation type
* @property {String} TEMPORAL The temporal annotation type
* @property {String} PLOT_SPATIAL The plot-spatial annotation type
*/
const ANNOTATION_TYPES = Object.freeze({
NOTEBOOK: 'NOTEBOOK',
GEOSPATIAL: 'GEOSPATIAL',
PIXEL_SPATIAL: 'PIXEL_SPATIAL',
TEMPORAL: 'TEMPORAL',
PLOT_SPATIAL: 'PLOT_SPATIAL'
});
/**
* @typedef {Object} Tag
* @property {String} key a unique identifier for the tag
* @property {String} backgroundColor eg. "#cc0000"
* @property {String} foregroundColor eg. "#ffffff"
*/
export default class AnnotationAPI extends EventEmitter {
constructor(openmct) {
super();
this.openmct = openmct;
this.availableTags = {};
this.ANNOTATION_TYPES = ANNOTATION_TYPES;
this.openmct.types.addType('annotation', {
name: 'Annotation',
description: 'A user created note or comment about time ranges, pixel space, and geospatial features.',
creatable: false,
cssClass: 'icon-notebook',
initialize: function (domainObject) {
domainObject.targets = domainObject.targets || {};
domainObject.originalContextPath = domainObject.originalContextPath || '';
domainObject.tags = domainObject.tags || [];
domainObject.contentText = domainObject.contentText || '';
domainObject.annotationType = domainObject.annotationType || 'plotspatial';
}
});
}
/**
* Create the a generic annotation
* @typedef {Object} CreateAnnotationOptions
* @property {String} name a name for the new parameter
* @property {import('../objects/ObjectAPI').DomainObject} domainObject the domain object to create
* @property {ANNOTATION_TYPES} annotationType the type of annotation to create
* @property {Tag[]} tags
* @property {String} contentText
* @property {import('../objects/ObjectAPI').Identifier[]} targets
*/
/**
* @method create
* @param {CreateAnnotationOptions} options
* @returns {Promise<import('../objects/ObjectAPI').DomainObject>} a promise which will resolve when the domain object
* has been created, or be rejected if it cannot be saved
*/
async create({name, domainObject, annotationType, tags, contentText, targets}) {
if (!Object.keys(this.ANNOTATION_TYPES).includes(annotationType)) {
throw new Error(`Unknown annotation type: ${annotationType}`);
}
if (!Object.keys(targets).length) {
throw new Error(`At least one target is required to create an annotation`);
}
const domainObjectKeyString = this.openmct.objects.makeKeyString(domainObject.identifier);
const originalPathObjects = await this.openmct.objects.getOriginalPath(domainObjectKeyString);
const originalContextPath = this.openmct.objects.getRelativePath(originalPathObjects);
const namespace = domainObject.identifier.namespace;
const type = 'annotation';
const typeDefinition = this.openmct.types.get(type);
const definition = typeDefinition.definition;
const createdObject = {
name,
type,
identifier: {
key: uuid(),
namespace
},
tags,
annotationType,
contentText,
originalContextPath
};
if (definition.initialize) {
definition.initialize(createdObject);
}
createdObject.targets = targets;
createdObject.originalContextPath = originalContextPath;
const success = await this.openmct.objects.save(createdObject);
if (success) {
this.emit('annotationCreated', createdObject);
return createdObject;
} else {
throw new Error('Failed to create object');
}
}
defineTag(tagKey, tagsDefinition) {
this.availableTags[tagKey] = tagsDefinition;
}
getAvailableTags() {
if (this.availableTags) {
const rearrangedToArray = Object.keys(this.availableTags).map(tagKey => {
return {
id: tagKey,
...this.availableTags[tagKey]
};
});
return rearrangedToArray;
} else {
return [];
}
}
async getAnnotation(query, searchType) {
let foundAnnotation = null;
const searchResults = (await Promise.all(this.openmct.objects.search(query, null, searchType))).flat();
if (searchResults) {
foundAnnotation = searchResults[0];
}
return foundAnnotation;
}
async addAnnotationTag(existingAnnotation, targetDomainObject, targetSpecificDetails, annotationType, tag) {
if (!existingAnnotation) {
const targets = {};
const targetKeyString = this.openmct.objects.makeKeyString(targetDomainObject.identifier);
targets[targetKeyString] = targetSpecificDetails;
const contentText = `${annotationType} tag`;
const annotationCreationArguments = {
name: contentText,
domainObject: targetDomainObject,
annotationType,
tags: [tag],
contentText,
targets
};
const newAnnotation = await this.create(annotationCreationArguments);
return newAnnotation;
} else {
const tagArray = [tag, ...existingAnnotation.tags];
this.openmct.objects.mutate(existingAnnotation, 'tags', tagArray);
return existingAnnotation;
}
}
removeAnnotationTag(existingAnnotation, tagToRemove) {
if (existingAnnotation && existingAnnotation.tags.includes(tagToRemove)) {
const cleanedArray = existingAnnotation.tags.filter(extantTag => extantTag !== tagToRemove);
this.openmct.objects.mutate(existingAnnotation, 'tags', cleanedArray);
} else {
throw new Error(`Asked to remove tag (${tagToRemove}) that doesn't exist`, existingAnnotation);
}
}
removeAnnotationTags(existingAnnotation) {
// just removes tags on the annotation as we can't really delete objects
if (existingAnnotation && existingAnnotation.tags) {
this.openmct.objects.mutate(existingAnnotation, 'tags', []);
}
}
#getMatchingTags(query) {
if (!query) {
return [];
}
const matchingTags = Object.keys(this.availableTags).filter(tagKey => {
if (this.availableTags[tagKey] && this.availableTags[tagKey].label) {
return this.availableTags[tagKey].label.toLowerCase().includes(query.toLowerCase());
}
return false;
});
return matchingTags;
}
#addTagMetaInformationToResults(results, matchingTagKeys) {
const tagsAddedToResults = results.map(result => {
const fullTagModels = result.tags.map(tagKey => {
const tagModel = this.availableTags[tagKey];
tagModel.tagID = tagKey;
return tagModel;
});
return {
fullTagModels,
matchingTagKeys,
...result
};
});
return tagsAddedToResults;
}
async #addTargetModelsToResults(results) {
const modelAddedToResults = await Promise.all(results.map(async result => {
const targetModels = await Promise.all(Object.keys(result.targets).map(async (targetID) => {
const targetModel = await this.openmct.objects.get(targetID);
const targetKeyString = this.openmct.objects.makeKeyString(targetModel.identifier);
const originalPathObjects = await this.openmct.objects.getOriginalPath(targetKeyString);
return {
originalPath: originalPathObjects,
...targetModel
};
}));
return {
targetModels,
...result
};
}));
return modelAddedToResults;
}
/**
* @method searchForTags
* @param {String} query A query to match against tags. E.g., "dr" will match the tags "drilling" and "driving"
* @param {Object} abortController An optional abort method to stop the query
* @returns {Promise} returns a model of matching tags with their target domain objects attached
*/
async searchForTags(query, abortController) {
const matchingTagKeys = this.#getMatchingTags(query);
const searchResults = (await Promise.all(this.openmct.objects.search(matchingTagKeys, abortController, this.openmct.objects.SEARCH_TYPES.TAGS))).flat();
const appliedTagSearchResults = this.#addTagMetaInformationToResults(searchResults, matchingTagKeys);
const appliedTargetsModels = await this.#addTargetModelsToResults(appliedTagSearchResults);
return appliedTargetsModels;
}
}

View File

@@ -1,176 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { createOpenMct, resetApplicationState } from '../../utils/testing';
import ExampleTagsPlugin from "../../../example/exampleTags/plugin";
describe("The Annotation API", () => {
let openmct;
let mockObjectProvider;
let mockDomainObject;
let mockAnnotationObject;
beforeEach((done) => {
openmct = createOpenMct();
openmct.install(new ExampleTagsPlugin());
const availableTags = openmct.annotation.getAvailableTags();
mockDomainObject = {
type: 'notebook',
name: 'fooRabbitNotebook',
identifier: {
key: 'some-object',
namespace: 'fooNameSpace'
}
};
mockAnnotationObject = {
type: 'annotation',
name: 'Some Notebook Annotation',
annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,
tags: [availableTags[0].id, availableTags[1].id],
identifier: {
key: 'anAnnotationKey',
namespace: 'fooNameSpace'
},
targets: {
'fooNameSpace:some-object': {
entryId: 'fooBarEntry'
}
}
};
mockObjectProvider = jasmine.createSpyObj("mock provider", [
"create",
"update",
"get"
]);
// eslint-disable-next-line require-await
mockObjectProvider.get = async (identifier) => {
if (identifier.key === mockDomainObject.identifier.key) {
return mockDomainObject;
} else if (identifier.key === mockAnnotationObject.identifier.key) {
return mockAnnotationObject;
} else {
return null;
}
};
mockObjectProvider.create.and.returnValue(Promise.resolve(true));
mockObjectProvider.update.and.returnValue(Promise.resolve(true));
openmct.objects.addProvider('fooNameSpace', mockObjectProvider);
openmct.on('start', done);
openmct.startHeadless();
});
afterEach(async () => {
openmct.objects.providers = {};
await resetApplicationState(openmct);
});
it("is defined", () => {
expect(openmct.annotation).toBeDefined();
});
describe("Creation", () => {
it("can create annotations", async () => {
const annotationCreationArguments = {
name: 'Test Annotation',
domainObject: mockDomainObject,
annotationType: openmct.annotation.ANNOTATION_TYPES.NOTEBOOK,
tags: ['sometag'],
contentText: "fooContext",
targets: {'fooTarget': {}}
};
const annotationObject = await openmct.annotation.create(annotationCreationArguments);
expect(annotationObject).toBeDefined();
expect(annotationObject.type).toEqual('annotation');
});
it("fails if annotation is an unknown type", async () => {
try {
await openmct.annotation.create('Garbage Annotation', mockDomainObject, 'garbageAnnotation', ['sometag'], "fooContext", {'fooTarget': {}});
} catch (error) {
expect(error).toBeDefined();
}
});
});
describe("Tagging", () => {
it("can create a tag", async () => {
const annotationObject = await openmct.annotation.addAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
expect(annotationObject).toBeDefined();
expect(annotationObject.type).toEqual('annotation');
expect(annotationObject.tags).toContain('aWonderfulTag');
});
it("can delete a tag", async () => {
const originalAnnotationObject = await openmct.annotation.addAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
const annotationObject = await openmct.annotation.addAnnotationTag(originalAnnotationObject, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'anotherTagToRemove');
expect(annotationObject).toBeDefined();
openmct.annotation.removeAnnotationTag(annotationObject, 'anotherTagToRemove');
expect(annotationObject.tags).toEqual(['aWonderfulTag']);
openmct.annotation.removeAnnotationTag(annotationObject, 'aWonderfulTag');
expect(annotationObject.tags).toEqual([]);
});
it("throws an error if deleting non-existent tag", async () => {
const annotationObject = await openmct.annotation.addAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
expect(annotationObject).toBeDefined();
expect(() => {
openmct.annotation.removeAnnotationTag(annotationObject, 'ThisTagShouldNotExist');
}).toThrow();
});
it("can remove all tags", async () => {
const annotationObject = await openmct.annotation.addAnnotationTag(null, mockDomainObject, {entryId: 'foo'}, openmct.annotation.ANNOTATION_TYPES.NOTEBOOK, 'aWonderfulTag');
expect(annotationObject).toBeDefined();
expect(() => {
openmct.annotation.removeAnnotationTags(annotationObject);
}).not.toThrow();
expect(annotationObject.tags).toEqual([]);
});
});
describe("Search", () => {
let sharedWorkerToRestore;
beforeEach(async () => {
// use local worker
sharedWorkerToRestore = openmct.objects.inMemorySearchProvider.worker;
openmct.objects.inMemorySearchProvider.worker = null;
await openmct.objects.inMemorySearchProvider.index(mockDomainObject);
await openmct.objects.inMemorySearchProvider.index(mockAnnotationObject);
});
afterEach(() => {
openmct.objects.inMemorySearchProvider.worker = sharedWorkerToRestore;
});
it("can search for tags", async () => {
const results = await openmct.annotation.searchForTags('S');
expect(results).toBeDefined();
expect(results.length).toEqual(1);
});
it("can get notebook annotations", async () => {
const targetKeyString = openmct.objects.makeKeyString(mockDomainObject.identifier);
const query = {
targetKeyString,
entryId: 'fooBarEntry'
};
const results = await openmct.annotation.getAnnotation(query, openmct.objects.SEARCH_TYPES.NOTEBOOK_ANNOTATIONS);
expect(results).toBeDefined();
expect(results.tags.length).toEqual(2);
});
});
});

View File

@@ -24,7 +24,6 @@ define([
'./actions/ActionsAPI', './actions/ActionsAPI',
'./composition/CompositionAPI', './composition/CompositionAPI',
'./Editor', './Editor',
'./faultmanagement/FaultManagementAPI',
'./forms/FormsAPI', './forms/FormsAPI',
'./indicators/IndicatorAPI', './indicators/IndicatorAPI',
'./menu/MenuAPI', './menu/MenuAPI',
@@ -35,13 +34,11 @@ define([
'./telemetry/TelemetryAPI', './telemetry/TelemetryAPI',
'./time/TimeAPI', './time/TimeAPI',
'./types/TypeRegistry', './types/TypeRegistry',
'./user/UserAPI', './user/UserAPI'
'./annotation/AnnotationAPI'
], function ( ], function (
ActionsAPI, ActionsAPI,
CompositionAPI, CompositionAPI,
EditorAPI, EditorAPI,
FaultManagementAPI,
FormsAPI, FormsAPI,
IndicatorAPI, IndicatorAPI,
MenuAPI, MenuAPI,
@@ -52,16 +49,14 @@ define([
TelemetryAPI, TelemetryAPI,
TimeAPI, TimeAPI,
TypeRegistry, TypeRegistry,
UserAPI, UserAPI
AnnotationAPI
) { ) {
return { return {
ActionsAPI: ActionsAPI.default, ActionsAPI: ActionsAPI.default,
CompositionAPI: CompositionAPI, CompositionAPI: CompositionAPI,
EditorAPI: EditorAPI, EditorAPI: EditorAPI,
FaultManagementAPI: FaultManagementAPI,
FormsAPI: FormsAPI, FormsAPI: FormsAPI,
IndicatorAPI: IndicatorAPI.default, IndicatorAPI: IndicatorAPI,
MenuAPI: MenuAPI.default, MenuAPI: MenuAPI.default,
NotificationAPI: NotificationAPI.default, NotificationAPI: NotificationAPI.default,
ObjectAPI: ObjectAPI, ObjectAPI: ObjectAPI,
@@ -70,7 +65,6 @@ define([
TelemetryAPI: TelemetryAPI, TelemetryAPI: TelemetryAPI,
TimeAPI: TimeAPI.default, TimeAPI: TimeAPI.default,
TypeRegistry: TypeRegistry, TypeRegistry: TypeRegistry,
UserAPI: UserAPI.default, UserAPI: UserAPI.default
AnnotationAPI: AnnotationAPI.default
}; };
}); });

View File

@@ -1,106 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
export default class FaultManagementAPI {
constructor(openmct) {
this.openmct = openmct;
}
addProvider(provider) {
this.provider = provider;
}
supportsActions() {
return this.provider?.acknowledgeFault !== undefined && this.provider?.shelveFault !== undefined;
}
request(domainObject) {
if (!this.provider?.supportsRequest(domainObject)) {
return Promise.reject();
}
return this.provider.request(domainObject);
}
subscribe(domainObject, callback) {
if (!this.provider?.supportsSubscribe(domainObject)) {
return Promise.reject();
}
return this.provider.subscribe(domainObject, callback);
}
acknowledgeFault(fault, ackData) {
return this.provider.acknowledgeFault(fault, ackData);
}
shelveFault(fault, shelveData) {
return this.provider.shelveFault(fault, shelveData);
}
}
/** @typedef {object} Fault
* @property {string} type
* @property {object} fault
* @property {boolean} fault.acknowledged
* @property {object} fault.currentValueInfo
* @property {number} fault.currentValueInfo.value
* @property {string} fault.currentValueInfo.rangeCondition
* @property {string} fault.currentValueInfo.monitoringResult
* @property {string} fault.id
* @property {string} fault.name
* @property {string} fault.namespace
* @property {number} fault.seqNum
* @property {string} fault.severity
* @property {boolean} fault.shelved
* @property {string} fault.shortDescription
* @property {string} fault.triggerTime
* @property {object} fault.triggerValueInfo
* @property {number} fault.triggerValueInfo.value
* @property {string} fault.triggerValueInfo.rangeCondition
* @property {string} fault.triggerValueInfo.monitoringResult
* @example
* {
* "type": "",
* "fault": {
* "acknowledged": true,
* "currentValueInfo": {
* "value": 0,
* "rangeCondition": "",
* "monitoringResult": ""
* },
* "id": "",
* "name": "",
* "namespace": "",
* "seqNum": 0,
* "severity": "",
* "shelved": true,
* "shortDescription": "",
* "triggerTime": "",
* "triggerValueInfo": {
* "value": 0,
* "rangeCondition": "",
* "monitoringResult": ""
* }
* }
* }
*/

View File

@@ -1,144 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* License); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an AS IS BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import {
createOpenMct,
resetApplicationState
} from '../../utils/testing';
const faultName = 'super duper fault';
const aFault = {
type: '',
fault: {
acknowledged: true,
currentValueInfo: {
value: 0,
rangeCondition: '',
monitoringResult: ''
},
id: '',
name: faultName,
namespace: '',
seqNum: 0,
severity: '',
shelved: true,
shortDescription: '',
triggerTime: '',
triggerValueInfo: {
value: 0,
rangeCondition: '',
monitoringResult: ''
}
}
};
const faultDomainObject = {
name: 'it is not your fault',
type: 'faultManagement',
identifier: {
key: 'nobodies',
namespace: 'fault'
}
};
const aComment = 'THIS is my fault.';
const faultManagementProvider = {
request() {
return Promise.resolve([aFault]);
},
subscribe(domainObject, callback) {
return () => {};
},
supportsRequest(domainObject) {
return domainObject.type === 'faultManagement';
},
supportsSubscribe(domainObject) {
return domainObject.type === 'faultManagement';
},
acknowledgeFault(fault, { comment = '' }) {
return Promise.resolve({
success: true
});
},
shelveFault(fault, shelveData) {
return Promise.resolve({
success: true
});
}
};
describe('The Fault Management API', () => {
let openmct;
beforeEach(() => {
openmct = createOpenMct();
openmct.install(openmct.plugins.FaultManagement());
// openmct.install(openmct.plugins.example.ExampleFaultSource());
openmct.faults.addProvider(faultManagementProvider);
});
afterEach(() => {
return resetApplicationState(openmct);
});
it('allows you to request a fault', async () => {
spyOn(faultManagementProvider, 'supportsRequest').and.callThrough();
let faultResponse = await openmct.faults.request(faultDomainObject);
expect(faultManagementProvider.supportsRequest).toHaveBeenCalledWith(faultDomainObject);
expect(faultResponse[0].fault.name).toEqual(faultName);
});
it('allows you to subscribe to a fault', () => {
spyOn(faultManagementProvider, 'subscribe').and.callThrough();
spyOn(faultManagementProvider, 'supportsSubscribe').and.callThrough();
let unsubscribe = openmct.faults.subscribe(faultDomainObject, () => {});
expect(unsubscribe).toEqual(jasmine.any(Function));
expect(faultManagementProvider.supportsSubscribe).toHaveBeenCalledWith(faultDomainObject);
expect(faultManagementProvider.subscribe).toHaveBeenCalledOnceWith(faultDomainObject, jasmine.any(Function));
});
it('will tell you if the fault management provider supports actions', () => {
expect(openmct.faults.supportsActions()).toBeTrue();
});
it('will allow you to acknowledge a fault', async () => {
spyOn(faultManagementProvider, 'acknowledgeFault').and.callThrough();
let ackResponse = await openmct.faults.acknowledgeFault(aFault, aComment);
expect(faultManagementProvider.acknowledgeFault).toHaveBeenCalledWith(aFault, aComment);
expect(ackResponse.success).toBeTrue();
});
it('will allow you to shelve a fault', async () => {
spyOn(faultManagementProvider, 'shelveFault').and.callThrough();
let shelveResponse = await openmct.faults.shelveFault(aFault, aComment);
expect(faultManagementProvider.shelveFault).toHaveBeenCalledWith(aFault, aComment);
expect(shelveResponse.success).toBeTrue();
});
});

View File

@@ -44,15 +44,19 @@
> >
{{ section.name }} {{ section.name }}
</h2> </h2>
<FormRow <div
v-for="(row, index) in section.rows" v-for="(row, index) in section.rows"
:key="row.id" :key="row.id"
:css-class="row.cssClass" class="u-contents"
>
<FormRow
:css-class="section.cssClass"
:first="index < 1" :first="index < 1"
:row="row" :row="row"
@onChange="onChange" @onChange="onChange"
/> />
</div> </div>
</div>
</form> </form>
<div class="mct-form__controls c-overlay__button-bar c-form__bottom-bar"> <div class="mct-form__controls c-overlay__button-bar c-form__bottom-bar">
@@ -60,7 +64,6 @@
tabindex="0" tabindex="0"
:disabled="isInvalid" :disabled="isInvalid"
class="c-button c-button--major" class="c-button c-button--major"
aria-label="Save"
@click="onSave" @click="onSave"
> >
{{ submitLabel }} {{ submitLabel }}
@@ -68,7 +71,6 @@
<button <button
tabindex="0" tabindex="0"
class="c-button js-cancel-button" class="c-button js-cancel-button"
aria-label="Cancel"
@click="onDismiss" @click="onDismiss"
> >
{{ cancelLabel }} {{ cancelLabel }}

View File

@@ -23,10 +23,7 @@
<template> <template>
<div <div
class="form-row c-form__row" class="form-row c-form__row"
:class="[ :class="[{ 'first': first }]"
{ 'first': first },
cssClass
]"
@onChange="onChange" @onChange="onChange"
> >
<div <div
@@ -37,7 +34,7 @@
</div> </div>
<div <div
class="c-form-row__state-indicator" class="c-form-row__state-indicator"
:class="reqClass" :class="rowClass"
> >
</div> </div>
<div <div
@@ -79,22 +76,24 @@ export default {
}; };
}, },
computed: { computed: {
reqClass() { rowClass() {
let reqClass = 'req'; let cssClass = this.cssClass;
if (!this.row.required) { if (!this.row.required) {
return; return;
} }
cssClass = `${cssClass} req`;
if (this.visited && this.valid !== undefined) { if (this.visited && this.valid !== undefined) {
if (this.valid === true) { if (this.valid === true) {
reqClass = 'valid'; cssClass = `${cssClass} valid`;
} else { } else {
reqClass = 'invalid'; cssClass = `${cssClass} invalid`;
} }
} }
return reqClass; return cssClass;
} }
}, },
mounted() { mounted() {

View File

@@ -19,47 +19,35 @@
* this source code distribution or the Licensing information page available * this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
<template> <template>
<div <div class="form-control autocomplete">
ref="autoCompleteForm" <span class="autocompleteInputAndArrow">
class="form-control c-input--autocomplete js-autocomplete"
>
<div
class="c-input--autocomplete__wrapper"
>
<input <input
ref="autoCompleteInput"
v-model="field" v-model="field"
class="c-input--autocomplete__input js-autocomplete__input" class="autocompleteInput"
type="text" type="text"
:placeholder="placeHolderText"
@click="inputClicked()" @click="inputClicked()"
@keydown="keyDown($event)" @keydown="keyDown($event)"
> >
<div <span
class="icon-arrow-down c-icon-button c-input--autocomplete__afford-arrow js-autocomplete__afford-arrow" class="icon-arrow-down"
@click="arrowClicked()" @click="arrowClicked()"
></div> ></span>
</div> </span>
<div <div
v-if="!hideOptions" class="autocompleteOptions"
class="c-menu c-input--autocomplete__options"
aria-label="Autocomplete Options"
@blur="hideOptions = true" @blur="hideOptions = true"
> >
<ul> <ul v-if="!hideOptions">
<li <li
v-for="opt in filteredOptions" v-for="opt in filteredOptions"
:key="opt.optionId" :key="opt.optionId"
:class="[ :class="{'optionPreSelected': optionIndex === opt.optionId}"
{'optionPreSelected': optionIndex === opt.optionId},
itemCssClass
]"
:style="itemStyle(opt)"
@click="fillInputWithString(opt.name)" @click="fillInputWithString(opt.name)"
@mouseover="optionMouseover(opt.optionId)" @mouseover="optionMouseover(opt.optionId)"
> >
{{ opt.name }} <span class="optionText">{{ opt.name }}</span>
</li> </li>
</ul> </ul>
</div> </div>
@@ -77,23 +65,7 @@ export default {
props: { props: {
model: { model: {
type: Object, type: Object,
required: true, required: true
default() {
return {};
}
},
placeHolderText: {
type: String,
default() {
return "";
}
},
itemCssClass: {
type: String,
required: false,
default() {
return "";
}
} }
}, },
data() { data() {
@@ -106,40 +78,31 @@ export default {
}, },
computed: { computed: {
filteredOptions() { filteredOptions() {
const fullOptions = this.options || []; const options = this.optionNames || [];
if (this.showFilteredOptions) { if (this.showFilteredOptions) {
const optionsFiltered = fullOptions return options
.filter(option => { .filter(option => {
if (option.name && this.field) { return option.toLowerCase().indexOf(this.field.toLowerCase()) >= 0;
return option.name.toLowerCase().indexOf(this.field.toLowerCase()) >= 0;
}
return false;
}).map((option, index) => { }).map((option, index) => {
return { return {
optionId: index, optionId: index,
name: option.name, name: option
color: option.color
}; };
}); });
return optionsFiltered;
} }
const optionsFiltered = fullOptions.map((option, index) => { return options.map((option, index) => {
return { return {
optionId: index, optionId: index,
name: option.name, name: option
color: option.color
}; };
}); });
return optionsFiltered;
} }
}, },
watch: { watch: {
field(newValue, oldValue) { field(newValue, oldValue) {
if (newValue !== oldValue) { if (newValue !== oldValue) {
const data = { const data = {
model: this.model, model: this.model,
value: newValue value: newValue
@@ -160,17 +123,17 @@ export default {
} }
}, },
mounted() { mounted() {
this.autocompleteInputAndArrow = this.$refs.autoCompleteForm; this.options = this.model.options;
this.autocompleteInputElement = this.$refs.autoCompleteInput; this.autocompleteInputAndArrow = this.$el.getElementsByClassName('autocompleteInputAndArrow')[0];
if (this.model.options && this.model.options.length && !this.model.options[0].name) { this.autocompleteInputElement = this.$el.getElementsByClassName('autocompleteInput')[0];
// If options is only an array of string. if (this.options[0].name) {
this.options = this.model.options.map((option) => { // If "options" include name, value pair
return { this.optionNames = this.options.map((opt) => {
name: option return opt.name;
};
}); });
} else { } else {
this.options = this.model.options; // If options is only an array of string.
this.optionNames = this.options;
} }
}, },
destroyed() { destroyed() {
@@ -259,12 +222,6 @@ export default {
}); });
} }
}); });
},
itemStyle(option) {
if (option.color) {
return { '--optionIconColor': option.color };
}
} }
} }
}; };

View File

@@ -28,7 +28,6 @@
> >
<input <input
v-model="field" v-model="field"
:aria-label="model.name"
type="number" type="number"
:min="model.min" :min="model.min"
:max="model.max" :max="model.max"

View File

@@ -19,27 +19,27 @@
* this source code distribution or the Licensing information page available * this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define([
import EventEmitter from "EventEmitter"; './SimpleIndicator',
import SimpleIndicator from "./SimpleIndicator"; 'lodash'
], function (
class IndicatorAPI extends EventEmitter { SimpleIndicator,
constructor(openmct) { _
super(); ) {
function IndicatorAPI(openmct) {
this.openmct = openmct; this.openmct = openmct;
this.indicatorObjects = []; this.indicatorObjects = [];
} }
getIndicatorObjectsByPriority() { IndicatorAPI.prototype.getIndicatorObjectsByPriority = function () {
const sortedIndicators = this.indicatorObjects.sort((a, b) => b.priority - a.priority); const sortedIndicators = this.indicatorObjects.sort((a, b) => b.priority - a.priority);
return sortedIndicators; return sortedIndicators;
} };
simpleIndicator() { IndicatorAPI.prototype.simpleIndicator = function () {
return new SimpleIndicator(this.openmct); return new SimpleIndicator(this.openmct);
} };
/** /**
* Accepts an indicator object, which is a simple object * Accepts an indicator object, which is a simple object
@@ -62,16 +62,14 @@ class IndicatorAPI extends EventEmitter {
* myIndicator.iconClass("icon-info"); * myIndicator.iconClass("icon-info");
* *
*/ */
add(indicator) { IndicatorAPI.prototype.add = function (indicator) {
if (!indicator.priority) { if (!indicator.priority) {
indicator.priority = this.openmct.priority.DEFAULT; indicator.priority = this.openmct.priority.DEFAULT;
} }
this.indicatorObjects.push(indicator); this.indicatorObjects.push(indicator);
};
this.emit('addIndicator', indicator); return IndicatorAPI;
}
} });
export default IndicatorAPI;

View File

@@ -20,18 +20,13 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
import EventEmitter from 'EventEmitter'; define(['zepto', './res/indicator-template.html'],
import indicatorTemplate from './res/indicator-template.html'; function ($, indicatorTemplate) {
import { convertTemplateToHTML } from '@/utils/template/templateHelpers';
const DEFAULT_ICON_CLASS = 'icon-info'; const DEFAULT_ICON_CLASS = 'icon-info';
class SimpleIndicator extends EventEmitter { function SimpleIndicator(openmct) {
constructor(openmct) {
super();
this.openmct = openmct; this.openmct = openmct;
this.element = convertTemplateToHTML(indicatorTemplate)[0]; this.element = $(indicatorTemplate)[0];
this.priority = openmct.priority.DEFAULT; this.priority = openmct.priority.DEFAULT;
this.textElement = this.element.querySelector('.js-indicator-text'); this.textElement = this.element.querySelector('.js-indicator-text');
@@ -40,17 +35,10 @@ class SimpleIndicator extends EventEmitter {
this.text('New Indicator'); this.text('New Indicator');
this.description(''); this.description('');
this.iconClass(DEFAULT_ICON_CLASS); this.iconClass(DEFAULT_ICON_CLASS);
this.statusClass('');
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) { SimpleIndicator.prototype.text = function (text) {
if (text !== undefined && text !== this.textValue) { if (text !== undefined && text !== this.textValue) {
this.textValue = text; this.textValue = text;
this.textElement.innerText = text; this.textElement.innerText = text;
@@ -63,18 +51,18 @@ class SimpleIndicator extends EventEmitter {
} }
return this.textValue; return this.textValue;
} };
description(description) { SimpleIndicator.prototype.description = function (description) {
if (description !== undefined && description !== this.descriptionValue) { if (description !== undefined && description !== this.descriptionValue) {
this.descriptionValue = description; this.descriptionValue = description;
this.element.title = description; this.element.title = description;
} }
return this.descriptionValue; return this.descriptionValue;
} };
iconClass(iconClass) { SimpleIndicator.prototype.iconClass = function (iconClass) {
if (iconClass !== undefined && iconClass !== this.iconClassValue) { if (iconClass !== undefined && iconClass !== this.iconClassValue) {
// element.classList is precious and throws errors if you try and add // element.classList is precious and throws errors if you try and add
// or remove empty strings // or remove empty strings
@@ -90,15 +78,15 @@ class SimpleIndicator extends EventEmitter {
} }
return this.iconClassValue; return this.iconClassValue;
} };
statusClass(statusClass) { SimpleIndicator.prototype.statusClass = function (statusClass) {
if (arguments.length === 1 && statusClass !== this.statusClassValue) { if (statusClass !== undefined && statusClass !== this.statusClassValue) {
if (this.statusClassValue) { if (this.statusClassValue) {
this.element.classList.remove(this.statusClassValue); this.element.classList.remove(this.statusClassValue);
} }
if (statusClass !== undefined) { if (statusClass) {
this.element.classList.add(statusClass); this.element.classList.add(statusClass);
} }
@@ -106,15 +94,8 @@ class SimpleIndicator extends EventEmitter {
} }
return this.statusClassValue; return this.statusClassValue;
} };
click(event) { return SimpleIndicator;
this.emit('click', event);
} }
);
getElement() {
return this.element;
}
}
export default SimpleIndicator;

View File

@@ -36,7 +36,7 @@
<li <li
v-for="action in options.actions" v-for="action in options.actions"
:key="action.name" :key="action.name"
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']" :class="action.cssClass"
:title="action.description" :title="action.description"
:data-testid="action.testId || false" :data-testid="action.testId || false"
@click="action.onItemClicked" @click="action.onItemClicked"

View File

@@ -39,10 +39,11 @@ class InMemorySearchProvider {
* If max results is not specified in query, use this as default. * If max results is not specified in query, use this as default.
*/ */
this.DEFAULT_MAX_RESULTS = 100; this.DEFAULT_MAX_RESULTS = 100;
this.openmct = openmct; this.openmct = openmct;
this.indexedIds = {}; this.indexedIds = {};
this.indexedCompositions = {}; this.indexedCompositions = {};
this.indexedTags = {};
this.idsToIndex = []; this.idsToIndex = [];
this.pendingIndex = {}; this.pendingIndex = {};
this.pendingRequests = 0; this.pendingRequests = 0;
@@ -51,18 +52,11 @@ class InMemorySearchProvider {
/** /**
* If we don't have SharedWorkers available (e.g., iOS) * If we don't have SharedWorkers available (e.g., iOS)
*/ */
this.localIndexedDomainObjects = {}; this.localIndexedItems = {};
this.localIndexedAnnotationsByDomainObject = {};
this.localIndexedAnnotationsByTag = {};
this.pendingQueries = {}; this.pendingQueries = {};
this.onWorkerMessage = this.onWorkerMessage.bind(this); this.onWorkerMessage = this.onWorkerMessage.bind(this);
this.onWorkerMessageError = this.onWorkerMessageError.bind(this); this.onWorkerMessageError = this.onWorkerMessageError.bind(this);
this.localSearchForObjects = this.localSearchForObjects.bind(this);
this.localSearchForAnnotations = this.localSearchForAnnotations.bind(this);
this.localSearchForTags = this.localSearchForTags.bind(this);
this.localSearchForNotebookAnnotations = this.localSearchForNotebookAnnotations.bind(this);
this.onAnnotationCreation = this.onAnnotationCreation.bind(this);
this.onerror = this.onWorkerError.bind(this); this.onerror = this.onWorkerError.bind(this);
this.startIndexing = this.startIndexing.bind(this); this.startIndexing = this.startIndexing.bind(this);
@@ -82,39 +76,13 @@ class InMemorySearchProvider {
startIndexing() { startIndexing() {
const rootObject = this.openmct.objects.rootProvider.rootObject; const rootObject = this.openmct.objects.rootProvider.rootObject;
this.searchTypes = this.openmct.objects.SEARCH_TYPES;
this.supportedSearchTypes = [this.searchTypes.OBJECTS, this.searchTypes.ANNOTATIONS, this.searchTypes.NOTEBOOK_ANNOTATIONS, this.searchTypes.TAGS];
this.scheduleForIndexing(rootObject.identifier); this.scheduleForIndexing(rootObject.identifier);
this.indexAnnotations();
if (typeof SharedWorker !== 'undefined') { if (typeof SharedWorker !== 'undefined') {
this.worker = this.startSharedWorker(); this.worker = this.startSharedWorker();
} else { } else {
// we must be on iOS // we must be on iOS
} }
this.openmct.annotation.on('annotationCreated', this.onAnnotationCreation);
}
indexAnnotations() {
const theInMemorySearchProvider = this;
Object.values(this.openmct.objects.providers).forEach(objectProvider => {
if (objectProvider.getAllObjects) {
const allObjects = objectProvider.getAllObjects();
if (allObjects) {
Object.values(allObjects).forEach(domainObject => {
if (domainObject.type === 'annotation') {
theInMemorySearchProvider.scheduleForIndexing(domainObject.identifier);
}
});
}
}
});
} }
/** /**
@@ -130,60 +98,51 @@ class InMemorySearchProvider {
return intermediateResponse; return intermediateResponse;
} }
search(query, searchType) { /**
* Query the search provider for results.
*
* @param {String} input the string to search by.
* @param {Number} maxResults max number of results to return.
* @returns {Promise} a promise for a modelResults object.
*/
query(input, maxResults) {
if (!maxResults) {
maxResults = this.DEFAULT_MAX_RESULTS;
}
const queryId = uuid(); const queryId = uuid();
const pendingQuery = this.getIntermediateResponse(); const pendingQuery = this.getIntermediateResponse();
this.pendingQueries[queryId] = pendingQuery; this.pendingQueries[queryId] = pendingQuery;
const searchOptions = {
queryId,
searchType,
query,
maxResults: this.DEFAULT_MAX_RESULTS
};
if (this.worker) { if (this.worker) {
this.#dispatchSearchToWorker(searchOptions); this.dispatchSearch(queryId, input, maxResults);
} else { } else {
this.#localQueryFallBack(searchOptions); this.localSearch(queryId, input, maxResults);
} }
return pendingQuery.promise; return pendingQuery.promise;
} }
#localQueryFallBack({queryId, searchType, query, maxResults}) {
if (searchType === this.searchTypes.OBJECTS) {
return this.localSearchForObjects(queryId, query, maxResults);
} else if (searchType === this.searchTypes.ANNOTATIONS) {
return this.localSearchForAnnotations(queryId, query, maxResults);
} else if (searchType === this.searchTypes.NOTEBOOK_ANNOTATIONS) {
return this.localSearchForNotebookAnnotations(queryId, query, maxResults);
} else if (searchType === this.searchTypes.TAGS) {
return this.localSearchForTags(queryId, query, maxResults);
} else {
throw new Error(`🤷‍♂️ Unknown search type passed: ${searchType}`);
}
}
supportsSearchType(searchType) {
return this.supportedSearchTypes.includes(searchType);
}
/** /**
* Handle messages from the worker. * Handle messages from the worker. Only really knows how to handle search
* results, which are parsed, transformed into a modelResult object, which
* is used to resolve the corresponding promise.
* @private * @private
*/ */
async onWorkerMessage(event) { async onWorkerMessage(event) {
if (event.data.request !== 'search') {
return;
}
const pendingQuery = this.pendingQueries[event.data.queryId]; const pendingQuery = this.pendingQueries[event.data.queryId];
const modelResults = { const modelResults = {
total: event.data.total total: event.data.total
}; };
modelResults.hits = await Promise.all(event.data.results.map(async (hit) => { modelResults.hits = await Promise.all(event.data.results.map(async (hit) => {
if (hit && hit.keyString) {
const identifier = this.openmct.objects.parseKeyString(hit.keyString); const identifier = this.openmct.objects.parseKeyString(hit.keyString);
const domainObject = await this.openmct.objects.get(identifier); const domainObject = await this.openmct.objects.get(identifier);
return domainObject; return domainObject;
}
})); }));
pendingQuery.resolve(modelResults); pendingQuery.resolve(modelResults);
@@ -224,8 +183,7 @@ class InMemorySearchProvider {
/** /**
* Schedule an id to be indexed at a later date. If there are less * Schedule an id to be indexed at a later date. If there are less
* pending requests than the maximum allowed, this will kick off an indexing request. * pending requests then allowed, will kick off an indexing request.
* This is done only when indexing first begins and we need to index a lot of objects.
* *
* @private * @private
* @param {identifier} id to be indexed. * @param {identifier} id to be indexed.
@@ -258,15 +216,6 @@ class InMemorySearchProvider {
} }
} }
onAnnotationCreation(annotationObject) {
const objectProvider = this.openmct.objects.getProvider(annotationObject.identifier);
if (objectProvider === undefined || objectProvider.search === undefined) {
const provider = this;
provider.index(annotationObject);
}
}
onNameMutation(domainObject, name) { onNameMutation(domainObject, name) {
const provider = this; const provider = this;
@@ -274,13 +223,6 @@ class InMemorySearchProvider {
provider.index(domainObject); provider.index(domainObject);
} }
onTagMutation(domainObject, newTags) {
domainObject.tags = newTags;
const provider = this;
provider.index(domainObject);
}
onCompositionMutation(domainObject, composition) { onCompositionMutation(domainObject, composition) {
const provider = this; const provider = this;
const indexedComposition = domainObject.composition; const indexedComposition = domainObject.composition;
@@ -317,13 +259,6 @@ class InMemorySearchProvider {
'composition', 'composition',
this.onCompositionMutation.bind(this, domainObject) this.onCompositionMutation.bind(this, domainObject)
); );
if (domainObject.type === 'annotation') {
this.indexedTags[keyString] = this.openmct.objects.observe(
domainObject,
'tags',
this.onTagMutation.bind(this, domainObject)
);
}
} }
if ((keyString !== 'ROOT')) { if ((keyString !== 'ROOT')) {
@@ -382,83 +317,26 @@ class InMemorySearchProvider {
* @private * @private
* @returns {String} a unique query Id for the query. * @returns {String} a unique query Id for the query.
*/ */
#dispatchSearchToWorker({queryId, searchType, query, maxResults}) { dispatchSearch(queryId, searchInput, maxResults) {
const message = { const message = {
request: searchType.toString(), request: 'search',
input: query, input: searchInput,
maxResults, maxResults,
queryId queryId
}; };
this.worker.port.postMessage(message); this.worker.port.postMessage(message);
} }
localIndexTags(keyString, objectToIndex, model) {
// add new tags
model.tags.forEach(tagID => {
if (!this.localIndexedAnnotationsByTag[tagID]) {
this.localIndexedAnnotationsByTag[tagID] = [];
}
const existsInIndex = this.localIndexedAnnotationsByTag[tagID].some(indexedObject => {
return indexedObject.keyString === objectToIndex.keyString;
});
if (!existsInIndex) {
this.localIndexedAnnotationsByTag[tagID].push(objectToIndex);
}
});
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 * A local version of the same SharedWorker function
* if we don't have SharedWorkers available (e.g., iOS) * if we don't have SharedWorkers available (e.g., iOS)
*/ */
localIndexItem(keyString, model) { localIndexItem(keyString, model) {
const objectToIndex = { this.localIndexedItems[keyString] = {
type: model.type, type: model.type,
name: model.name, name: model.name,
keyString keyString
}; };
if (model && (model.type === 'annotation')) {
if (model.targets) {
this.localIndexAnnotation(objectToIndex, model);
}
if (model.tags) {
this.localIndexTags(keyString, objectToIndex, model);
}
} else {
this.localIndexedDomainObjects[keyString] = objectToIndex;
}
} }
/** /**
@@ -468,122 +346,21 @@ class InMemorySearchProvider {
* Gets search results from the indexedItems based on provided search * Gets search results from the indexedItems based on provided search
* input. Returns matching results from indexedItems * input. Returns matching results from indexedItems
*/ */
localSearchForObjects(queryId, searchInput, maxResults) { localSearch(queryId, searchInput, maxResults) {
// This results dictionary will have domain object ID keys which // This results dictionary will have domain object ID keys which
// point to the value the domain object's score. // point to the value the domain object's score.
let results = []; let results;
const input = searchInput.trim().toLowerCase(); const input = searchInput.trim().toLowerCase();
const message = { const message = {
request: 'searchForObjects', request: 'search',
results: [], results: {},
total: 0, total: 0,
queryId queryId
}; };
results = Object.values(this.localIndexedDomainObjects).filter((indexedItem) => { results = Object.values(this.localIndexedItems).filter((indexedItem) => {
return indexedItem.name.toLowerCase().includes(input); return indexedItem.name.toLowerCase().includes(input);
}) || [];
message.total = results.length;
message.results = results
.slice(0, maxResults);
const eventToReturn = {
data: message
};
this.onWorkerMessage(eventToReturn);
}
/**
* A local version of the same SharedWorker function
* if we don't have SharedWorkers available (e.g., iOS)
*/
localSearchForAnnotations(queryId, searchInput, maxResults) {
// This results dictionary will have domain object ID keys which
// point to the value the domain object's score.
let results = [];
const message = {
request: 'searchForAnnotations',
results: [],
total: 0,
queryId
};
results = this.localIndexedAnnotationsByDomainObject[searchInput] || [];
message.total = results.length;
message.results = results
.slice(0, maxResults);
const eventToReturn = {
data: message
};
this.onWorkerMessage(eventToReturn);
}
/**
* A local version of the same SharedWorker function
* if we don't have SharedWorkers available (e.g., iOS)
*/
localSearchForTags(queryId, matchingTagKeys, maxResults) {
let results = [];
const message = {
request: 'searchForTags',
results: [],
total: 0,
queryId
};
if (matchingTagKeys) {
matchingTagKeys.forEach(matchingTag => {
const matchingAnnotations = this.localIndexedAnnotationsByTag[matchingTag];
if (matchingAnnotations) {
matchingAnnotations.forEach(matchingAnnotation => {
const existsInResults = results.some(indexedObject => {
return matchingAnnotation.keyString === indexedObject.keyString;
}); });
if (!existsInResults) {
results.push(matchingAnnotation);
}
});
}
});
}
message.total = results.length;
message.results = results
.slice(0, maxResults);
const eventToReturn = {
data: message
};
this.onWorkerMessage(eventToReturn);
}
/**
* A local version of the same SharedWorker function
* if we don't have SharedWorkers available (e.g., iOS)
*/
localSearchForNotebookAnnotations(queryId, {entryId, targetKeyString}, maxResults) {
// This results dictionary will have domain object ID keys which
// point to the value the domain object's score.
let results = [];
const message = {
request: 'searchForNotebookAnnotations',
results: [],
total: 0,
queryId
};
const matchingAnnotations = this.localIndexedAnnotationsByDomainObject[targetKeyString];
if (matchingAnnotations) {
results = matchingAnnotations.filter(matchingAnnotation => {
if (!matchingAnnotation.targets) {
return false;
}
const target = matchingAnnotation.targets[targetKeyString];
return (target && target.entryId && (target.entryId === entryId));
});
}
message.total = results.length; message.total = results.length;
message.results = results message.results = results

View File

@@ -26,27 +26,16 @@
(function () { (function () {
// An object composed of domain object IDs and models // An object composed of domain object IDs and models
// {id: domainObject's ID, name: domainObject's name} // {id: domainObject's ID, name: domainObject's name}
const indexedDomainObjects = {}; const indexedItems = {};
const indexedAnnotationsByDomainObject = {};
const indexedAnnotationsByTag = {};
self.onconnect = function (e) { self.onconnect = function (e) {
const port = e.ports[0]; const port = e.ports[0];
port.onmessage = function (event) { port.onmessage = function (event) {
const requestType = event.data.request; if (event.data.request === 'index') {
if (requestType === 'index') {
indexItem(event.data.keyString, event.data.model); indexItem(event.data.keyString, event.data.model);
} else if (requestType === 'OBJECTS') { } else if (event.data.request === 'search') {
port.postMessage(searchForObjects(event.data)); port.postMessage(search(event.data));
} else if (requestType === 'ANNOTATIONS') {
port.postMessage(searchForAnnotations(event.data));
} else if (requestType === 'TAGS') {
port.postMessage(searchForTags(event.data));
} else if (requestType === 'NOTEBOOK_ANNOTATIONS') {
port.postMessage(searchForNotebookAnnotations(event.data));
} else {
throw new Error(`Unknown request ${event.data.request}`);
} }
}; };
@@ -59,70 +48,12 @@
console.error('Error on feed', error); console.error('Error on feed', error);
}; };
function indexAnnotation(objectToIndex, model) {
Object.keys(model.targets).forEach(targetID => {
if (!indexedAnnotationsByDomainObject[targetID]) {
indexedAnnotationsByDomainObject[targetID] = [];
}
objectToIndex.targets = model.targets;
objectToIndex.tags = model.tags;
const existsInIndex = indexedAnnotationsByDomainObject[targetID].some(indexedObject => {
return indexedObject.keyString === objectToIndex.keyString;
});
if (!existsInIndex) {
indexedAnnotationsByDomainObject[targetID].push(objectToIndex);
}
});
}
function indexTags(keyString, objectToIndex, model) {
// add new tags
model.tags.forEach(tagID => {
if (!indexedAnnotationsByTag[tagID]) {
indexedAnnotationsByTag[tagID] = [];
}
const existsInIndex = indexedAnnotationsByTag[tagID].some(indexedObject => {
return indexedObject.keyString === objectToIndex.keyString;
});
if (!existsInIndex) {
indexedAnnotationsByTag[tagID].push(objectToIndex);
}
});
// remove old tags
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) { function indexItem(keyString, model) {
const objectToIndex = { indexedItems[keyString] = {
type: model.type, type: model.type,
name: model.name, name: model.name,
keyString keyString
}; };
if (model && (model.type === 'annotation')) {
if (model.targets) {
indexAnnotation(objectToIndex, model);
}
if (model.tags) {
indexTags(keyString, objectToIndex, model);
}
} else {
indexedDomainObjects[keyString] = objectToIndex;
}
} }
/** /**
@@ -134,98 +65,21 @@
* * maxResults: The maximum number of search results desired * * maxResults: The maximum number of search results desired
* * queryId: an id identifying this query, will be returned. * * queryId: an id identifying this query, will be returned.
*/ */
function searchForObjects(data) { function search(data) {
let results = []; // This results dictionary will have domain object ID keys which
// point to the value the domain object's score.
let results;
const input = data.input.trim().toLowerCase(); const input = data.input.trim().toLowerCase();
const message = { const message = {
request: 'searchForObjects', request: 'search',
results: [],
total: 0,
queryId: data.queryId
};
results = Object.values(indexedDomainObjects).filter((indexedItem) => {
return indexedItem.name.toLowerCase().includes(input);
}) || [];
message.total = results.length;
message.results = results
.slice(0, data.maxResults);
return message;
}
function searchForAnnotations(data) {
let results = [];
const message = {
request: 'searchForAnnotations',
results: [],
total: 0,
queryId: data.queryId
};
results = indexedAnnotationsByDomainObject[data.input] || [];
message.total = results.length;
message.results = results
.slice(0, data.maxResults);
return message;
}
function searchForTags(data) {
let results = [];
const message = {
request: 'searchForTags',
results: [],
total: 0,
queryId: data.queryId
};
if (data.input) {
data.input.forEach(matchingTag => {
const matchingAnnotations = indexedAnnotationsByTag[matchingTag];
if (matchingAnnotations) {
matchingAnnotations.forEach(matchingAnnotation => {
const existsInResults = results.some(indexedObject => {
return matchingAnnotation.keyString === indexedObject.keyString;
});
if (!existsInResults) {
results.push(matchingAnnotation);
}
});
}
});
}
message.total = results.length;
message.results = results
.slice(0, data.maxResults);
return message;
}
function searchForNotebookAnnotations(data) {
let results = [];
const message = {
request: 'searchForNotebookAnnotations',
results: {}, results: {},
total: 0, total: 0,
queryId: data.queryId queryId: data.queryId
}; };
const matchingAnnotations = indexedAnnotationsByDomainObject[data.input.targetKeyString]; results = Object.values(indexedItems).filter((indexedItem) => {
if (matchingAnnotations) { return indexedItem.name.toLowerCase().includes(input);
results = matchingAnnotations.filter(matchingAnnotation => {
if (!matchingAnnotation.targets) {
return false;
}
const target = matchingAnnotation.targets[data.input.targetKeyString];
return (target && target.entryId && (target.entryId === data.input.entryId));
}); });
}
message.total = results.length; message.total = results.length;
message.results = results message.results = results

View File

@@ -30,55 +30,15 @@ import Transaction from './Transaction';
import ConflictError from './ConflictError'; import ConflictError from './ConflictError';
import InMemorySearchProvider from './InMemorySearchProvider'; import InMemorySearchProvider from './InMemorySearchProvider';
/**
* Uniquely identifies a domain object.
*
* @typedef Identifier
* @memberof module:openmct.ObjectAPI~
* @property {string} namespace the namespace to/from which this domain
* object should be loaded/stored.
* @property {string} key a unique identifier for the domain object
* within that namespace
*/
/**
* A domain object is an entity of relevance to a user's workflow, that
* should appear as a distinct and meaningful object within the user
* interface. Examples of domain objects are folders, telemetry sensors,
* and so forth.
*
* A few common properties are defined for domain objects. Beyond these,
* individual types of domain objects may add more as they see fit.
*
* @typedef DomainObject
* @property {module:openmct.ObjectAPI~Identifier} identifier a key/namespace pair which
* uniquely identifies this domain object
* @property {string} type the type of domain object
* @property {string} name the human-readable name for this domain object
* @property {string} [creator] the user name of the creator of this domain
* object
* @property {number} [modified] the time, in milliseconds since the UNIX
* epoch, at which this domain object was last modified
* @property {module:openmct.ObjectAPI~Identifier[]} [composition] if
* present, this will be used by the default composition provider
* to load domain objects
* @memberof module:openmct
*/
/** /**
* Utilities for loading, saving, and manipulating domain objects. * Utilities for loading, saving, and manipulating domain objects.
* @interface ObjectAPI * @interface ObjectAPI
* @memberof module:openmct * @memberof module:openmct
*/ */
export default class ObjectAPI {
constructor(typeRegistry, openmct) { function ObjectAPI(typeRegistry, openmct) {
this.openmct = openmct; this.openmct = openmct;
this.typeRegistry = typeRegistry; this.typeRegistry = typeRegistry;
this.SEARCH_TYPES = Object.freeze({
OBJECTS: 'OBJECTS',
ANNOTATIONS: 'ANNOTATIONS',
NOTEBOOK_ANNOTATIONS: 'NOTEBOOK_ANNOTATIONS',
TAGS: 'TAGS'
});
this.eventEmitter = new EventEmitter(); this.eventEmitter = new EventEmitter();
this.providers = {}; this.providers = {};
this.rootRegistry = new RootRegistry(openmct); this.rootRegistry = new RootRegistry(openmct);
@@ -96,31 +56,41 @@ export default class ObjectAPI {
} }
/** /**
* Retrieve the provider for a given identifier. * Set fallback provider, this is an internal API for legacy reasons.
* @private
*/ */
getProvider(identifier) { ObjectAPI.prototype.supersecretSetFallbackProvider = function (p) {
this.fallbackProvider = p;
};
/**
* Retrieve the provider for a given identifier.
* @private
*/
ObjectAPI.prototype.getProvider = function (identifier) {
if (identifier.key === 'ROOT') { if (identifier.key === 'ROOT') {
return this.rootProvider; return this.rootProvider;
} }
return this.providers[identifier.namespace] || this.fallbackProvider; return this.providers[identifier.namespace] || this.fallbackProvider;
} };
/** /**
* Get an active transaction instance * Get an active transaction instance
* @returns {Transaction} a transaction object * @returns {Transaction} a transaction object
*/ */
getActiveTransaction() { ObjectAPI.prototype.getActiveTransaction = function () {
return this.transaction; return this.transaction;
} };
/** /**
* Get the root-level object. * Get the root-level object.
* @returns {Promise.<DomainObject>} a promise for the root object * @returns {Promise.<DomainObject>} a promise for the root object
*/ */
getRoot() { ObjectAPI.prototype.getRoot = function () {
return this.rootProvider.get(); return this.rootProvider.get();
} };
/** /**
* Register a new object provider for a particular namespace. * Register a new object provider for a particular namespace.
@@ -131,9 +101,9 @@ export default class ObjectAPI {
* @memberof {module:openmct.ObjectAPI#} * @memberof {module:openmct.ObjectAPI#}
* @name addProvider * @name addProvider
*/ */
addProvider(namespace, provider) { ObjectAPI.prototype.addProvider = function (namespace, provider) {
this.providers[namespace] = provider; this.providers[namespace] = provider;
} };
/** /**
* Provides the ability to read, write, and delete domain objects. * Provides the ability to read, write, and delete domain objects.
@@ -189,7 +159,7 @@ export default class ObjectAPI {
* has been saved, or be rejected if it cannot be saved * has been saved, or be rejected if it cannot be saved
*/ */
get(identifier, abortSignal) { ObjectAPI.prototype.get = function (identifier, abortSignal) {
let keystring = this.makeKeyString(identifier); let keystring = this.makeKeyString(identifier);
if (this.cache[keystring] !== undefined) { if (this.cache[keystring] !== undefined) {
@@ -233,11 +203,7 @@ export default class ObjectAPI {
delete this.cache[keystring]; delete this.cache[keystring];
if (!result) {
//no result means resource either doesn't exist or is missing
//otherwise it's an error, and we shouldn't apply interceptors
result = this.applyGetInterceptors(identifier); result = this.applyGetInterceptors(identifier);
}
return result; return result;
}); });
@@ -245,7 +211,7 @@ export default class ObjectAPI {
this.cache[keystring] = objectPromise; this.cache[keystring] = objectPromise;
return objectPromise; return objectPromise;
} };
/** /**
* Search for domain objects. * Search for domain objects.
@@ -259,33 +225,23 @@ export default class ObjectAPI {
* @memberof module:openmct.ObjectAPI# * @memberof module:openmct.ObjectAPI#
* @param {string} query the term to search for * @param {string} query the term to search for
* @param {AbortController.signal} abortSignal (optional) signal to cancel downstream fetch requests * @param {AbortController.signal} abortSignal (optional) signal to cancel downstream fetch requests
* @param {string} searchType the type of search as defined by SEARCH_TYPES
* @returns {Array.<Promise.<module:openmct.DomainObject>>} * @returns {Array.<Promise.<module:openmct.DomainObject>>}
* an array of promises returned from each object provider's search function * an array of promises returned from each object provider's search function
* each resolving to domain objects matching provided search query and options. * each resolving to domain objects matching provided search query and options.
*/ */
search(query, abortSignal, searchType = this.SEARCH_TYPES.OBJECTS) { ObjectAPI.prototype.search = function (query, abortSignal) {
if (!Object.keys(this.SEARCH_TYPES).includes(searchType.toUpperCase())) {
throw new Error(`Unknown search type: ${searchType}`);
}
const searchPromises = Object.values(this.providers) const searchPromises = Object.values(this.providers)
.filter(provider => { .filter(provider => provider.search !== undefined)
return ((provider.supportsSearchType !== undefined) && provider.supportsSearchType(searchType)); .map(provider => provider.search(query, abortSignal));
}) // abortSignal doesn't seem to be used in generic search?
.map(provider => provider.search(query, abortSignal, searchType)); searchPromises.push(this.inMemorySearchProvider.query(query, null)
if (!this.inMemorySearchProvider.supportsSearchType(searchType)) {
throw new Error(`${searchType} not implemented in inMemorySearchProvider`);
}
searchPromises.push(this.inMemorySearchProvider.search(query, searchType)
.then(results => results.hits .then(results => results.hits
.map(hit => { .map(hit => {
return hit; return hit;
}))); })));
return searchPromises; return searchPromises;
} };
/** /**
* Will fetch object for the given identifier, returning a version of the object that will automatically keep * Will fetch object for the given identifier, returning a version of the object that will automatically keep
@@ -298,7 +254,7 @@ export default class ObjectAPI {
* @returns {Promise.<MutableDomainObject>} a promise that will resolve with a MutableDomainObject if * @returns {Promise.<MutableDomainObject>} a promise that will resolve with a MutableDomainObject if
* the object can be mutated. * the object can be mutated.
*/ */
getMutable(identifier) { ObjectAPI.prototype.getMutable = function (identifier) {
if (!this.supportsMutation(identifier)) { if (!this.supportsMutation(identifier)) {
throw new Error(`Object "${this.makeKeyString(identifier)}" does not support mutation.`); throw new Error(`Object "${this.makeKeyString(identifier)}" does not support mutation.`);
} }
@@ -306,7 +262,7 @@ export default class ObjectAPI {
return this.get(identifier).then((object) => { return this.get(identifier).then((object) => {
return this._toMutable(object); return this._toMutable(object);
}); });
} };
/** /**
* This function is for cleaning up a mutable domain object when you're done with it. * This function is for cleaning up a mutable domain object when you're done with it.
@@ -314,44 +270,45 @@ export default class ObjectAPI {
* platform (eg. passed into a `view()` function) then the platform is responsible for its lifecycle. * platform (eg. passed into a `view()` function) then the platform is responsible for its lifecycle.
* @param {MutableDomainObject} domainObject * @param {MutableDomainObject} domainObject
*/ */
destroyMutable(domainObject) { ObjectAPI.prototype.destroyMutable = function (domainObject) {
if (domainObject.isMutable) { if (domainObject.isMutable) {
return domainObject.$destroy(); return domainObject.$destroy();
} else { } else {
throw new Error("Attempted to destroy non-mutable domain object"); throw new Error("Attempted to destroy non-mutable domain object");
} }
} };
delete() { ObjectAPI.prototype.delete = function () {
throw new Error('Delete not implemented'); throw new Error('Delete not implemented');
} };
isPersistable(idOrKeyString) { ObjectAPI.prototype.isPersistable = function (idOrKeyString) {
let identifier = utils.parseKeyString(idOrKeyString); let identifier = utils.parseKeyString(idOrKeyString);
let provider = this.getProvider(identifier); let provider = this.getProvider(identifier);
return provider !== undefined return provider !== undefined
&& provider.create !== undefined && provider.create !== undefined
&& provider.update !== undefined; && provider.update !== undefined;
} };
isMissing(domainObject) { ObjectAPI.prototype.isMissing = function (domainObject) {
let identifier = utils.makeKeyString(domainObject.identifier); let identifier = utils.makeKeyString(domainObject.identifier);
let missingName = 'Missing: ' + identifier; let missingName = 'Missing: ' + identifier;
return domainObject.name === missingName; return domainObject.name === missingName;
} };
/** /**
* Save this domain object in its current state. * Save this domain object in its current state. EXPERIMENTAL
* *
* @private
* @memberof module:openmct.ObjectAPI# * @memberof module:openmct.ObjectAPI#
* @param {module:openmct.DomainObject} domainObject the domain object to * @param {module:openmct.DomainObject} domainObject the domain object to
* save * save
* @returns {Promise} a promise which will resolve when the domain object * @returns {Promise} a promise which will resolve when the domain object
* has been saved, or be rejected if it cannot be saved * has been saved, or be rejected if it cannot be saved
*/ */
save(domainObject) { ObjectAPI.prototype.save = function (domainObject) {
let provider = this.getProvider(domainObject.identifier); let provider = this.getProvider(domainObject.identifier);
let savedResolve; let savedResolve;
let savedReject; let savedReject;
@@ -359,7 +316,7 @@ export default class ObjectAPI {
if (!this.isPersistable(domainObject.identifier)) { if (!this.isPersistable(domainObject.identifier)) {
result = Promise.reject('Object provider does not support saving'); result = Promise.reject('Object provider does not support saving');
} else if (this.#hasAlreadyBeenPersisted(domainObject)) { } else if (hasAlreadyBeenPersisted(domainObject)) {
result = Promise.resolve(true); result = Promise.resolve(true);
} else { } else {
const persistedTime = Date.now(); const persistedTime = Date.now();
@@ -388,25 +345,25 @@ export default class ObjectAPI {
} }
return result; return result;
} };
/** /**
* After entering into edit mode, creates a new instance of Transaction to keep track of changes in Objects * After entering into edit mode, creates a new instance of Transaction to keep track of changes in Objects
*/ */
startTransaction() { ObjectAPI.prototype.startTransaction = function () {
if (this.isTransactionActive()) { if (this.isTransactionActive()) {
throw new Error("Unable to start new Transaction: Previous Transaction is active"); throw new Error("Unable to start new Transaction: Previous Transaction is active");
} }
this.transaction = new Transaction(this); this.transaction = new Transaction(this);
} };
/** /**
* Clear instance of Transaction * Clear instance of Transaction
*/ */
endTransaction() { ObjectAPI.prototype.endTransaction = function () {
this.transaction = null; this.transaction = null;
} };
/** /**
* Add a root-level object. * Add a root-level object.
@@ -419,9 +376,9 @@ export default class ObjectAPI {
* @method addRoot * @method addRoot
* @memberof module:openmct.ObjectAPI# * @memberof module:openmct.ObjectAPI#
*/ */
addRoot(identifier, priority) { ObjectAPI.prototype.addRoot = function (identifier, priority) {
this.rootRegistry.addRoot(identifier, priority); this.rootRegistry.addRoot(identifier, priority);
} };
/** /**
* Register an object interceptor that transforms a domain object requested via module:openmct.ObjectAPI.get * Register an object interceptor that transforms a domain object requested via module:openmct.ObjectAPI.get
@@ -432,30 +389,30 @@ export default class ObjectAPI {
* @method addGetInterceptor * @method addGetInterceptor
* @memberof module:openmct.InterceptorRegistry# * @memberof module:openmct.InterceptorRegistry#
*/ */
addGetInterceptor(interceptorDef) { ObjectAPI.prototype.addGetInterceptor = function (interceptorDef) {
this.interceptorRegistry.addInterceptor(interceptorDef); this.interceptorRegistry.addInterceptor(interceptorDef);
} };
/** /**
* Retrieve the interceptors for a given domain object. * Retrieve the interceptors for a given domain object.
* @private * @private
*/ */
#listGetInterceptors(identifier, object) { ObjectAPI.prototype.listGetInterceptors = function (identifier, object) {
return this.interceptorRegistry.getInterceptors(identifier, object); return this.interceptorRegistry.getInterceptors(identifier, object);
} };
/** /**
* Inovke interceptors if applicable for a given domain object. * Inovke interceptors if applicable for a given domain object.
* @private * @private
*/ */
applyGetInterceptors(identifier, domainObject) { ObjectAPI.prototype.applyGetInterceptors = function (identifier, domainObject) {
const interceptors = this.#listGetInterceptors(identifier, domainObject); const interceptors = this.listGetInterceptors(identifier, domainObject);
interceptors.forEach(interceptor => { interceptors.forEach(interceptor => {
domainObject = interceptor.invoke(identifier, domainObject); domainObject = interceptor.invoke(identifier, domainObject);
}); });
return domainObject; return domainObject;
} };
/** /**
* Return relative url path from a given object path * Return relative url path from a given object path
@@ -463,12 +420,13 @@ export default class ObjectAPI {
* @param {Array} objectPath * @param {Array} objectPath
* @returns {string} relative url for object * @returns {string} relative url for object
*/ */
getRelativePath(objectPath) { ObjectAPI.prototype.getRelativePath = function (objectPath) {
return objectPath return objectPath
.map(p => this.makeKeyString(p.identifier)) .map(p => this.makeKeyString(p.identifier))
.reverse() .reverse()
.join('/'); .join('/')
} ;
};
/** /**
* Modify a domain object. * Modify a domain object.
@@ -478,7 +436,7 @@ export default class ObjectAPI {
* @method mutate * @method mutate
* @memberof module:openmct.ObjectAPI# * @memberof module:openmct.ObjectAPI#
*/ */
mutate(domainObject, path, value) { ObjectAPI.prototype.mutate = function (domainObject, path, value) {
if (!this.supportsMutation(domainObject.identifier)) { if (!this.supportsMutation(domainObject.identifier)) {
throw `Error: Attempted to mutate immutable object ${domainObject.name}`; throw `Error: Attempted to mutate immutable object ${domainObject.name}`;
} }
@@ -505,12 +463,12 @@ export default class ObjectAPI {
} else { } else {
this.save(domainObject); this.save(domainObject);
} }
} };
/** /**
* @private * @private
*/ */
_toMutable(object) { ObjectAPI.prototype._toMutable = function (object) {
let mutableObject; let mutableObject;
if (object.isMutable) { if (object.isMutable) {
@@ -540,14 +498,14 @@ export default class ObjectAPI {
} }
return mutableObject; return mutableObject;
} };
/** /**
* Updates a domain object based on its latest persisted state. Note that this will mutate the provided object. * Updates a domain object based on its latest persisted state. Note that this will mutate the provided object.
* @param {module:openmct.DomainObject} domainObject an object to refresh from its persistence store * @param {module:openmct.DomainObject} domainObject an object to refresh from its persistence store
* @returns {Promise} the provided object, updated to reflect the latest persisted state of the object. * @returns {Promise} the provided object, updated to reflect the latest persisted state of the object.
*/ */
async refresh(domainObject) { ObjectAPI.prototype.refresh = async function (domainObject) {
const refreshedObject = await this.get(domainObject.identifier); const refreshedObject = await this.get(domainObject.identifier);
if (domainObject.isMutable) { if (domainObject.isMutable) {
@@ -557,15 +515,15 @@ export default class ObjectAPI {
} }
return domainObject; return domainObject;
} };
/** /**
* @param module:openmct.ObjectAPI~Identifier identifier An object identifier * @param module:openmct.ObjectAPI~Identifier identifier An object identifier
* @returns {boolean} true if the object can be mutated, otherwise returns false * @returns {boolean} true if the object can be mutated, otherwise returns false
*/ */
supportsMutation(identifier) { ObjectAPI.prototype.supportsMutation = function (identifier) {
return this.isPersistable(identifier); return this.isPersistable(identifier);
} };
/** /**
* Observe changes to a domain object. * Observe changes to a domain object.
@@ -576,7 +534,7 @@ export default class ObjectAPI {
* @method observe * @method observe
* @memberof module:openmct.ObjectAPI# * @memberof module:openmct.ObjectAPI#
*/ */
observe(domainObject, path, callback) { ObjectAPI.prototype.observe = function (domainObject, path, callback) {
if (domainObject.isMutable) { if (domainObject.isMutable) {
return domainObject.$observe(path, callback); return domainObject.$observe(path, callback);
} else { } else {
@@ -585,38 +543,38 @@ export default class ObjectAPI {
return () => mutable.$destroy(); return () => mutable.$destroy();
} }
} };
/** /**
* @param {module:openmct.ObjectAPI~Identifier} identifier * @param {module:openmct.ObjectAPI~Identifier} identifier
* @returns {string} A string representation of the given identifier, including namespace and key * @returns {string} A string representation of the given identifier, including namespace and key
*/ */
makeKeyString(identifier) { ObjectAPI.prototype.makeKeyString = function (identifier) {
return utils.makeKeyString(identifier); return utils.makeKeyString(identifier);
} };
/** /**
* @param {string} keyString A string representation of the given identifier, that is, a namespace and key separated by a colon. * @param {string} keyString A string representation of the given identifier, that is, a namespace and key separated by a colon.
* @returns {module:openmct.ObjectAPI~Identifier} An identifier object * @returns {module:openmct.ObjectAPI~Identifier} An identifier object
*/ */
parseKeyString(keyString) { ObjectAPI.prototype.parseKeyString = function (keyString) {
return utils.parseKeyString(keyString); return utils.parseKeyString(keyString);
} };
/** /**
* Given any number of identifiers, will return true if they are all equal, otherwise false. * Given any number of identifiers, will return true if they are all equal, otherwise false.
* @param {module:openmct.ObjectAPI~Identifier[]} identifiers * @param {module:openmct.ObjectAPI~Identifier[]} identifiers
*/ */
areIdsEqual(...identifiers) { ObjectAPI.prototype.areIdsEqual = function (...identifiers) {
return identifiers.map(utils.parseKeyString) return identifiers.map(utils.parseKeyString)
.every(identifier => { .every(identifier => {
return identifier === identifiers[0] return identifier === identifiers[0]
|| (identifier.namespace === identifiers[0].namespace || (identifier.namespace === identifiers[0].namespace
&& identifier.key === identifiers[0].key); && identifier.key === identifiers[0].key);
}); });
} };
getOriginalPath(identifier, path = []) { ObjectAPI.prototype.getOriginalPath = function (identifier, path = []) {
return this.get(identifier).then((domainObject) => { return this.get(identifier).then((domainObject) => {
path.push(domainObject); path.push(domainObject);
let location = domainObject.location; let location = domainObject.location;
@@ -627,22 +585,58 @@ export default class ObjectAPI {
return path; return path;
} }
}); });
} };
isObjectPathToALink(domainObject, objectPath) { ObjectAPI.prototype.isObjectPathToALink = function (domainObject, objectPath) {
return objectPath !== undefined return objectPath !== undefined
&& objectPath.length > 1 && objectPath.length > 1
&& domainObject.location !== this.makeKeyString(objectPath[1].identifier); && domainObject.location !== this.makeKeyString(objectPath[1].identifier);
} };
isTransactionActive() { ObjectAPI.prototype.isTransactionActive = function () {
return Boolean(this.transaction && this.openmct.editor.isEditing()); return Boolean(this.transaction && this.openmct.editor.isEditing());
} };
#hasAlreadyBeenPersisted(domainObject) { /**
* Uniquely identifies a domain object.
*
* @typedef Identifier
* @memberof module:openmct.ObjectAPI~
* @property {string} namespace the namespace to/from which this domain
* object should be loaded/stored.
* @property {string} key a unique identifier for the domain object
* within that namespace
*/
/**
* A domain object is an entity of relevance to a user's workflow, that
* should appear as a distinct and meaningful object within the user
* interface. Examples of domain objects are folders, telemetry sensors,
* and so forth.
*
* A few common properties are defined for domain objects. Beyond these,
* individual types of domain objects may add more as they see fit.
*
* @property {module:openmct.ObjectAPI~Identifier} identifier a key/namespace pair which
* uniquely identifies this domain object
* @property {string} type the type of domain object
* @property {string} name the human-readable name for this domain object
* @property {string} [creator] the user name of the creator of this domain
* object
* @property {number} [modified] the time, in milliseconds since the UNIX
* epoch, at which this domain object was last modified
* @property {module:openmct.ObjectAPI~Identifier[]} [composition] if
* present, this will be used by the default composition provider
* to load domain objects
* @typedef DomainObject
* @memberof module:openmct
*/
function hasAlreadyBeenPersisted(domainObject) {
const result = domainObject.persisted !== undefined const result = domainObject.persisted !== undefined
&& domainObject.persisted >= domainObject.modified; && domainObject.persisted >= domainObject.modified;
return result; return result;
} }
}
export default ObjectAPI;

View File

@@ -17,16 +17,13 @@ describe("The Object API Search Function", () => {
openmct = createOpenMct(); openmct = createOpenMct();
mockObjectProvider = jasmine.createSpyObj("mock object provider", [ mockObjectProvider = jasmine.createSpyObj("mock object provider", [
"search", "supportsSearchType" "search"
]); ]);
anotherMockObjectProvider = jasmine.createSpyObj("another mock object provider", [ anotherMockObjectProvider = jasmine.createSpyObj("another mock object provider", [
"search", "supportsSearchType" "search"
]); ]);
openmct.objects.addProvider('objects', mockObjectProvider); openmct.objects.addProvider('objects', mockObjectProvider);
openmct.objects.addProvider('other-objects', anotherMockObjectProvider); openmct.objects.addProvider('other-objects', anotherMockObjectProvider);
mockObjectProvider.supportsSearchType.and.callFake(() => {
return true;
});
mockObjectProvider.search.and.callFake(() => { mockObjectProvider.search.and.callFake(() => {
return new Promise(resolve => { return new Promise(resolve => {
const mockProviderSearch = { const mockProviderSearch = {
@@ -41,9 +38,6 @@ describe("The Object API Search Function", () => {
}, MOCK_PROVIDER_SEARCH_DELAY); }, MOCK_PROVIDER_SEARCH_DELAY);
}); });
}); });
anotherMockObjectProvider.supportsSearchType.and.callFake(() => {
return true;
});
anotherMockObjectProvider.search.and.callFake(() => { anotherMockObjectProvider.search.and.callFake(() => {
return new Promise(resolve => { return new Promise(resolve => {
const anotherMockProviderSearch = { const anotherMockProviderSearch = {
@@ -116,8 +110,8 @@ describe("The Object API Search Function", () => {
namespace: '' namespace: ''
}); });
openmct.objects.addProvider('foo', defaultObjectProvider); openmct.objects.addProvider('foo', defaultObjectProvider);
spyOn(openmct.objects.inMemorySearchProvider, "search").and.callThrough(); spyOn(openmct.objects.inMemorySearchProvider, "query").and.callThrough();
spyOn(openmct.objects.inMemorySearchProvider, "localSearchForObjects").and.callThrough(); spyOn(openmct.objects.inMemorySearchProvider, "localSearch").and.callThrough();
openmct.on('start', async () => { openmct.on('start', async () => {
mockIdentifier1 = { mockIdentifier1 = {
@@ -161,7 +155,7 @@ describe("The Object API Search Function", () => {
it("can provide indexing without a provider", () => { it("can provide indexing without a provider", () => {
openmct.objects.search('foo'); openmct.objects.search('foo');
expect(openmct.objects.inMemorySearchProvider.search).toHaveBeenCalled(); expect(openmct.objects.inMemorySearchProvider.query).toHaveBeenCalled();
}); });
it("can do partial search", async () => { it("can do partial search", async () => {
@@ -183,22 +177,16 @@ describe("The Object API Search Function", () => {
}); });
describe("Without Shared Workers", () => { describe("Without Shared Workers", () => {
let sharedWorkerToRestore;
beforeEach(async () => { beforeEach(async () => {
// use local worker
sharedWorkerToRestore = openmct.objects.inMemorySearchProvider.worker;
openmct.objects.inMemorySearchProvider.worker = null; openmct.objects.inMemorySearchProvider.worker = null;
// reindex locally // reindex locally
await openmct.objects.inMemorySearchProvider.index(mockDomainObject1); await openmct.objects.inMemorySearchProvider.index(mockDomainObject1);
await openmct.objects.inMemorySearchProvider.index(mockDomainObject2); await openmct.objects.inMemorySearchProvider.index(mockDomainObject2);
await openmct.objects.inMemorySearchProvider.index(mockDomainObject3); await openmct.objects.inMemorySearchProvider.index(mockDomainObject3);
}); });
afterEach(() => {
openmct.objects.inMemorySearchProvider.worker = sharedWorkerToRestore;
});
it("calls local search", () => { it("calls local search", () => {
openmct.objects.search('foo'); openmct.objects.search('foo');
expect(openmct.objects.inMemorySearchProvider.localSearchForObjects).toHaveBeenCalled(); expect(openmct.objects.inMemorySearchProvider.localSearch).toHaveBeenCalled();
}); });
it("can do partial search", async () => { it("can do partial search", async () => {

View File

@@ -7,7 +7,6 @@
<div class="c-overlay__outer"> <div class="c-overlay__outer">
<button <button
v-if="dismissable" v-if="dismissable"
aria-label="Close"
class="c-click-icon c-overlay__close-button icon-x" class="c-click-icon c-overlay__close-button icon-x"
@click="destroy" @click="destroy"
></button> ></button>

File diff suppressed because it is too large Load Diff

View File

@@ -21,7 +21,7 @@
*****************************************************************************/ *****************************************************************************/
import { createOpenMct, resetApplicationState } from 'utils/testing'; import { createOpenMct, resetApplicationState } from 'utils/testing';
import TelemetryAPI from './TelemetryAPI'; import TelemetryAPI from './TelemetryAPI';
import TelemetryCollection from './TelemetryCollection'; const { TelemetryCollection } = require("./TelemetryCollection");
describe('Telemetry API', function () { describe('Telemetry API', function () {
let openmct; let openmct;

View File

@@ -26,7 +26,7 @@ import { LOADED_ERROR, TIMESYSTEM_KEY_NOTIFICATION, TIMESYSTEM_KEY_WARNING } fro
/** Class representing a Telemetry Collection. */ /** Class representing a Telemetry Collection. */
export default class TelemetryCollection extends EventEmitter { export class TelemetryCollection extends EventEmitter {
/** /**
* Creates a Telemetry Collection * Creates a Telemetry Collection
* *
@@ -49,7 +49,6 @@ export default class TelemetryCollection extends EventEmitter {
this.pageState = undefined; this.pageState = undefined;
this.lastBounds = undefined; this.lastBounds = undefined;
this.requestAbort = undefined; this.requestAbort = undefined;
this.isStrategyLatest = this.options.strategy === 'latest';
} }
/** /**
@@ -127,8 +126,7 @@ export default class TelemetryCollection extends EventEmitter {
this.requestAbort = new AbortController(); this.requestAbort = new AbortController();
options.signal = this.requestAbort.signal; options.signal = this.requestAbort.signal;
this.emit('requestStarted'); this.emit('requestStarted');
const modifiedOptions = await this.openmct.telemetry.applyRequestInterceptors(this.domainObject, options); historicalData = await historicalProvider.request(this.domainObject, options);
historicalData = await historicalProvider.request(this.domainObject, modifiedOptions);
} catch (error) { } catch (error) {
if (error.name !== 'AbortError') { if (error.name !== 'AbortError') {
console.error('Error requesting telemetry data...'); console.error('Error requesting telemetry data...');
@@ -170,18 +168,17 @@ export default class TelemetryCollection extends EventEmitter {
* @private * @private
*/ */
_processNewTelemetry(telemetryData) { _processNewTelemetry(telemetryData) {
performance.mark('tlm:process:start');
if (telemetryData === undefined) { if (telemetryData === undefined) {
return; return;
} }
let latestBoundedDatum = this.boundedTelemetry[this.boundedTelemetry.length - 1];
let data = Array.isArray(telemetryData) ? telemetryData : [telemetryData]; let data = Array.isArray(telemetryData) ? telemetryData : [telemetryData];
let parsedValue; let parsedValue;
let beforeStartOfBounds; let beforeStartOfBounds;
let afterEndOfBounds; let afterEndOfBounds;
let added = []; let added = [];
// loop through, sort and dedupe
for (let datum of data) { for (let datum of data) {
parsedValue = this.parseTime(datum); parsedValue = this.parseTime(datum);
beforeStartOfBounds = parsedValue < this.lastBounds.start; beforeStartOfBounds = parsedValue < this.lastBounds.start;
@@ -221,19 +218,9 @@ export default class TelemetryCollection extends EventEmitter {
} }
if (added.length) { if (added.length) {
// if latest strategy is requested, we need to check if the value is the latest unmitted value
if (this.isStrategyLatest) {
this.boundedTelemetry = [this.boundedTelemetry[this.boundedTelemetry.length - 1]];
// if true, then this value has yet to be emitted
if (this.boundedTelemetry[0] !== latestBoundedDatum) {
this.emit('add', this.boundedTelemetry);
}
} else {
this.emit('add', added); this.emit('add', added);
} }
} }
}
/** /**
* Finds the correct insertion point for the given telemetry datum. * Finds the correct insertion point for the given telemetry datum.
@@ -291,9 +278,6 @@ export default class TelemetryCollection extends EventEmitter {
if (startChanged) { if (startChanged) {
testDatum[this.timeKey] = bounds.start; testDatum[this.timeKey] = bounds.start;
// a little more complicated if not latest strategy
if (!this.isStrategyLatest) {
// Calculate the new index of the first item within the bounds // Calculate the new index of the first item within the bounds
startIndex = _.sortedIndexBy( startIndex = _.sortedIndexBy(
this.boundedTelemetry, this.boundedTelemetry,
@@ -301,10 +285,6 @@ export default class TelemetryCollection extends EventEmitter {
datum => this.parseTime(datum) datum => this.parseTime(datum)
); );
discarded = this.boundedTelemetry.splice(0, startIndex); discarded = this.boundedTelemetry.splice(0, startIndex);
} else if (this.parseTime(testDatum) > this.parseTime(this.boundedTelemetry[0])) {
discarded = this.boundedTelemetry;
this.boundedTelemetry = [];
}
} }
if (endChanged) { if (endChanged) {
@@ -316,6 +296,7 @@ export default class TelemetryCollection extends EventEmitter {
datum => this.parseTime(datum) datum => this.parseTime(datum)
); );
added = this.futureBuffer.splice(0, endIndex); added = this.futureBuffer.splice(0, endIndex);
this.boundedTelemetry = [...this.boundedTelemetry, ...added];
} }
if (discarded.length > 0) { if (discarded.length > 0) {
@@ -323,13 +304,6 @@ export default class TelemetryCollection extends EventEmitter {
} }
if (added.length > 0) { if (added.length > 0) {
if (!this.isStrategyLatest) {
this.boundedTelemetry = [...this.boundedTelemetry, ...added];
} else {
added = [added[added.length - 1]];
this.boundedTelemetry = added;
}
this.emit('add', added); this.emit('add', added);
} }
} else { } else {
@@ -348,14 +322,7 @@ export default class TelemetryCollection extends EventEmitter {
* @private * @private
*/ */
_setTimeSystem(timeSystem) { _setTimeSystem(timeSystem) {
let domains = []; let domains = this.metadata.valuesForHints(['domain']);
let metadataValue = { format: timeSystem.key };
if (this.metadata) {
domains = this.metadata.valuesForHints(['domain']);
metadataValue = this.metadata.value(timeSystem.key) || { format: timeSystem.key };
}
let domain = domains.find((d) => d.key === timeSystem.key); let domain = domains.find((d) => d.key === timeSystem.key);
if (domain !== undefined) { if (domain !== undefined) {
@@ -368,6 +335,7 @@ export default class TelemetryCollection extends EventEmitter {
this.openmct.notifications.alert(TIMESYSTEM_KEY_NOTIFICATION); this.openmct.notifications.alert(TIMESYSTEM_KEY_NOTIFICATION);
} }
let metadataValue = this.metadata.value(timeSystem.key) || { format: timeSystem.key };
let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue); let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
this.parseTime = (datum) => { this.parseTime = (datum) => {
@@ -388,6 +356,7 @@ export default class TelemetryCollection extends EventEmitter {
* @todo handle subscriptions more granually * @todo handle subscriptions more granually
*/ */
_reset() { _reset() {
performance.mark('tlm:reset');
this.boundedTelemetry = []; this.boundedTelemetry = [];
this.futureBuffer = []; this.futureBuffer = [];

View File

@@ -121,18 +121,6 @@ define([
return _.sortBy(matchingMetadata, ...iteratees); return _.sortBy(matchingMetadata, ...iteratees);
}; };
/**
* check out of a given metadata has array values
*/
TelemetryMetadataManager.prototype.isArrayValue = function (metadata) {
const regex = /\[\]$/g;
if (!metadata.format && !metadata.formatString) {
return false;
}
return (metadata.format || metadata.formatString).match(regex) !== null;
};
TelemetryMetadataManager.prototype.getFilterableValues = function () { TelemetryMetadataManager.prototype.getFilterableValues = function () {
return this.valueMetadatas.filter(metadatum => metadatum.filters && metadatum.filters.length > 0); return this.valueMetadatas.filter(metadatum => metadatum.filters && metadatum.filters.length > 0);
}; };
@@ -150,7 +138,7 @@ define([
valueMetadata = this.values()[0]; valueMetadata = this.values()[0];
} }
return valueMetadata; return valueMetadata.key;
}; };
return TelemetryMetadataManager; return TelemetryMetadataManager;

View File

@@ -1,68 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
export default class TelemetryRequestInterceptorRegistry {
/**
* A TelemetryRequestInterceptorRegistry maintains the definitions for different interceptors that may be invoked on telemetry
* requests.
* @interface TelemetryRequestInterceptorRegistry
* @memberof module:openmct
*/
constructor() {
this.interceptors = [];
}
/**
* @interface TelemetryRequestInterceptorDef
* @property {function} appliesTo function that determines if this interceptor should be called for the given identifier/request
* @property {function} invoke function that transforms the provided request and returns the transformed request
* @property {function} priority the priority for this interceptor. A higher number returned has more weight than a lower number
* @memberof module:openmct TelemetryRequestInterceptorRegistry#
*/
/**
* Register a new telemetry request interceptor.
*
* @param {module:openmct.RequestInterceptorDef} requestInterceptorDef the interceptor to add
* @method addInterceptor
* @memberof module:openmct.TelemetryRequestInterceptorRegistry#
*/
addInterceptor(interceptorDef) {
//TODO: sort by priority
this.interceptors.push(interceptorDef);
}
/**
* Retrieve all interceptors applicable to a domain object/request.
* @method getInterceptors
* @returns [module:openmct.RequestInterceptorDef] the registered interceptors for this identifier/request
* @memberof module:openmct.TelemetryRequestInterceptorRegistry#
*/
getInterceptors(identifier, request) {
return this.interceptors.filter(interceptor => {
return typeof interceptor.appliesTo === 'function'
&& interceptor.appliesTo(identifier, request);
});
}
}

View File

@@ -43,23 +43,9 @@ define([
}; };
this.valueMetadata = valueMetadata; this.valueMetadata = valueMetadata;
this.formatter = formatMap.get(valueMetadata.format) || numberFormatter;
function getNonArrayValue(value) { if (valueMetadata.format === 'enum') {
//metadata format could have array formats ex. string[]/number[]
const arrayRegex = /\[\]$/g;
if (value && value.match(arrayRegex)) {
return value.replace(arrayRegex, '');
}
return value;
}
let valueMetadataFormat = getNonArrayValue(valueMetadata.format);
//Is there an existing formatter for the format specified? If not, default to number format
this.formatter = formatMap.get(valueMetadataFormat) || numberFormatter;
if (valueMetadataFormat === 'enum') {
this.formatter = {}; this.formatter = {};
this.enumerations = valueMetadata.enumerations.reduce(function (vm, e) { this.enumerations = valueMetadata.enumerations.reduce(function (vm, e) {
vm.byValue[e.value] = e.string; vm.byValue[e.value] = e.string;
@@ -91,13 +77,13 @@ define([
// Check for formatString support once instead of per format call. // Check for formatString support once instead of per format call.
if (valueMetadata.formatString) { if (valueMetadata.formatString) {
const baseFormat = this.formatter.format; const baseFormat = this.formatter.format;
const formatString = getNonArrayValue(valueMetadata.formatString); const formatString = valueMetadata.formatString;
this.formatter.format = function (value) { this.formatter.format = function (value) {
return printj.sprintf(formatString, baseFormat.call(this, value)); return printj.sprintf(formatString, baseFormat.call(this, value));
}; };
} }
if (valueMetadataFormat === 'string') { if (valueMetadata.format === 'string') {
this.formatter.parse = function (value) { this.formatter.parse = function (value) {
if (value === undefined) { if (value === undefined) {
return ''; return '';
@@ -122,14 +108,7 @@ define([
TelemetryValueFormatter.prototype.parse = function (datum) { TelemetryValueFormatter.prototype.parse = function (datum) {
if (_.isObject(datum)) { if (_.isObject(datum)) {
const objectDatum = datum[this.valueMetadata.source]; return this.formatter.parse(datum[this.valueMetadata.source]);
if (Array.isArray(objectDatum)) {
return objectDatum.map((item) => {
return this.formatter.parse(item);
});
} else {
return this.formatter.parse(objectDatum);
}
} }
return this.formatter.parse(datum); return this.formatter.parse(datum);
@@ -137,14 +116,7 @@ define([
TelemetryValueFormatter.prototype.format = function (datum) { TelemetryValueFormatter.prototype.format = function (datum) {
if (_.isObject(datum)) { if (_.isObject(datum)) {
const objectDatum = datum[this.valueMetadata.source]; return this.formatter.format(datum[this.valueMetadata.source]);
if (Array.isArray(objectDatum)) {
return objectDatum.map((item) => {
return this.formatter.format(item);
});
} else {
return this.formatter.format(objectDatum);
}
} }
return this.formatter.format(datum); return this.formatter.format(datum);

View File

@@ -1,295 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import EventEmitter from "EventEmitter";
export default class StatusAPI extends EventEmitter {
#userAPI;
#openmct;
constructor(userAPI, openmct) {
super();
this.#userAPI = userAPI;
this.#openmct = openmct;
this.onProviderStatusChange = this.onProviderStatusChange.bind(this);
this.onProviderPollQuestionChange = this.onProviderPollQuestionChange.bind(this);
this.listenToStatusEvents = this.listenToStatusEvents.bind(this);
this.#openmct.once('destroy', () => {
const provider = this.#userAPI.getProvider();
if (typeof provider?.off === 'function') {
provider.off('statusChange', this.onProviderStatusChange);
provider.off('pollQuestionChange', this.onProviderPollQuestionChange);
}
});
this.#userAPI.on('providerAdded', this.listenToStatusEvents);
}
/**
* Fetch the currently defined operator status poll question. When presented with a status poll question, all operators will reply with their current status.
* @returns {Promise<PollQuestion>}
*/
getPollQuestion() {
const provider = this.#userAPI.getProvider();
if (provider.getPollQuestion) {
return provider.getPollQuestion();
} else {
this.#userAPI.error("User provider does not support polling questions");
}
}
/**
* Set a poll question for operators to respond to. When presented with a status poll question, all operators will reply with their current status.
* @param {String} questionText - The text of the question
* @returns {Promise<Boolean>} true if operation was successful, otherwise false.
*/
async setPollQuestion(questionText) {
const canSetPollQuestion = await this.canSetPollQuestion();
if (canSetPollQuestion) {
const provider = this.#userAPI.getProvider();
const result = await provider.setPollQuestion(questionText);
try {
await this.resetAllStatuses();
} catch (error) {
console.warn("Poll question set but unable to clear operator statuses.");
console.error(error);
}
return result;
} else {
this.#userAPI.error("User provider does not support setting polling question");
}
}
/**
* Can the currently logged in user set the operator status poll question.
* @returns {Promise<Boolean>}
*/
canSetPollQuestion() {
const provider = this.#userAPI.getProvider();
if (provider.canSetPollQuestion) {
return provider.canSetPollQuestion();
} else {
return Promise.resolve(false);
}
}
/**
* @returns {Promise<Array<Status>>} the complete list of possible states that an operator can reply to a poll question with.
*/
async getPossibleStatuses() {
const provider = this.#userAPI.getProvider();
if (provider.getPossibleStatuses) {
const possibleStatuses = await provider.getPossibleStatuses() || [];
return possibleStatuses.map(status => status);
} else {
this.#userAPI.error("User provider cannot provide statuses");
}
}
/**
* @param {import("./UserAPI").Role} role The role to fetch the current status for.
* @returns {Promise<Status>} the current status of the provided role
*/
async getStatusForRole(role) {
const provider = this.#userAPI.getProvider();
if (provider.getStatusForRole) {
const status = await provider.getStatusForRole(role);
return status;
} else {
this.#userAPI.error("User provider does not support role status");
}
}
/**
* @param {import("./UserAPI").Role} role
* @returns {Promise<Boolean>} true if the configured UserProvider can provide status for the given role
* @see StatusUserProvider
*/
canProvideStatusForRole(role) {
const provider = this.#userAPI.getProvider();
if (provider.canProvideStatusForRole) {
return provider.canProvideStatusForRole(role);
} else {
return false;
}
}
/**
* @param {import("./UserAPI").Role} role The role to set the status for.
* @param {Status} status The status to set for the provided role
* @returns {Promise<Boolean>} true if operation was successful, otherwise false.
*/
setStatusForRole(role, status) {
const provider = this.#userAPI.getProvider();
if (provider.setStatusForRole) {
return provider.setStatusForRole(role, status);
} else {
this.#userAPI.error("User provider does not support setting role status");
}
}
/**
* Resets the status of the provided role back to its default status.
* @param {import("./UserAPI").Role} role The role to set the status for.
* @returns {Promise<Boolean>} true if operation was successful, otherwise false.
*/
async resetStatusForRole(role) {
const provider = this.#userAPI.getProvider();
const defaultStatus = await this.getDefaultStatusForRole(role);
if (provider.setStatusForRole) {
return provider.setStatusForRole(role, defaultStatus);
} else {
this.#userAPI.error("User provider does not support resetting role status");
}
}
/**
* Resets the status of all operators to their default status
* @returns {Promise<Boolean>} true if operation was successful, otherwise false.
*/
async resetAllStatuses() {
const allStatusRoles = await this.getAllStatusRoles();
return Promise.all(allStatusRoles.map(role => this.resetStatusForRole(role)));
}
/**
* The default status. This is the status that will be used before the user has selected any status.
* @param {import("./UserAPI").Role} role
* @returns {Promise<Status>} the default operator status if no other has been set.
*/
async getDefaultStatusForRole(role) {
const provider = this.#userAPI.getProvider();
const defaultStatus = await provider.getDefaultStatusForRole(role);
return defaultStatus;
}
/**
* All possible status roles. A status role is a user role that can provide status. In some systems
* this may be all user roles, but there may be cases where some users are not are not polled
* for status if they do not have a real-time operational role.
*
* @returns {Promise<Array<import("./UserAPI").Role>>} the default operator status if no other has been set.
*/
getAllStatusRoles() {
const provider = this.#userAPI.getProvider();
if (provider.getAllStatusRoles) {
return provider.getAllStatusRoles();
} else {
this.#userAPI.error("User provider cannot provide all status roles");
}
}
/**
* The status role of the current user. A user may have multiple roles, but will only have one role
* that provides status at any time.
* @returns {Promise<import("./UserAPI").Role>} the role for which the current user can provide status.
*/
getStatusRoleForCurrentUser() {
const provider = this.#userAPI.getProvider();
if (provider.getStatusRoleForCurrentUser) {
return provider.getStatusRoleForCurrentUser();
} else {
this.#userAPI.error("User provider cannot provide role status for this user");
}
}
/**
* @returns {Promise<Boolean>} true if the configured UserProvider can provide status for the currently logged in user, false otherwise.
* @see StatusUserProvider
*/
async canProvideStatusForCurrentUser() {
const provider = this.#userAPI.getProvider();
if (provider.getStatusRoleForCurrentUser) {
const activeStatusRole = await this.#userAPI.getProvider().getStatusRoleForCurrentUser();
const canProvideStatus = await this.canProvideStatusForRole(activeStatusRole);
return canProvideStatus;
} else {
return false;
}
}
/**
* Private internal function that cannot be made #private because it needs to be registered as a callback to the user provider
* @private
*/
listenToStatusEvents(provider) {
if (typeof provider.on === 'function') {
provider.on('statusChange', this.onProviderStatusChange);
provider.on('pollQuestionChange', this.onProviderPollQuestionChange);
}
}
/**
* @private
*/
onProviderStatusChange(newStatus) {
this.emit('statusChange', newStatus);
}
/**
* @private
*/
onProviderPollQuestionChange(pollQuestion) {
this.emit('pollQuestionChange', pollQuestion);
}
}
/**
* @typedef {import('./UserProvider')} UserProvider
*/
/**
* @typedef {import('./StatusUserProvider')} StatusUserProvider
*/
/**
* The PollQuestion type
* @typedef {Object} PollQuestion
* @property {String} question - The question to be presented to users
* @property {Number} timestamp - The time that the poll question was set.
*/
/**
* The Status type
* @typedef {Object} Status
* @property {String} key - A unique identifier for this status
* @property {Number} label - A human readable label for this status
*/

View File

@@ -1,81 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import UserProvider from "./UserProvider";
export default class StatusUserProvider extends UserProvider {
/**
* @param {('statusChange'|'pollQuestionChange')} event the name of the event to listen to
* @param {Function} callback a function to invoke when this event occurs
*/
on(event, callback) {}
/**
* @param {('statusChange'|'pollQuestionChange')} event the name of the event to stop listen to
* @param {Function} callback the callback function used to register the listener
*/
off(event, callback) {}
/**
* @returns {import("./StatusAPI").PollQuestion} the current status poll question
*/
async getPollQuestion() {}
/**
* @param {import("./StatusAPI").PollQuestion} pollQuestion a new poll question to set
* @returns {Promise<Boolean>} true if operation was successful, otherwise false
*/
async setPollQuestion(pollQuestion) {}
/**
* @returns {Promise<Boolean>} true if the current user can set the poll question, otherwise false
*/
async canSetPollQuestion() {}
/**
* @returns {Promise<Array<import("./StatusAPI").Status>>} a list of the possible statuses that an operator can be in
*/
async getPossibleStatuses() {}
/**
* @param {import("./UserAPI").Role} role
* @returns {Promise<import("./StatusAPI").Status}
*/
async getStatusForRole(role) {}
/**
* @param {import("./UserAPI").Role} role
* @returns {Promise<import("./StatusAPI").Status}
*/
async getDefaultStatusForRole(role) {}
/**
* @param {import("./UserAPI").Role} role
* @param {*} status
* @returns {Promise<Boolean>} true if operation was successful, otherwise false.
*/
async setStatusForRole(role, status) {}
/**
* @param {import("./UserAPI").Role} role
* @returns {Promise<Boolean} true if the user provider can provide status for the given role
*/
async canProvideStatusForRole(role) {}
/**
* @returns {Promise<Array<import("./UserAPI").Role>>} a list of all available status roles, if user permissions allow it.
*/
async getAllStatusRoles() {}
/**
* @returns {Promise<import("./UserAPI").Role>} the active status role for the currently logged in user
*/
async getStatusRoleForCurrentUser() {}
}

View File

@@ -25,22 +25,16 @@ import {
MULTIPLE_PROVIDER_ERROR, MULTIPLE_PROVIDER_ERROR,
NO_PROVIDER_ERROR NO_PROVIDER_ERROR
} from './constants'; } from './constants';
import StatusAPI from './StatusAPI';
import User from './User'; import User from './User';
class UserAPI extends EventEmitter { class UserAPI extends EventEmitter {
/** constructor(openmct) {
* @param {OpenMCT} openmct
* @param {UserAPIConfiguration} config
*/
constructor(openmct, config) {
super(); super();
this._openmct = openmct; this._openmct = openmct;
this._provider = undefined; this._provider = undefined;
this.User = User; this.User = User;
this.status = new StatusAPI(this, openmct, config);
} }
/** /**
@@ -53,15 +47,12 @@ class UserAPI extends EventEmitter {
*/ */
setProvider(provider) { setProvider(provider) {
if (this.hasProvider()) { if (this.hasProvider()) {
this.error(MULTIPLE_PROVIDER_ERROR); this._error(MULTIPLE_PROVIDER_ERROR);
} }
this._provider = provider; this._provider = provider;
this.emit('providerAdded', this._provider);
}
getProvider() { this.emit('providerAdded', this._provider);
return this._provider;
} }
/** /**
@@ -83,7 +74,7 @@ class UserAPI extends EventEmitter {
* @throws Will throw an error if no user provider is set * @throws Will throw an error if no user provider is set
*/ */
getCurrentUser() { getCurrentUser() {
this.noProviderCheck(); this._noProviderCheck();
return this._provider.getCurrentUser(); return this._provider.getCurrentUser();
} }
@@ -114,7 +105,7 @@ class UserAPI extends EventEmitter {
* @throws Will throw an error if no user provider is set * @throws Will throw an error if no user provider is set
*/ */
hasRole(roleId) { hasRole(roleId) {
this.noProviderCheck(); this._noProviderCheck();
return this._provider.hasRole(roleId); return this._provider.hasRole(roleId);
} }
@@ -125,9 +116,9 @@ class UserAPI extends EventEmitter {
* @private * @private
* @throws Will throw an error if no user provider is set * @throws Will throw an error if no user provider is set
*/ */
noProviderCheck() { _noProviderCheck() {
if (!this.hasProvider()) { if (!this.hasProvider()) {
this.error(NO_PROVIDER_ERROR); this._error(NO_PROVIDER_ERROR);
} }
} }
@@ -138,26 +129,9 @@ class UserAPI extends EventEmitter {
* @param {string} error description of error * @param {string} error description of error
* @throws Will throw error passed in * @throws Will throw error passed in
*/ */
error(error) { _error(error) {
throw new Error(error); throw new Error(error);
} }
} }
export default UserAPI; export default UserAPI;
/**
* @typedef {String} Role
*/
/**
* @typedef {Object} OpenMCT
*/
/**
* @typedef {{statusStyles: Object.<string, StatusStyleDefinition>}} UserAPIConfiguration
*/
/**
* @typedef {Object} StatusStyleDefinition
* @property {String} iconClass The icon class to apply to the status indicator when this status is active "icon-circle-slash",
* @property {String} iconClassPoll The icon class to apply to the poll question indicator when this style is active eg. "icon-status-poll-question-mark"
* @property {String} statusClass The class to apply to the indicator when this status is active eg. "s-status-error"
* @property {String} statusBgColor The background color to apply in the status summary section of the poll question popup for this status eg."#9900cc"
* @property {String} statusFgColor The foreground color to apply in the status summary section of the poll question popup for this status eg. "#fff"
*/

View File

@@ -1,36 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
export default class UserProvider {
/**
* @returns {Promise<User>} A promise that resolves with the currently logged in user
*/
getCurrentUser() {}
/**
* @returns {Boolean} true if a user is currently logged in, otherwise false
*/
isLoggedIn() {}
/**
* @param {String} role
* @returns {Promise<Boolean>} true if the current user has the given role
*/
hasRole(role) {}
}

View File

@@ -1,103 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import {
createOpenMct,
resetApplicationState
} from '../../utils/testing';
describe("The User Status API", () => {
let openmct;
let userProvider;
let mockUser;
beforeEach(() => {
userProvider = jasmine.createSpyObj("userProvider", [
"setPollQuestion",
"getPollQuestion",
"getCurrentUser",
"getPossibleStatuses",
"getAllStatusRoles",
"canSetPollQuestion",
"isLoggedIn",
"on"
]);
openmct = createOpenMct();
mockUser = new openmct.user.User("test-user", "A test user");
userProvider.getCurrentUser.and.returnValue(Promise.resolve(mockUser));
userProvider.getPossibleStatuses.and.returnValue(Promise.resolve([]));
userProvider.getAllStatusRoles.and.returnValue(Promise.resolve([]));
userProvider.canSetPollQuestion.and.returnValue(Promise.resolve(false));
userProvider.isLoggedIn.and.returnValue(true);
});
afterEach(() => {
return resetApplicationState(openmct);
});
describe("the poll question", () => {
it('can be set via a user status provider if supported', () => {
openmct.user.setProvider(userProvider);
userProvider.canSetPollQuestion.and.returnValue(Promise.resolve(true));
return openmct.user.status.setPollQuestion('This is a poll question').then(() => {
expect(userProvider.setPollQuestion).toHaveBeenCalledWith('This is a poll question');
});
});
// fit('emits an event when the poll question changes', () => {
// const pollQuestionChangeCallback = jasmine.createSpy('pollQuestionChangeCallback');
// let pollQuestionListener;
// userProvider.canSetPollQuestion.and.returnValue(Promise.resolve(true));
// userProvider.on.and.callFake((eventName, listener) => {
// if (eventName === 'pollQuestionChange') {
// pollQuestionListener = listener;
// }
// });
// openmct.user.on('pollQuestionChange', pollQuestionChangeCallback);
// openmct.user.setProvider(userProvider);
// return openmct.user.status.setPollQuestion('This is a poll question').then(() => {
// expect(pollQuestionListener).toBeDefined();
// pollQuestionListener();
// expect(pollQuestionChangeCallback).toHaveBeenCalled();
// const pollQuestion = pollQuestionChangeCallback.calls.mostRecent().args[0];
// expect(pollQuestion.question).toBe('This is a poll question');
// openmct.user.off('pollQuestionChange', pollQuestionChangeCallback);
// });
// });
it('cannot be set if the user is not permitted', () => {
openmct.user.setProvider(userProvider);
userProvider.canSetPollQuestion.and.returnValue(Promise.resolve(false));
return openmct.user.status.setPollQuestion('This is a poll question').catch((error) => {
expect(error).toBeInstanceOf(Error);
}).finally(() => {
expect(userProvider.setPollQuestion).not.toHaveBeenCalled();
});
});
});
});

View File

@@ -197,7 +197,7 @@ export default {
} }
}, },
setUnit() { setUnit() {
this.unit = this.valueMetadata ? this.valueMetadata.unit : ''; this.unit = this.valueMetadata.unit || '';
}, },
firstNonDomainAttribute(metadata) { firstNonDomainAttribute(metadata) {
return metadata return metadata

View File

@@ -83,8 +83,6 @@ export default {
for (let ladTable of ladTables) { for (let ladTable of ladTables) {
for (let telemetryObject of ladTable) { for (let telemetryObject of ladTable) {
let metadata = this.openmct.telemetry.getMetadata(telemetryObject.domainObject); let metadata = this.openmct.telemetry.getMetadata(telemetryObject.domainObject);
if (metadata) {
for (let metadatum of metadata.valueMetadatas) { for (let metadatum of metadata.valueMetadatas) {
if (metadatum.unit) { if (metadatum.unit) {
return true; return true;
@@ -92,7 +90,6 @@ export default {
} }
} }
} }
}
return false; return false;
} }

View File

@@ -20,8 +20,10 @@
* at runtime from the About dialog for additional information. * at runtime from the About dialog for additional information.
*****************************************************************************/ *****************************************************************************/
define([], define(
function () { ['zepto'],
function ($) {
// Set of connection states; changing among these states will be // Set of connection states; changing among these states will be
// reflected in the indicator's appearance. // reflected in the indicator's appearance.
// CONNECTED: Everything nominal, expect to be able to read/write. // CONNECTED: Everything nominal, expect to be able to read/write.
@@ -73,16 +75,11 @@ define([],
}; };
URLIndicator.prototype.fetchUrl = function () { URLIndicator.prototype.fetchUrl = function () {
fetch(this.URLpath) $.ajax({
.then(response => { type: 'GET',
if (response.ok) { url: this.URLpath,
this.handleSuccess(); success: this.handleSuccess,
} else { error: this.handleError
this.handleError();
}
})
.catch(error => {
this.handleError();
}); });
}; };

View File

@@ -25,35 +25,37 @@ define(
"utils/testing", "utils/testing",
"./URLIndicator", "./URLIndicator",
"./URLIndicatorPlugin", "./URLIndicatorPlugin",
"../../MCT" "../../MCT",
"zepto"
], ],
function ( function (
testingUtils, testingUtils,
URLIndicator, URLIndicator,
URLIndicatorPlugin, URLIndicatorPlugin,
MCT MCT,
$
) { ) {
const defaultAjaxFunction = $.ajax;
describe("The URLIndicator", function () { describe("The URLIndicator", function () {
let openmct; let openmct;
let indicatorElement; let indicatorElement;
let pluginOptions; let pluginOptions;
let ajaxOptions;
let urlIndicator; // eslint-disable-line let urlIndicator; // eslint-disable-line
let fetchSpy;
beforeEach(function () { beforeEach(function () {
jasmine.clock().install(); jasmine.clock().install();
openmct = new testingUtils.createOpenMct(); openmct = new testingUtils.createOpenMct();
spyOn(openmct.indicators, 'add'); spyOn(openmct.indicators, 'add');
fetchSpy = spyOn(window, 'fetch').and.callFake(() => Promise.resolve({ spyOn($, 'ajax');
ok: true $.ajax.and.callFake(function (options) {
})); ajaxOptions = options;
});
}); });
afterEach(function () { afterEach(function () {
if (window.fetch.restore) { $.ajax = defaultAjaxFunction;
window.fetch.restore();
}
jasmine.clock().uninstall(); jasmine.clock().uninstall();
return testingUtils.resetApplicationState(openmct); return testingUtils.resetApplicationState(openmct);
@@ -94,11 +96,11 @@ define(
expect(indicatorElement.classList.contains('iconClass-checked')).toBe(true); expect(indicatorElement.classList.contains('iconClass-checked')).toBe(true);
}); });
it("uses custom interval", function () { it("uses custom interval", function () {
expect(window.fetch).toHaveBeenCalledTimes(1); expect($.ajax.calls.count()).toEqual(1);
jasmine.clock().tick(1); jasmine.clock().tick(1);
expect(window.fetch).toHaveBeenCalledTimes(1); expect($.ajax.calls.count()).toEqual(1);
jasmine.clock().tick(pluginOptions.interval + 1); jasmine.clock().tick(pluginOptions.interval + 1);
expect(window.fetch).toHaveBeenCalledTimes(2); expect($.ajax.calls.count()).toEqual(2);
}); });
it("uses custom label if supplied in initialization", function () { it("uses custom label if supplied in initialization", function () {
expect(indicatorElement.textContent.indexOf(pluginOptions.label) >= 0).toBe(true); expect(indicatorElement.textContent.indexOf(pluginOptions.label) >= 0).toBe(true);
@@ -118,21 +120,18 @@ define(
it("requests the provided URL", function () { it("requests the provided URL", function () {
jasmine.clock().tick(pluginOptions.interval + 1); jasmine.clock().tick(pluginOptions.interval + 1);
expect(window.fetch).toHaveBeenCalledWith(pluginOptions.url); expect(ajaxOptions.url).toEqual(pluginOptions.url);
}); });
it("indicates success if connection is nominal", async function () { it("indicates success if connection is nominal", function () {
jasmine.clock().tick(pluginOptions.interval + 1); jasmine.clock().tick(pluginOptions.interval + 1);
await urlIndicator.fetchUrl(); ajaxOptions.success();
expect(indicatorElement.classList.contains('s-status-on')).toBe(true); expect(indicatorElement.classList.contains('s-status-on')).toBe(true);
}); });
it("indicates an error when the server cannot be reached", async function () { it("indicates an error when the server cannot be reached", function () {
fetchSpy.and.callFake(() => Promise.resolve({
ok: false
}));
jasmine.clock().tick(pluginOptions.interval + 1); jasmine.clock().tick(pluginOptions.interval + 1);
await urlIndicator.fetchUrl(); ajaxOptions.error();
expect(indicatorElement.classList.contains('s-status-warning-hi')).toBe(true); expect(indicatorElement.classList.contains('s-status-warning-hi')).toBe(true);
}); });
}); });

View File

@@ -21,6 +21,7 @@
*****************************************************************************/ *****************************************************************************/
import AutoflowTabularPlugin from './AutoflowTabularPlugin'; import AutoflowTabularPlugin from './AutoflowTabularPlugin';
import AutoflowTabularConstants from './AutoflowTabularConstants'; import AutoflowTabularConstants from './AutoflowTabularConstants';
import $ from 'zepto';
import DOMObserver from './dom-observer'; import DOMObserver from './dom-observer';
import { import {
createOpenMct, createOpenMct,
@@ -121,7 +122,7 @@ xdescribe("AutoflowTabularPlugin", () => {
name: "Object " + key name: "Object " + key
}; };
}); });
testContainer = document.createElement('div'); testContainer = $('<div>')[0];
domObserver = new DOMObserver(testContainer); domObserver = new DOMObserver(testContainer);
testHistories = testKeys.reduce((histories, key, index) => { testHistories = testKeys.reduce((histories, key, index) => {
@@ -194,7 +195,7 @@ xdescribe("AutoflowTabularPlugin", () => {
describe("when rows have been populated", () => { describe("when rows have been populated", () => {
function rowsMatch() { function rowsMatch() {
const rows = testContainer.querySelectorAll(".l-autoflow-row").length; const rows = $(testContainer).find(".l-autoflow-row").length;
return rows === testChildren.length; return rows === testChildren.length;
} }
@@ -240,20 +241,20 @@ xdescribe("AutoflowTabularPlugin", () => {
const nextWidth = const nextWidth =
initialWidth + AutoflowTabularConstants.COLUMN_WIDTH_STEP; initialWidth + AutoflowTabularConstants.COLUMN_WIDTH_STEP;
expect(testContainer.querySelector('.l-autoflow-col').css('width')) expect($(testContainer).find('.l-autoflow-col').css('width'))
.toEqual(initialWidth + 'px'); .toEqual(initialWidth + 'px');
testContainer.querySelector('.change-column-width').click(); $(testContainer).find('.change-column-width').click();
function widthHasChanged() { function widthHasChanged() {
const width = testContainer.querySelector('.l-autoflow-col').css('width'); const width = $(testContainer).find('.l-autoflow-col').css('width');
return width !== initialWidth + 'px'; return width !== initialWidth + 'px';
} }
return domObserver.when(widthHasChanged) return domObserver.when(widthHasChanged)
.then(() => { .then(() => {
expect(testContainer.querySelector('.l-autoflow-col').css('width')) expect($(testContainer).find('.l-autoflow-col').css('width'))
.toEqual(nextWidth + 'px'); .toEqual(nextWidth + 'px');
}); });
}); });
@@ -266,13 +267,13 @@ xdescribe("AutoflowTabularPlugin", () => {
it("displays historical telemetry", () => { it("displays historical telemetry", () => {
function rowTextDefined() { function rowTextDefined() {
return testContainer.querySelector(".l-autoflow-item").filter(".r").text() !== ""; return $(testContainer).find(".l-autoflow-item").filter(".r").text() !== "";
} }
return domObserver.when(rowTextDefined).then(() => { return domObserver.when(rowTextDefined).then(() => {
testKeys.forEach((key, index) => { testKeys.forEach((key, index) => {
const datum = testHistories[key]; const datum = testHistories[key];
const $cell = testContainer.querySelector(".l-autoflow-row").eq(index).find(".r"); const $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r");
expect($cell.text()).toEqual(String(datum.range)); expect($cell.text()).toEqual(String(datum.range));
}); });
}); });
@@ -293,7 +294,7 @@ xdescribe("AutoflowTabularPlugin", () => {
return waitsForChange().then(() => { return waitsForChange().then(() => {
testData.forEach((datum, index) => { testData.forEach((datum, index) => {
const $cell = testContainer.querySelector(".l-autoflow-row").eq(index).find(".r"); const $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r");
expect($cell.text()).toEqual(String(datum.range)); expect($cell.text()).toEqual(String(datum.range));
}); });
}); });
@@ -311,7 +312,7 @@ xdescribe("AutoflowTabularPlugin", () => {
return waitsForChange().then(() => { return waitsForChange().then(() => {
testKeys.forEach((datum, index) => { testKeys.forEach((datum, index) => {
const $cell = testContainer.querySelector(".l-autoflow-row").eq(index).find(".r"); const $cell = $(testContainer).find(".l-autoflow-row").eq(index).find(".r");
expect($cell.hasClass(testClass)).toBe(true); expect($cell.hasClass(testClass)).toBe(true);
}); });
}); });
@@ -321,16 +322,16 @@ xdescribe("AutoflowTabularPlugin", () => {
const rowHeight = AutoflowTabularConstants.ROW_HEIGHT; const rowHeight = AutoflowTabularConstants.ROW_HEIGHT;
const sliderHeight = AutoflowTabularConstants.SLIDER_HEIGHT; const sliderHeight = AutoflowTabularConstants.SLIDER_HEIGHT;
const count = testKeys.length; const count = testKeys.length;
const $container = testContainer; const $container = $(testContainer);
let promiseChain = Promise.resolve(); let promiseChain = Promise.resolve();
function columnsHaveAutoflowed() { function columnsHaveAutoflowed() {
const itemsHeight = $container.querySelector('.l-autoflow-items').height(); const itemsHeight = $container.find('.l-autoflow-items').height();
const availableHeight = itemsHeight - sliderHeight; const availableHeight = itemsHeight - sliderHeight;
const availableRows = Math.max(Math.floor(availableHeight / rowHeight), 1); const availableRows = Math.max(Math.floor(availableHeight / rowHeight), 1);
const columns = Math.ceil(count / availableRows); const columns = Math.ceil(count / availableRows);
return $container.querySelector('.l-autoflow-col').length === columns; return $container.find('.l-autoflow-col').length === columns;
} }
$container.find('.abs').css({ $container.find('.abs').css({

View File

@@ -40,6 +40,14 @@ export default {
BarGraph BarGraph
}, },
inject: ['openmct', 'domainObject', 'path'], inject: ['openmct', 'domainObject', 'path'],
props: {
options: {
type: Object,
default() {
return {};
}
}
},
data() { data() {
this.telemetryObjects = {}; this.telemetryObjects = {};
this.telemetryObjectFormats = {}; this.telemetryObjectFormats = {};
@@ -67,9 +75,7 @@ export default {
this.setTimeContext(); this.setTimeContext();
this.loadComposition(); this.loadComposition();
this.unobserveAxes = this.openmct.objects.observe(this.domainObject, 'configuration.axes', this.refreshData);
this.unobserveInterpolation = this.openmct.objects.observe(this.domainObject, 'configuration.useInterpolation', this.refreshData);
this.unobserveBar = this.openmct.objects.observe(this.domainObject, 'configuration.useBar', this.refreshData);
}, },
beforeDestroy() { beforeDestroy() {
this.stopFollowingTimeContext(); this.stopFollowingTimeContext();
@@ -80,19 +86,8 @@ export default {
return; return;
} }
this.composition.off('add', this.addToComposition); this.composition.off('add', this.addTelemetryObject);
this.composition.off('remove', this.removeTelemetryObject); this.composition.off('remove', this.removeTelemetryObject);
if (this.unobserveAxes) {
this.unobserveAxes();
}
if (this.unobserveInterpolation) {
this.unobserveInterpolation();
}
if (this.unobserveBar) {
this.unobserveBar();
}
}, },
methods: { methods: {
setTimeContext() { setTimeContext() {
@@ -110,42 +105,6 @@ export default {
this.timeContext.off('bounds', this.refreshData); this.timeContext.off('bounds', this.refreshData);
} }
}, },
addToComposition(telemetryObject) {
if (Object.values(this.telemetryObjects).length > 0) {
this.confirmRemoval(telemetryObject);
} else {
this.addTelemetryObject(telemetryObject);
}
},
confirmRemoval(telemetryObject) {
const dialog = this.openmct.overlays.dialog({
iconClass: 'alert',
message: 'This action will replace the current telemetry source. Do you want to continue?',
buttons: [
{
label: 'Ok',
emphasis: true,
callback: () => {
const oldTelemetryObject = Object.values(this.telemetryObjects)[0];
this.removeFromComposition(oldTelemetryObject);
this.removeTelemetryObject(oldTelemetryObject.identifier);
this.addTelemetryObject(telemetryObject);
dialog.dismiss();
}
},
{
label: 'Cancel',
callback: () => {
this.removeFromComposition(telemetryObject);
dialog.dismiss();
}
}
]
});
},
removeFromComposition(telemetryObject) {
this.composition.remove(telemetryObject);
},
addTelemetryObject(telemetryObject) { addTelemetryObject(telemetryObject) {
// grab information we need from the added telmetry object // grab information we need from the added telmetry object
const key = this.openmct.objects.makeKeyString(telemetryObject.identifier); const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);
@@ -206,12 +165,7 @@ export default {
const yAxisMetadata = metadata.valuesForHints(['range'])[0]; const yAxisMetadata = metadata.valuesForHints(['range'])[0];
//Exclude 'name' and 'time' based metadata specifically, from the x-Axis values by using range hints only //Exclude 'name' and 'time' based metadata specifically, from the x-Axis values by using range hints only
const xAxisMetadata = metadata.valuesForHints(['range']) const xAxisMetadata = metadata.valuesForHints(['range']);
.map((metaDatum) => {
metaDatum.isArrayValue = metadata.isArrayValue(metaDatum);
return metaDatum;
});
return { return {
xAxisMetadata, xAxisMetadata,
@@ -229,7 +183,13 @@ export default {
loadComposition() { loadComposition() {
this.composition = this.openmct.composition.get(this.domainObject); this.composition = this.openmct.composition.get(this.domainObject);
this.composition.on('add', this.addToComposition); if (!this.composition) {
this.addTelemetryObject(this.domainObject);
return;
}
this.composition.on('add', this.addTelemetryObject);
this.composition.on('remove', this.removeTelemetryObject); this.composition.on('remove', this.removeTelemetryObject);
this.composition.load(); this.composition.load();
}, },
@@ -252,10 +212,7 @@ export default {
}, },
removeTelemetryObject(identifier) { removeTelemetryObject(identifier) {
const key = this.openmct.objects.makeKeyString(identifier); const key = this.openmct.objects.makeKeyString(identifier);
if (this.telemetryObjects[key]) {
delete this.telemetryObjects[key]; delete this.telemetryObjects[key];
}
if (this.telemetryObjectFormats && this.telemetryObjectFormats[key]) { if (this.telemetryObjectFormats && this.telemetryObjectFormats[key]) {
delete this.telemetryObjectFormats[key]; delete this.telemetryObjectFormats[key];
} }
@@ -280,72 +237,49 @@ export default {
this.openmct.notifications.alert(data.message); this.openmct.notifications.alert(data.message);
} }
if (!this.isDataInTimeRange(data, key, telemetryObject)) { if (!this.isDataInTimeRange(data, key)) {
return;
}
if (this.domainObject.configuration.axes.xKey === undefined || this.domainObject.configuration.axes.yKey === undefined) {
return; return;
} }
let xValues = []; let xValues = [];
let yValues = []; let yValues = [];
let xAxisMetadata = axisMetadata.xAxisMetadata.find(metadata => metadata.key === this.domainObject.configuration.axes.xKey);
if (xAxisMetadata && xAxisMetadata.isArrayValue) {
//populate x and y values
let metadataKey = this.domainObject.configuration.axes.xKey;
if (data[metadataKey] !== undefined) {
xValues = this.parse(key, metadataKey, data);
}
metadataKey = this.domainObject.configuration.axes.yKey;
if (data[metadataKey] !== undefined) {
yValues = this.parse(key, metadataKey, data);
}
} else {
//populate X and Y values for plotly //populate X and Y values for plotly
axisMetadata.xAxisMetadata.filter(metadataObj => !metadataObj.isArrayValue).forEach((metadata) => { axisMetadata.xAxisMetadata.forEach((metadata) => {
if (!xAxisMetadata) {
//Assign the first metadata to use for any formatting
xAxisMetadata = metadata;
}
xValues.push(metadata.name); xValues.push(metadata.name);
if (data[metadata.key]) { if (data[metadata.key]) {
const parsedValue = this.parse(key, metadata.key, data); const formattedValue = this.format(key, metadata.key, data);
yValues.push(parsedValue); yValues.push(formattedValue);
} else { } else {
yValues.push(null); yValues.push(null);
} }
}); });
}
let trace = { let trace = {
key, key,
name: telemetryObject.name, name: telemetryObject.name,
x: xValues, x: xValues,
y: yValues, y: yValues,
xAxisMetadata: xAxisMetadata, text: yValues.map(String),
xAxisMetadata: axisMetadata.xAxisMetadata,
yAxisMetadata: axisMetadata.yAxisMetadata, yAxisMetadata: axisMetadata.yAxisMetadata,
type: this.domainObject.configuration.useBar ? 'bar' : 'scatter', type: this.options.type ? this.options.type : 'bar',
mode: 'lines',
line: {
shape: this.domainObject.configuration.useInterpolation
},
marker: { marker: {
color: this.domainObject.configuration.barStyles.series[key].color color: this.domainObject.configuration.barStyles.series[key].color
}, },
hoverinfo: this.domainObject.configuration.useBar ? 'skip' : 'x+y' hoverinfo: 'skip'
}; };
if (this.options.type) {
trace.mode = 'markers';
trace.hoverinfo = 'x+y';
}
this.addTrace(trace, key); this.addTrace(trace, key);
}, },
isDataInTimeRange(datum, key, telemetryObject) { isDataInTimeRange(datum, key) {
const timeSystemKey = this.timeContext.timeSystem().key; const timeSystemKey = this.timeContext.timeSystem().key;
const metadata = this.openmct.telemetry.getMetadata(telemetryObject); let currentTimestamp = this.parse(key, timeSystemKey, datum);
let metadataValue = metadata.value(timeSystemKey) || { key: timeSystemKey };
let currentTimestamp = this.parse(key, metadataValue.key, datum);
return currentTimestamp && this.timeContext.bounds().end >= currentTimestamp; return currentTimestamp && this.timeContext.bounds().end >= currentTimestamp;
}, },
@@ -365,8 +299,7 @@ export default {
}, },
requestDataFor(telemetryObject) { requestDataFor(telemetryObject) {
const axisMetadata = this.getAxisMetadata(telemetryObject); const axisMetadata = this.getAxisMetadata(telemetryObject);
const options = this.getOptions(); this.openmct.telemetry.request(telemetryObject)
this.openmct.telemetry.request(telemetryObject, options)
.then(data => { .then(data => {
data.forEach((datum) => { data.forEach((datum) => {
this.addDataToGraph(telemetryObject, datum, axisMetadata); this.addDataToGraph(telemetryObject, datum, axisMetadata);

View File

@@ -20,155 +20,18 @@
at runtime from the About dialog for additional information. at runtime from the About dialog for additional information.
--> -->
<template> <template>
<div class="c-bar-graph-options js-bar-plot-option"> <ul class="c-tree c-bar-graph-options">
<ul class="c-tree">
<h2 title="Display properties for this object">Bar Graph Series</h2> <h2 title="Display properties for this object">Bar Graph Series</h2>
<li> <li
<series-options v-for="series in domainObject.composition"
v-for="series in plotSeries"
:key="series.key" :key="series.key"
>
<series-options
:item="series" :item="series"
:color-palette="colorPalette" :color-palette="colorPalette"
/> />
</li> </li>
</ul> </ul>
<div class="grid-properties">
<ul class="l-inspector-part">
<h2 title="Y axis settings for this object">Axes</h2>
<li class="grid-row">
<div
class="grid-cell label"
title="X axis selection."
>X Axis</div>
<div
v-if="isEditing"
class="grid-cell value"
>
<select
v-model="xKey"
@change="updateForm('xKey')"
>
<option
v-for="option in xKeyOptions"
:key="`xKey-${option.value}`"
:value="option.value"
:selected="option.value === xKey"
>
{{ option.name }}
</option>
</select>
</div>
<div
v-else
class="grid-cell value"
>{{ xKeyLabel }}</div>
</li>
<li
v-if="yKey !== ''"
class="grid-row"
>
<div
class="grid-cell label"
title="Y axis selection."
>Y Axis</div>
<div
v-if="isEditing"
class="grid-cell value"
>
<select
v-model="yKey"
@change="updateForm('yKey')"
>
<option
v-for="option in yKeyOptions"
:key="`yKey-${option.value}`"
:value="option.value"
:selected="option.value === yKey"
>
{{ option.name }}
</option>
</select>
</div>
<div
v-else
class="grid-cell value"
>{{ yKeyLabel }}</div>
</li>
</ul>
</div>
<div class="grid-properties">
<ul class="l-inspector-part">
<h2 title="Settings for plot">Settings</h2>
<li class="grid-row">
<div
v-if="isEditing"
class="grid-cell label"
title="Display style for the plot"
>Display Style</div>
<div
v-if="isEditing"
class="grid-cell value"
>
<select
v-model="useBar"
@change="updateBar"
>
<option :value="true">Bar</option>
<option :value="false">Line</option>
</select>
</div>
<div
v-if="!isEditing"
class="grid-cell label"
title="Display style for plot"
>Display Style</div>
<div
v-if="!isEditing"
class="grid-cell value"
>{{ {
'true': 'Bar',
'false': 'Line'
}[useBar] }}
</div>
</li>
<li
v-if="!useBar"
class="grid-row"
>
<div
v-if="isEditing"
class="grid-cell label"
title="The rendering method to join lines for this series."
>Line Method</div>
<div
v-if="isEditing"
class="grid-cell value"
>
<select
v-model="useInterpolation"
@change="updateInterpolation"
>
<option value="linear">Linear interpolate</option>
<option value="hv">Step after</option>
</select>
</div>
<div
v-if="!isEditing"
class="grid-cell label"
title="The rendering method to join lines for this series."
>Line Method</div>
<div
v-if="!isEditing"
class="grid-cell value"
>{{ {
'linear': 'Linear interpolation',
'hv': 'Step After'
}[useInterpolation] }}
</div>
</li>
</ul>
</div>
</div>
</template> </template>
<script> <script>
@@ -182,17 +45,8 @@ export default {
inject: ['openmct', 'domainObject'], inject: ['openmct', 'domainObject'],
data() { data() {
return { return {
xKey: this.domainObject.configuration.axes.xKey,
yKey: this.domainObject.configuration.axes.yKey,
xKeyLabel: '',
yKeyLabel: '',
plotSeries: [],
yKeyOptions: [],
xKeyOptions: [],
isEditing: this.openmct.editor.isEditing(), isEditing: this.openmct.editor.isEditing(),
colorPalette: this.colorPalette, colorPalette: this.colorPalette
useInterpolation: this.domainObject.configuration.useInterpolation,
useBar: this.domainObject.configuration.useBar
}; };
}, },
computed: { computed: {
@@ -205,189 +59,13 @@ export default {
}, },
mounted() { mounted() {
this.openmct.editor.on('isEditing', this.setEditState); this.openmct.editor.on('isEditing', this.setEditState);
this.composition = this.openmct.composition.get(this.domainObject);
this.registerListeners();
this.composition.load();
}, },
beforeDestroy() { beforeDestroy() {
this.openmct.editor.off('isEditing', this.setEditState); this.openmct.editor.off('isEditing', this.setEditState);
this.stopListening();
}, },
methods: { methods: {
setEditState(isEditing) { setEditState(isEditing) {
this.isEditing = isEditing; this.isEditing = isEditing;
},
registerListeners() {
this.composition.on('add', this.addSeries);
this.composition.on('remove', this.removeSeries);
this.unobserve = this.openmct.objects.observe(this.domainObject, 'configuration.axes', this.setKeysAndSetupOptions);
},
stopListening() {
this.composition.off('add', this.addSeries);
this.composition.off('remove', this.removeSeries);
if (this.unobserve) {
this.unobserve();
}
},
addSeries(series, index) {
this.$set(this.plotSeries, this.plotSeries.length, series);
this.setupOptions();
},
removeSeries(seriesIdentifier) {
const index = this.plotSeries.findIndex(plotSeries => this.openmct.objects.areIdsEqual(seriesIdentifier, plotSeries.identifier));
if (index >= 0) {
this.$delete(this.plotSeries, index);
this.setupOptions();
}
},
setKeysAndSetupOptions() {
this.xKey = this.domainObject.configuration.axes.xKey;
this.yKey = this.domainObject.configuration.axes.yKey;
this.setupOptions();
},
setupOptions() {
this.xKeyOptions = [];
this.yKeyOptions = [];
if (this.plotSeries.length <= 0) {
return;
}
let update = false;
const series = this.plotSeries[0];
const metadata = this.openmct.telemetry.getMetadata(series);
const metadataRangeValues = metadata.valuesForHints(['range']).map((metaDatum) => {
metaDatum.isArrayValue = metadata.isArrayValue(metaDatum);
return metaDatum;
});
const metadataArrayValues = metadataRangeValues.filter(metadataObj => metadataObj.isArrayValue);
const metadataValues = metadataRangeValues.filter(metadataObj => !metadataObj.isArrayValue);
metadataArrayValues.forEach((metadataValue) => {
this.xKeyOptions.push({
name: metadataValue.name || metadataValue.key,
value: metadataValue.key,
isArrayValue: metadataValue.isArrayValue
});
this.yKeyOptions.push({
name: metadataValue.name || metadataValue.key,
value: metadataValue.key,
isArrayValue: metadataValue.isArrayValue
});
});
//Metadata values that are not array values will be grouped together as x-axis only option.
// Here, the y-axis is not relevant.
if (metadataValues.length) {
this.xKeyOptions.push(
metadataValues.reduce((previousValue, currentValue) => {
return {
name: previousValue?.name ? `${previousValue.name}, ${currentValue.name}` : `${currentValue.name}`,
value: currentValue.key,
isArrayValue: currentValue.isArrayValue
};
}, {name: ''})
);
}
let xKeyOptionIndex;
let yKeyOptionIndex;
if (this.domainObject.configuration.axes.xKey) {
xKeyOptionIndex = this.xKeyOptions.findIndex(option => option.value === this.domainObject.configuration.axes.xKey);
if (xKeyOptionIndex > -1) {
this.xKey = this.xKeyOptions[xKeyOptionIndex].value;
this.xKeyLabel = this.xKeyOptions[xKeyOptionIndex].name;
}
} else {
if (this.xKey === undefined) {
update = true;
xKeyOptionIndex = 0;
this.xKey = this.xKeyOptions[xKeyOptionIndex].value;
this.xKeyLabel = this.xKeyOptions[xKeyOptionIndex].name;
}
}
if (metadataRangeValues.length > 1) {
if (this.domainObject.configuration.axes.yKey && this.domainObject.configuration.axes.yKey !== 'none') {
yKeyOptionIndex = this.yKeyOptions.findIndex(option => option.value === this.domainObject.configuration.axes.yKey);
if (yKeyOptionIndex > -1 && yKeyOptionIndex !== xKeyOptionIndex) {
this.yKey = this.yKeyOptions[yKeyOptionIndex].value;
this.yKeyLabel = this.yKeyOptions[yKeyOptionIndex].name;
}
} else {
if (this.yKey === undefined) {
yKeyOptionIndex = this.yKeyOptions.findIndex((option, index) => index !== xKeyOptionIndex);
if (yKeyOptionIndex > -1) {
update = true;
this.yKey = this.yKeyOptions[yKeyOptionIndex].value;
this.yKeyLabel = this.yKeyOptions[yKeyOptionIndex].name;
}
}
}
this.yKeyOptions = this.yKeyOptions.map((option, index) => {
if (index === xKeyOptionIndex) {
option.name = `${option.name} (swap)`;
option.swap = yKeyOptionIndex;
} else {
option.name = option.name.replace(' (swap)', '');
option.swap = undefined;
}
return option;
});
} else if (this.xKey !== undefined && this.domainObject.configuration.axes.yKey === undefined) {
this.domainObject.configuration.axes.yKey = 'none';
}
this.xKeyOptions = this.xKeyOptions.map((option, index) => {
if (index === yKeyOptionIndex) {
option.name = `${option.name} (swap)`;
option.swap = xKeyOptionIndex;
} else {
option.name = option.name.replace(' (swap)', '');
option.swap = undefined;
}
return option;
});
if (update === true) {
this.saveConfiguration();
}
},
updateForm(property) {
if (property === 'xKey') {
const xKeyOption = this.xKeyOptions.find(option => option.value === this.xKey);
if (xKeyOption.swap !== undefined) {
//swap
this.yKey = this.xKeyOptions[xKeyOption.swap].value;
} else if (!xKeyOption.isArrayValue) {
this.yKey = 'none';
} else {
this.yKey = undefined;
}
} else if (property === 'yKey') {
const yKeyOption = this.yKeyOptions.find(option => option.value === this.yKey);
if (yKeyOption.swap !== undefined) {
//swap
this.xKey = this.yKeyOptions[yKeyOption.swap].value;
}
}
this.saveConfiguration();
},
saveConfiguration() {
this.openmct.objects.mutate(this.domainObject, `configuration.axes`, {
xKey: this.xKey,
yKey: this.yKey
});
},
updateInterpolation(event) {
this.openmct.objects.mutate(this.domainObject, `configuration.useInterpolation`, this.useInterpolation);
},
updateBar(event) {
this.openmct.objects.mutate(this.domainObject, `configuration.useBar`, this.useBar);
} }
} }
}; };

View File

@@ -38,19 +38,16 @@
<div class="c-object-label__name">{{ name }}</div> <div class="c-object-label__name">{{ name }}</div>
</div> </div>
</li> </li>
<ul class="grid-properties">
<li class="grid-row">
<ColorSwatch <ColorSwatch
v-if="expanded" v-if="expanded"
:current-color="currentColor" :current-color="currentColor"
title="Manually set the color for this bar graph series." title="Manually set the color for this bar graph series."
edit-title="Manually set the color for this bar graph series." edit-title="Manually set the color for this bar graph series"
view-title="The color for this bar graph series." view-title="The color for this bar graph series."
short-label="Color" short-label="Color"
class="grid-properties"
@colorSet="setColor" @colorSet="setColor"
/> />
</li>
</ul>
</ul> </ul>
</template> </template>
@@ -112,6 +109,7 @@ export default {
} }
}, },
mounted() { mounted() {
this.key = this.openmct.objects.makeKeyString(this.item);
this.initColorAndName(); this.initColorAndName();
this.removeBarStylesListener = this.openmct.objects.observe(this.domainObject, `configuration.barStyles.series["${this.key}"]`, this.initColorAndName); this.removeBarStylesListener = this.openmct.objects.observe(this.domainObject, `configuration.barStyles.series["${this.key}"]`, this.initColorAndName);
}, },
@@ -122,7 +120,6 @@ export default {
}, },
methods: { methods: {
initColorAndName() { initColorAndName() {
this.key = this.openmct.objects.makeKeyString(this.item.identifier);
// this is called before the plot is initialized // this is called before the plot is initialized
if (!this.domainObject.configuration.barStyles.series[this.key]) { if (!this.domainObject.configuration.barStyles.series[this.key]) {
const color = this.colorPalette.getNextColor().asHexString(); const color = this.colorPalette.getNextColor().asHexString();

Some files were not shown because too many files have changed in this diff Show More