Compare commits

..

2 Commits

Author SHA1 Message Date
Jesse Mazzella
a31e5e591a Use global time context 2022-06-30 19:54:48 -07:00
Jesse Mazzella
dfa0b54c7c Wait for clock initialization before requesting historical data 2022-06-30 14:48:27 -07:00
98 changed files with 1375 additions and 2404 deletions

View File

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

View File

@@ -30,8 +30,7 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version: '16'
- run: npx playwright@1.23.0 install
- run: npx playwright install chrome-beta
- run: npx playwright@1.21.1 install
- run: npm install
- run: npm run test:e2e:full
- name: Archive test results

View File

@@ -17,7 +17,7 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version: '16'
- run: npx playwright@1.23.0 install
- run: npx playwright@1.21.1 install
- run: npm install
- name: Run the e2e visual tests
run: npm run test:e2e:visual

22
.gitignore vendored
View File

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

31
app.js
View File

@@ -12,7 +12,6 @@ const express = require('express');
const app = express();
const fs = require('fs');
const request = require('request');
const __DEV__ = process.env.NODE_ENV === 'development';
// Defaults
options.port = options.port || options.p || 8080;
@@ -50,18 +49,14 @@ class WatchRunPlugin {
}
const webpack = require('webpack');
let webpackConfig;
if (__DEV__) {
webpackConfig = require('./webpack.dev');
webpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin());
webpackConfig.entry.openmct = [
'webpack-hot-middleware/client?reload=true',
webpackConfig.entry.openmct
];
webpackConfig.plugins.push(new WatchRunPlugin());
} else {
webpackConfig = require('./webpack.coverage');
}
const webpackConfig = require('./webpack.dev.js');
webpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin());
webpackConfig.plugins.push(new WatchRunPlugin());
webpackConfig.entry.openmct = [
'webpack-hot-middleware/client?reload=true',
webpackConfig.entry.openmct
];
const compiler = webpack(webpackConfig);
@@ -73,12 +68,10 @@ app.use(require('webpack-dev-middleware')(
}
));
if (__DEV__) {
app.use(require('webpack-hot-middleware')(
compiler,
{}
));
}
app.use(require('webpack-hot-middleware')(
compiler,
{}
));
// Expose index.html for development users.
app.get('/', function (req, res) {

View File

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

View File

@@ -1,13 +1,8 @@
/* This file extends the base functionality of the playwright test framework to enable
* code coverage instrumentation, console log error detection and working with a 3rd
* party Chrome-as-a-service extension called Browserless.
*/
/* eslint-disable no-undef */
// This file extends the base functionality of the playwright test framework
const base = require('@playwright/test');
const { expect } = require('@playwright/test');
const fs = require('fs');
const path = require('path');
const { v4: uuid } = require('uuid');
/**
* Takes a `ConsoleMessage` and returns a formatted string
@@ -21,30 +16,7 @@ function consoleMessageToString(msg) {
at (${url} ${lineNumber}:${columnNumber})`;
}
//The following is based on https://github.com/mxschmitt/playwright-test-coverage
// eslint-disable-next-line no-undef
const istanbulCLIOutput = path.join(process.cwd(), '.nyc_output');
// eslint-disable-next-line no-undef
exports.test = base.test.extend({
//The following is based on https://github.com/mxschmitt/playwright-test-coverage
context: async ({ context }, use) => {
await context.addInitScript(() =>
window.addEventListener('beforeunload', () =>
(window).collectIstanbulCoverage(JSON.stringify((window).__coverage__))
)
);
await fs.promises.mkdir(istanbulCLIOutput, { recursive: true });
await context.exposeFunction('collectIstanbulCoverage', (coverageJSON) => {
if (coverageJSON) {
fs.writeFileSync(path.join(istanbulCLIOutput, `playwright_coverage_${uuid()}.json`), coverageJSON);
}
});
await use(context);
for (const page of context.pages()) {
await page.evaluate(() => (window).collectIstanbulCoverage(JSON.stringify((window).__coverage__)));
}
},
page: async ({ baseURL, page }, use) => {
const messages = [];
page.on('console', (msg) => messages.push(msg));

View File

@@ -4,30 +4,28 @@
// eslint-disable-next-line no-unused-vars
const { devices } = require('@playwright/test');
const MAX_FAILURES = 5;
const NUM_WORKERS = 2;
/** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = {
retries: 3, //Retries 3 times for a total of 4. When running sharded and with maxFailures = 5, this should ensure that flake is managed without failing the full suite
retries: 1,
testDir: 'tests',
testIgnore: '**/*.perf.spec.js', //Ignore performance tests and define in playwright-perfromance.config.js
timeout: 60 * 1000,
webServer: {
command: 'cross-env NODE_ENV=test npm run start',
url: 'http://localhost:8080/#',
command: 'npm run start',
port: 8080,
timeout: 200 * 1000,
reuseExistingServer: false
reuseExistingServer: !process.env.CI
},
maxFailures: MAX_FAILURES, //Limits failures to 5 to reduce CI Waste
workers: NUM_WORKERS, //Limit to 2 for CircleCI Agent
maxFailures: process.env.CI ? 5 : undefined, //Limits failures to 5 to reduce CI Waste
workers: 2, //Limit to 2 for CircleCI Agent
use: {
baseURL: 'http://localhost:8080/',
headless: true,
ignoreHTTPSErrors: true,
screenshot: 'only-on-failure',
trace: 'on-first-retry',
video: 'off'
video: 'on-first-retry'
},
projects: [
{
@@ -38,7 +36,6 @@ const config = {
},
{
name: 'MMOC',
testMatch: '**/*.e2e.spec.js', // only run e2e tests
grepInvert: /@snapshot/,
use: {
browserName: 'chromium',
@@ -47,30 +44,20 @@ const config = {
height: 1440
}
}
},
{
name: 'firefox',
testMatch: '**/*.e2e.spec.js', // only run e2e tests
grepInvert: /@snapshot/,
use: {
browserName: 'firefox'
}
},
{
name: 'chrome-beta', //Only Chrome Beta is available on ubuntu -- not chrome canary
testMatch: '**/*.e2e.spec.js', // only run e2e tests
grepInvert: /@snapshot/,
use: {
browserName: 'chromium',
channel: 'chrome-beta'
}
}
/*{
name: 'ipad',
use: {
browserName: 'webkit',
...devices['iPad (gen 7) landscape'] // Complete List https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json
}
}*/
],
reporter: [
['list'],
['html', {
open: 'never',
outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840
outputFolder: '../test-results/html/'
}],
['junit', { outputFile: 'test-results/results.xml' }],
['github']

View File

@@ -12,10 +12,10 @@ const config = {
testIgnore: '**/*.perf.spec.js',
timeout: 30 * 1000,
webServer: {
command: 'cross-env NODE_ENV=test npm run start',
url: 'http://localhost:8080/#',
command: 'npm run start',
port: 8080,
timeout: 120 * 1000,
reuseExistingServer: true
reuseExistingServer: !process.env.CI
},
workers: 1,
use: {
@@ -25,7 +25,7 @@ const config = {
ignoreHTTPSErrors: true,
screenshot: 'only-on-failure',
trace: 'retain-on-failure',
video: 'off'
video: 'retain-on-failure'
},
projects: [
{
@@ -36,7 +36,6 @@ const config = {
},
{
name: 'MMOC',
testMatch: '**/*.e2e.spec.js', // only run e2e tests
grepInvert: /@snapshot/,
use: {
browserName: 'chromium',
@@ -45,58 +44,20 @@ const config = {
height: 1440
}
}
},
{
name: 'safari',
testMatch: '**/*.e2e.spec.js', // only run e2e tests
grep: /@ipad/, // only run ipad tests due to this bug https://github.com/microsoft/playwright/issues/8340
grepInvert: /@snapshot/,
use: {
browserName: 'webkit'
}
},
{
name: 'firefox',
testMatch: '**/*.e2e.spec.js', // only run e2e tests
grepInvert: /@snapshot/,
use: {
browserName: 'firefox'
}
},
{
name: 'canary',
testMatch: '**/*.e2e.spec.js', // only run e2e tests
grepInvert: /@snapshot/,
use: {
browserName: 'chromium',
channel: 'chrome-canary' //Note this is not available in ubuntu/CircleCI
}
},
{
name: 'chrome-beta',
testMatch: '**/*.e2e.spec.js', // only run e2e tests
grepInvert: /@snapshot/,
use: {
browserName: 'chromium',
channel: 'chrome-beta'
}
},
{
}
/*{
name: 'ipad',
testMatch: '**/*.e2e.spec.js', // only run e2e tests
grep: /@ipad/,
grepInvert: /@snapshot/,
use: {
browserName: 'webkit',
...devices['iPad (gen 7) landscape'] // Complete List https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json
}
}
}*/
],
reporter: [
['list'],
['html', {
open: 'on-failure',
outputFolder: '../html-test-results' //Must be in different location due to https://github.com/microsoft/playwright/issues/12840
outputFolder: '../test-results'
}]
]
};

View File

@@ -2,24 +2,22 @@
// playwright.config.js
// @ts-check
const CI = process.env.CI === 'true';
/** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = {
retries: 1, //Only for debugging purposes because trace is enabled only on first retry
retries: 1, //Only for debugging purposes
testDir: 'tests/performance/',
timeout: 60 * 1000,
workers: 1, //Only run in serial with 1 worker
webServer: {
command: 'cross-env NODE_ENV=test npm run start',
url: 'http://localhost:8080/#',
command: 'npm run start',
port: 8080,
timeout: 200 * 1000,
reuseExistingServer: !CI
reuseExistingServer: !process.env.CI
},
use: {
browserName: "chromium",
baseURL: 'http://localhost:8080/',
headless: CI, //Only if running locally
headless: Boolean(process.env.CI), //Only if running locally
ignoreHTTPSErrors: true,
screenshot: 'off',
trace: 'on-first-retry',

View File

@@ -9,8 +9,8 @@ const config = {
timeout: 90 * 1000,
workers: 1, // visual tests should never run in parallel due to test pollution
webServer: {
command: 'cross-env NODE_ENV=test npm run start',
url: 'http://localhost:8080/#',
command: 'npm run start',
port: 8080,
timeout: 200 * 1000,
reuseExistingServer: !process.env.CI
},
@@ -21,7 +21,7 @@ const config = {
ignoreHTTPSErrors: true,
screenshot: 'on',
trace: 'off',
video: 'off'
video: 'on'
},
reporter: [
['list'],

View File

@@ -36,7 +36,7 @@ test.describe('Branding tests', () => {
await page.click('.l-shell__app-logo');
// Verify that the NASA Logo Appears
await expect(page.locator('.c-about__image')).toBeVisible();
await expect(await page.locator('.c-about__image')).toBeVisible();
// Modify the Build information in 'about' Modal
const versionInformationLocator = page.locator('ul.t-info.l-info.s-info');
@@ -58,7 +58,6 @@ test.describe('Branding tests', () => {
page.waitForEvent('popup'),
page.locator('text=click here for third party licensing information').click()
]);
await page2.waitForLoadState('networkidle'); //Avoids timing issues with juggler/firefox
expect(page2.waitForURL('**/licenses**')).toBeTruthy();
});
});

View File

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

View File

@@ -1,55 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
This test suite is dedicated to testing our use of the playwright framework as it
relates to how we've extended it (i.e. ./e2e/fixtures.js) and assumptions made in our dev environment
(app.js and ./e2e/webpack-dev-middleware.js)
*/
const { test } = require('../fixtures.js');
test.describe('fixtures.js tests', () => {
test('Verify that tests fail if console.error is thrown', async ({ page }) => {
test.fail();
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
//Verify that ../fixtures.js detects console log errors
await Promise.all([
page.evaluate(() => console.error('This should result in a failure')),
page.waitForEvent('console') // always wait for the event to happen while triggering it!
]);
});
test('Verify that tests pass if console.warn is thrown', async ({ page }) => {
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
//Verify that ../fixtures.js detects console log errors
await Promise.all([
page.evaluate(() => console.warn('This should result in a pass')),
page.waitForEvent('console') // always wait for the event to happen while triggering it!
]);
});
});

View File

@@ -33,7 +33,7 @@ let conditionSetUrl;
let getConditionSetIdentifierFromUrl;
test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
test.beforeAll(async ({ browser}) => {
test.beforeAll(async ({ browser }) => {
const context = await browser.newContext();
const page = await context.newPage();
//Go to baseURL
@@ -55,25 +55,27 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
await context.storageState({ path: './e2e/test-data/recycled_local_storage.json' });
//Set object identifier from url
conditionSetUrl = page.url();
conditionSetUrl = await page.url();
console.log('conditionSetUrl ' + conditionSetUrl);
getConditionSetIdentifierFromUrl = conditionSetUrl.split('/').pop().split('?')[0];
getConditionSetIdentifierFromUrl = await conditionSetUrl.split('/').pop().split('?')[0];
console.debug('getConditionSetIdentifierFromUrl ' + getConditionSetIdentifierFromUrl);
});
test.afterAll(async ({ browser }) => {
await browser.close();
});
//Load localStorage for subsequent tests
test.use({ storageState: './e2e/test-data/recycled_local_storage.json' });
//Begin suite of tests again localStorage
test('Condition set object properties persist in main view and inspector @localStorage', async ({ page }) => {
test('Condition set object properties persist in main view and inspector', async ({ page }) => {
//Navigate to baseURL with injected localStorage
await page.goto(conditionSetUrl, { waitUntil: 'networkidle' });
//Assertions on loaded Condition Set in main view. This is a stateful transition step after page.goto()
//Assertions on loaded Condition Set in main view
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
//Assertions on loaded Condition Set in Inspector
expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy();
await expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy;
//Reload Page
await Promise.all([
@@ -84,13 +86,13 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
//Re-verify after reload
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
//Assertions on loaded Condition Set in Inspector
expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy();
await expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy;
});
test('condition set object can be modified on @localStorage', async ({ page }) => {
await page.goto(conditionSetUrl, { waitUntil: 'networkidle' });
//Assertions on loaded Condition Set in main view. This is a stateful transition step after page.goto()
//Assertions on loaded Condition Set in main view
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
//Update the Condition Set properties
@@ -110,18 +112,18 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
// Verify Inspector properties
// Verify Inspector has updated Name property
expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy();
await expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy();
// Verify Inspector Details has updated Name property
expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy();
await expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy();
// Verify Tree reflects updated Name proprety
// Expand Tree
await page.locator('text=Open MCT My Items >> span >> nth=3').click();
// Verify Condition Set Object is renamed in Tree
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
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Renamed');
expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
//Reload Page
await Promise.all([
@@ -134,27 +136,26 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
// Verify Inspector properties
// Verify Inspector has updated Name property
expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy();
await expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy();
// Verify Inspector Details has updated Name property
expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy();
await expect.soft(page.locator('text=Renamed Condition Set').nth(2)).toBeTruthy();
// Verify Tree reflects updated Name proprety
// Expand Tree
await page.locator('text=Open MCT My Items >> span >> nth=3').click();
// Verify Condition Set Object is renamed in Tree
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
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Renamed');
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 }) => {
//Navigate to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
//Assertions on loaded Condition Set in main view. This is a stateful transition step after page.goto()
await expect(page.locator('a:has-text("Unnamed Condition Set Condition Set") >> nth=0')).toBeVisible();
const numberOfConditionSetsToStart = await page.locator('a:has-text("Unnamed Condition Set Condition Set")').count();
//Expect Unnamed Condition Set to be visible in Main View
await expect(page.locator('a:has-text("Unnamed Condition Set Condition Set") >> nth=0')).toBeVisible();
// Search for Unnamed Condition Set
await page.locator('[aria-label="OpenMCT Search"] input[type="search"]').fill('Unnamed Condition Set');
@@ -162,14 +163,13 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
await page.locator('[aria-label="OpenMCT Search"] >> text=Unnamed Condition Set').first().click();
// Click hamburger button
await page.locator('[title="More options"]').click();
// Click text=Remove
await page.locator('text=Remove').click();
await page.locator('text=OK').click();
//Expect Unnamed Condition Set to be removed in Main View
const numberOfConditionSetsAtEnd = await page.locator('a:has-text("Unnamed Condition Set Condition Set")').count();
expect(numberOfConditionSetsAtEnd).toEqual(numberOfConditionSetsToStart - 1);
//Feature?

View File

@@ -20,19 +20,11 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
export default function stackedPlotConfigurationInterceptor(openmct) {
// this will be called from the test suite with
// await page.addInitScript({ path: path.join(__dirname, 'addInitGauge.js') });
// it will install the Gauge since it is not installed by default
openmct.objects.addGetInterceptor({
appliesTo: (identifier, domainObject) => {
return domainObject && domainObject.type === 'telemetry.plot.stacked';
},
invoke: (identifier, object) => {
if (object && object.configuration && object.configuration.series === undefined) {
object.configuration.series = [];
}
return object;
}
});
}
document.addEventListener('DOMContentLoaded', () => {
const openmct = window.openmct;
openmct.install(openmct.plugins.Gauge());
});

View File

@@ -29,10 +29,7 @@ but only assume that example imagery is present.
const { test } = require('../../../fixtures.js');
const { expect } = require('@playwright/test');
const backgroundImageSelector = '.c-imagery__main-image__background-image';
//The following block of tests verifies the basic functionality of example imagery and serves as a template for Imagery objects embedded in other objects.
test.describe('Example Imagery Object', () => {
test.describe('Example Imagery', () => {
test.beforeEach(async ({ page }) => {
//Go to baseURL
@@ -51,30 +48,28 @@ test.describe('Example Imagery Object', () => {
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
// Close Banner
await page.locator('.c-message-banner__close-button').click();
//Wait until Save Banner is gone
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');
await page.locator(backgroundImageSelector).hover({trial: true});
});
const backgroundImageSelector = '.c-imagery__main-image__background-image';
test('Can use Mouse Wheel to zoom in and out of latest image', async ({ page }) => {
const bgImageLocator = page.locator(backgroundImageSelector);
const deltaYStep = 100; //equivalent to 1x zoom
await page.locator(backgroundImageSelector).hover({trial: true});
await bgImageLocator.hover({trial: true});
const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox();
// zoom in
await page.locator(backgroundImageSelector).hover({trial: true});
await bgImageLocator.hover({trial: true});
await page.mouse.wheel(0, deltaYStep * 2);
// wait for zoom animation to finish
await page.locator(backgroundImageSelector).hover({trial: true});
await bgImageLocator.hover({trial: true});
const imageMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox();
// zoom out
await page.locator(backgroundImageSelector).hover({trial: true});
await bgImageLocator.hover({trial: true});
await page.mouse.wheel(0, -deltaYStep);
// wait for zoom animation to finish
await page.locator(backgroundImageSelector).hover({trial: true});
await bgImageLocator.hover({trial: true});
const imageMouseZoomedOut = await page.locator(backgroundImageSelector).boundingBox();
expect(imageMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height);
@@ -88,12 +83,13 @@ test.describe('Example Imagery Object', () => {
const deltaYStep = 100; //equivalent to 1x zoom
const panHotkey = process.platform === 'linux' ? ['Control', 'Alt'] : ['Alt'];
await page.locator(backgroundImageSelector).hover({trial: true});
const bgImageLocator = page.locator(backgroundImageSelector);
await bgImageLocator.hover({trial: true});
// zoom in
await page.mouse.wheel(0, deltaYStep * 2);
await page.locator(backgroundImageSelector).hover({trial: true});
const zoomedBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
await bgImageLocator.hover({trial: true});
const zoomedBoundingBox = await bgImageLocator.boundingBox();
const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;
const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
// move to the right
@@ -116,7 +112,7 @@ test.describe('Example Imagery Object', () => {
await page.mouse.move(imageCenterX - 200, imageCenterY, 10);
await page.mouse.up();
await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
const afterRightPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
const afterRightPanBoundingBox = await bgImageLocator.boundingBox();
expect(zoomedBoundingBox.x).toBeGreaterThan(afterRightPanBoundingBox.x);
// pan left
@@ -125,7 +121,7 @@ test.describe('Example Imagery Object', () => {
await page.mouse.move(imageCenterX, imageCenterY, 10);
await page.mouse.up();
await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
const afterLeftPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
const afterLeftPanBoundingBox = await bgImageLocator.boundingBox();
expect(afterRightPanBoundingBox.x).toBeLessThan(afterLeftPanBoundingBox.x);
// pan up
@@ -135,7 +131,7 @@ test.describe('Example Imagery Object', () => {
await page.mouse.move(imageCenterX, imageCenterY + 200, 10);
await page.mouse.up();
await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
const afterUpPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
const afterUpPanBoundingBox = await bgImageLocator.boundingBox();
expect(afterUpPanBoundingBox.y).toBeGreaterThan(afterLeftPanBoundingBox.y);
// pan down
@@ -144,71 +140,72 @@ test.describe('Example Imagery Object', () => {
await page.mouse.move(imageCenterX, imageCenterY - 200, 10);
await page.mouse.up();
await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
const afterDownPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
const afterDownPanBoundingBox = await bgImageLocator.boundingBox();
expect(afterDownPanBoundingBox.y).toBeLessThan(afterUpPanBoundingBox.y);
});
test('Can use + - buttons to zoom on the image', async ({ page }) => {
await page.locator(backgroundImageSelector).hover({trial: true});
const bgImageLocator = page.locator(backgroundImageSelector);
await bgImageLocator.hover({trial: true});
const zoomInBtn = page.locator('.t-btn-zoom-in').nth(0);
const zoomOutBtn = page.locator('.t-btn-zoom-out').nth(0);
const initialBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
const initialBoundingBox = await bgImageLocator.boundingBox();
await zoomInBtn.click();
await zoomInBtn.click();
// wait for zoom animation to finish
await page.locator(backgroundImageSelector).hover({trial: true});
const zoomedInBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
await bgImageLocator.hover({trial: true});
const zoomedInBoundingBox = await bgImageLocator.boundingBox();
expect(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
expect(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);
await zoomOutBtn.click();
// wait for zoom animation to finish
await page.locator(backgroundImageSelector).hover({trial: true});
const zoomedOutBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
await bgImageLocator.hover({trial: true});
const zoomedOutBoundingBox = await bgImageLocator.boundingBox();
expect(zoomedOutBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height);
expect(zoomedOutBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width);
});
test('Can use the reset button to reset the image', async ({ page }, testInfo) => {
test.slow(testInfo.project === 'chrome-beta', "This test is slow in chrome-beta");
test('Can use the reset button to reset the image', async ({ page }) => {
const bgImageLocator = page.locator(backgroundImageSelector);
// wait for zoom animation to finish
await page.locator(backgroundImageSelector).hover({trial: true});
await bgImageLocator.hover({trial: true});
const zoomInBtn = page.locator('.t-btn-zoom-in').nth(0);
const zoomResetBtn = page.locator('.t-btn-zoom-reset').nth(0);
const initialBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
const initialBoundingBox = await bgImageLocator.boundingBox();
await zoomInBtn.click();
// wait for zoom animation to finish
await page.locator(backgroundImageSelector).hover({trial: true});
await bgImageLocator.hover({trial: true});
await zoomInBtn.click();
// wait for zoom animation to finish
await page.locator(backgroundImageSelector).hover({trial: true});
await bgImageLocator.hover({trial: true});
const zoomedInBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
const zoomedInBoundingBox = await bgImageLocator.boundingBox();
expect.soft(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
expect.soft(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);
await zoomResetBtn.click();
// wait for zoom animation to finish
// FIXME: The zoom is flakey, sometimes not returning to original dimensions
// https://github.com/nasa/openmct/issues/5491
await expect.poll(async () => {
await zoomResetBtn.click();
const boundingBox = await page.locator(backgroundImageSelector).boundingBox();
await bgImageLocator.hover({trial: true});
return boundingBox;
}, {
timeout: 10 * 1000
}).toEqual(initialBoundingBox);
const resetBoundingBox = await bgImageLocator.boundingBox();
expect.soft(resetBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height);
expect.soft(resetBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width);
expect.soft(resetBoundingBox.height).toEqual(initialBoundingBox.height);
expect(resetBoundingBox.width).toEqual(initialBoundingBox.width);
});
test('Using the zoom features does not pause telemetry', async ({ page }) => {
const bgImageLocator = page.locator(backgroundImageSelector);
const pausePlayButton = page.locator('.c-button.pause-play');
// wait for zoom animation to finish
await page.locator(backgroundImageSelector).hover({trial: true});
await bgImageLocator.hover({trial: true});
// open the time conductor drop down
await page.locator('button:has-text("Fixed Timespan")').click();
@@ -219,7 +216,7 @@ test.describe('Example Imagery Object', () => {
const zoomInBtn = page.locator('.t-btn-zoom-in').nth(0);
await zoomInBtn.click();
// wait for zoom animation to finish
await page.locator(backgroundImageSelector).hover({trial: true});
await bgImageLocator.hover({trial: true});
return expect(pausePlayButton).not.toHaveClass(/is-paused/);
});
@@ -232,8 +229,8 @@ test.describe('Example Imagery Object', () => {
// ('Clicking on the left arrow should pause the imagery and go to previous image');
// ('If the imagery view is in pause mode, it should not be updated when new images come in');
// ('If the imagery view is not in pause mode, it should be updated when new images come in');
test('Example Imagery in Display layout', async ({ page, browserName }) => {
test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox');
const backgroundImageSelector = '.c-imagery__main-image__background-image';
test('Example Imagery in Display layout', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5265'
@@ -265,7 +262,8 @@ test('Example Imagery in Display layout', async ({ page, browserName }) => {
// Wait until Save Banner is gone
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');
await page.locator(backgroundImageSelector).hover({trial: true});
const bgImageLocator = page.locator(backgroundImageSelector);
await bgImageLocator.hover({trial: true});
// Click previous image button
const previousImageButton = page.locator('.c-nav--prev');
@@ -277,15 +275,15 @@ test('Example Imagery in Display layout', async ({ page, browserName }) => {
// Zoom in
const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox();
await page.locator(backgroundImageSelector).hover({trial: true});
await bgImageLocator.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 zoomedBoundingBox = await bgImageLocator.boundingBox();
const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;
const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
// Wait for zoom animation to finish
await page.locator(backgroundImageSelector).hover({trial: true});
await bgImageLocator.hover({trial: true});
const imageMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox();
expect(imageMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height);
expect(imageMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width);
@@ -309,11 +307,11 @@ test('Example Imagery in Display layout', async ({ page, browserName }) => {
await page.locator('[data-testid=conductor-modeOption-realtime]').click();
// Zoom in on next image
await page.locator(backgroundImageSelector).hover({trial: true});
await bgImageLocator.hover({trial: true});
await page.mouse.wheel(0, deltaYStep * 2);
// Wait for zoom animation to finish
await page.locator(backgroundImageSelector).hover({trial: true});
await bgImageLocator.hover({trial: true});
const imageNextMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox();
expect(imageNextMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height);
expect(imageNextMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width);
@@ -339,6 +337,7 @@ test('Example Imagery in Display layout', async ({ page, browserName }) => {
});
test.describe('Example imagery thumbnails resize in display layouts', () => {
test('Resizing the layout changes thumbnail visibility and size', async ({ page }) => {
await page.goto('/', { waitUntil: 'networkidle' });
@@ -427,137 +426,12 @@ test.describe('Example imagery thumbnails resize in display layouts', () => {
});
test.describe('Example Imagery in Flexible layout', () => {
test('Example Imagery in Flexible layout', async ({ page, browserName }) => {
test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox');
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5326'
});
// Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
// Click the Create button
await page.click('button:has-text("Create")');
// Click text=Example Imagery
await page.click('text=Example Imagery');
// Clear and set Image load delay (milliseconds)
await page.click('input[type="number"]', {clickCount: 3});
await page.type('input[type="number"]', "20");
// Click text=OK
await Promise.all([
page.waitForNavigation({waitUntil: 'networkidle'}),
page.click('text=OK'),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
// Wait until Save Banner is gone
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');
await page.locator(backgroundImageSelector).hover({trial: true});
// Click the Create button
await page.click('button:has-text("Create")');
// Click text=Flexible Layout
await page.click('text=Flexible Layout');
// Assert Flexable layout
await expect(page.locator('.js-form-title')).toHaveText('Create a New Flexible Layout');
await page.locator('form[name="mctForm"] >> text=My Items').click();
// Click My Items
await Promise.all([
page.locator('text=OK').click(),
page.waitForNavigation({waitUntil: 'networkidle'})
]);
// Click My Items
await page.locator('.c-disclosure-triangle').click();
// Right click example imagery
await page.click(('text=Unnamed Example Imagery'), { button: 'right' });
// Click move
await page.locator('.icon-move').click();
// Click triangle to open sub menu
await page.locator('.c-form__section .c-disclosure-triangle').click();
// Click Flexable Layout
await page.click('.c-overlay__outer >> text=Unnamed Flexible Layout');
// Click text=OK
await page.locator('text=OK').click();
// Save template
await saveTemplate(page);
// Zoom in
await mouseZoomIn(page);
// Center the mouse pointer
const zoomedBoundingBox = await await page.locator(backgroundImageSelector).boundingBox();
const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;
const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
await page.mouse.move(imageCenterX, imageCenterY);
// Pan zoom
await panZoomAndAssertImageProperties(page);
// Click previous image button
const previousImageButton = page.locator('.c-nav--prev');
await previousImageButton.click();
// Verify previous image
const selectedImage = page.locator('.selected');
await expect(selectedImage).toBeVisible();
// Click time conductor mode button
await page.locator('.c-mode-button').click();
// Select local clock mode
await page.locator('[data-testid=conductor-modeOption-realtime]').click();
// Zoom in on next image
await mouseZoomIn(page);
// Click previous image button
await previousImageButton.click();
// Verify previous image
await expect(selectedImage).toBeVisible();
const imageCount = await page.locator('.c-imagery__thumb').count();
await expect.poll(async () => {
const newImageCount = await page.locator('.c-imagery__thumb').count();
return newImageCount;
}, {
message: "verify that old images are discarded",
timeout: 6 * 1000
}).toBe(imageCount);
// Verify selected image is still displayed
await expect(selectedImage).toBeVisible();
// Unpause imagery
await page.locator('.pause-play').click();
//Get background-image url from background-image css prop
await assertBackgroundImageUrlFromBackgroundCss(page);
// Open the image filter menu
await page.locator('[role=toolbar] button[title="Brightness and contrast"]').click();
// Drag the brightness and contrast sliders around and assert filter values
await dragBrightnessSliderAndAssertFilterValues(page);
await dragContrastSliderAndAssertFilterValues(page);
});
test.fixme('Can use Mouse Wheel to zoom in and out of previous image');
test.fixme('Can use alt+drag to move around image once zoomed in');
test.fixme('Can zoom into the latest image and the real-time/fixed-time imagery will pause');
test.fixme('Clicking on the left arrow should pause the imagery and go to previous image');
test.fixme('If the imagery view is in pause mode, it should not be updated when new images come in');
test.fixme('If the imagery view is not in pause mode, it should be updated when new images come in');
});
test.describe('Example Imagery in Tabs view', () => {
@@ -569,185 +443,3 @@ test.describe('Example Imagery in Tabs view', () => {
test.fixme('If the imagery view is in pause mode, it should not be updated when new images come in');
test.fixme('If the imagery view is not in pause mode, it should be updated when new images come in');
});
/**
* @param {import('@playwright/test').Page} page
*/
async function saveTemplate(page) {
await page.locator('.c-button--menu.c-button--major.icon-save').click();
await page.locator('text=Save and Finish Editing').click();
}
/**
* Drag the brightness slider to max, min, and midpoint and assert the filter values
* @param {import('@playwright/test').Page} page
*/
async function dragBrightnessSliderAndAssertFilterValues(page) {
const brightnessSlider = 'div.c-image-controls__slider-wrapper.icon-brightness > input';
const brightnessBoundingBox = await page.locator(brightnessSlider).boundingBox();
const brightnessMidX = brightnessBoundingBox.x + brightnessBoundingBox.width / 2;
const brightnessMidY = brightnessBoundingBox.y + brightnessBoundingBox.height / 2;
await page.locator(brightnessSlider).hover({trial: true});
await page.mouse.down();
await page.mouse.move(brightnessBoundingBox.x + brightnessBoundingBox.width, brightnessMidY);
await assertBackgroundImageBrightness(page, '500');
await page.mouse.move(brightnessBoundingBox.x, brightnessMidY);
await assertBackgroundImageBrightness(page, '0');
await page.mouse.move(brightnessMidX, brightnessMidY);
await assertBackgroundImageBrightness(page, '250');
await page.mouse.up();
}
/**
* Drag the contrast slider to max, min, and midpoint and assert the filter values
* @param {import('@playwright/test').Page} page
*/
async function dragContrastSliderAndAssertFilterValues(page) {
const contrastSlider = 'div.c-image-controls__slider-wrapper.icon-contrast > input';
const contrastBoundingBox = await page.locator(contrastSlider).boundingBox();
const contrastMidX = contrastBoundingBox.x + contrastBoundingBox.width / 2;
const contrastMidY = contrastBoundingBox.y + contrastBoundingBox.height / 2;
await page.locator(contrastSlider).hover({trial: true});
await page.mouse.down();
await page.mouse.move(contrastBoundingBox.x + contrastBoundingBox.width, contrastMidY);
await assertBackgroundImageContrast(page, '500');
await page.mouse.move(contrastBoundingBox.x, contrastMidY);
await assertBackgroundImageContrast(page, '0');
await page.mouse.move(contrastMidX, contrastMidY);
await assertBackgroundImageContrast(page, '250');
await page.mouse.up();
}
/**
* Gets the filter:brightness value of the current background-image and
* asserts against an expected value
* @param {import('@playwright/test').Page} page
* @param {String} expected The expected brightness value
*/
async function assertBackgroundImageBrightness(page, expected) {
const backgroundImage = page.locator('.c-imagery__main-image__background-image');
// Get the brightness filter value (i.e: filter: brightness(500%) => "500")
const actual = await backgroundImage.evaluate((el) => {
return el.style.filter.match(/brightness\((\d{1,3})%\)/)[1];
});
expect(actual).toBe(expected);
}
/**
* Gets the filter:contrast value of the current background-image and
* asserts against an expected value
* @param {import('@playwright/test').Page} page
* @param {String} expected The expected contrast value
*/
async function assertBackgroundImageContrast(page, expected) {
const backgroundImage = page.locator('.c-imagery__main-image__background-image');
// Get the contrast filter value (i.e: filter: contrast(500%) => "500")
const actual = await backgroundImage.evaluate((el) => {
return el.style.filter.match(/contrast\((\d{1,3})%\)/)[1];
});
expect(actual).toBe(expected);
}
/**
* @param {import('@playwright/test').Page} page
*/
async function assertBackgroundImageUrlFromBackgroundCss(page) {
const backgroundImage = page.locator('.c-imagery__main-image__background-image');
let backgroundImageUrl = await backgroundImage.evaluate((el) => {
return window.getComputedStyle(el).getPropertyValue('background-image').match(/url\(([^)]+)\)/)[1];
});
let backgroundImageUrl1 = backgroundImageUrl.slice(1, -1); //forgive me, padre
console.log('backgroundImageUrl1 ' + backgroundImageUrl1);
let backgroundImageUrl2;
await expect.poll(async () => {
// Verify next image has updated
let backgroundImageUrlNext = await backgroundImage.evaluate((el) => {
return window.getComputedStyle(el).getPropertyValue('background-image').match(/url\(([^)]+)\)/)[1];
});
backgroundImageUrl2 = backgroundImageUrlNext.slice(1, -1); //forgive me, padre
return backgroundImageUrl2;
}, {
message: "verify next image has updated",
timeout: 6 * 1000
}).not.toBe(backgroundImageUrl1);
console.log('backgroundImageUrl2 ' + backgroundImageUrl2);
}
/**
* @param {import('@playwright/test').Page} page
*/
async function panZoomAndAssertImageProperties(page) {
const panHotkey = process.platform === 'linux' ? ['Control', 'Alt'] : ['Alt'];
const expectedAltText = process.platform === 'linux' ? 'Ctrl+Alt drag to pan' : 'Alt drag to pan';
const imageryHintsText = await page.locator('.c-imagery__hints').innerText();
expect(expectedAltText).toEqual(imageryHintsText);
const zoomedBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;
const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
// Pan right
await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
await page.mouse.down();
await page.mouse.move(imageCenterX - 200, imageCenterY, 10);
await page.mouse.up();
await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
const afterRightPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
expect(zoomedBoundingBox.x).toBeGreaterThan(afterRightPanBoundingBox.x);
// Pan left
await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
await page.mouse.down();
await page.mouse.move(imageCenterX, imageCenterY, 10);
await page.mouse.up();
await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
const afterLeftPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
expect(afterRightPanBoundingBox.x).toBeLessThan(afterLeftPanBoundingBox.x);
// Pan up
await page.mouse.move(imageCenterX, imageCenterY);
await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
await page.mouse.down();
await page.mouse.move(imageCenterX, imageCenterY + 200, 10);
await page.mouse.up();
await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
const afterUpPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
expect(afterUpPanBoundingBox.y).toBeGreaterThanOrEqual(afterLeftPanBoundingBox.y);
// Pan down
await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
await page.mouse.down();
await page.mouse.move(imageCenterX, imageCenterY - 200, 10);
await page.mouse.up();
await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
const afterDownPanBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
expect(afterDownPanBoundingBox.y).toBeLessThanOrEqual(afterUpPanBoundingBox.y);
}
/**
* @param {import('@playwright/test').Page} page
*/
async function mouseZoomIn(page) {
// Zoom in
const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox();
await page.locator(backgroundImageSelector).hover({trial: true});
const deltaYStep = 100; // equivalent to 1x zoom
await page.mouse.wheel(0, deltaYStep * 2);
const zoomedBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;
const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
// center the mouse pointer
await page.mouse.move(imageCenterX, imageCenterY);
// Wait for zoom animation to finish
await page.locator(backgroundImageSelector).hover({trial: true});
const imageMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox();
expect(imageMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height);
expect(imageMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width);
}

View File

@@ -27,15 +27,112 @@ const path = require('path');
const TEST_TEXT = 'Testing text for entries.';
const TEST_TEXT_NAME = 'Test Page';
const CUSTOM_NAME = 'CUSTOM_NAME';
const COMMIT_BUTTON_TEXT = 'button:has-text("Commit Entries")';
const SINE_WAVE_GENERATOR = 'text=Unnamed Sine Wave Generator';
const NOTEBOOK_DROP_AREA = '.c-notebook__drag-area';
test.describe('Restricted Notebook', () => {
/**
* @param {import('@playwright/test').Page} page
*/
async function startAndAddNotebookObject(page) {
// eslint-disable-next-line no-undef
await page.addInitScript({ path: path.join(__dirname, 'addInitRestrictedNotebook.js') });
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
//Click the Create button
await page.click('button:has-text("Create")');
// Click text=CUSTOME_NAME
await page.click(`text=${CUSTOM_NAME}`); // secondarily tests renamability also
// Click text=OK
await Promise.all([
page.waitForNavigation({waitUntil: 'networkidle'}),
page.click('text=OK')
]);
return;
}
/**
* @param {import('@playwright/test').Page} page
*/
async function enterTextEntry(page) {
// Click .c-notebook__drag-area
await page.locator(NOTEBOOK_DROP_AREA).click();
// enter text
await page.locator('div.c-ne__text').click();
await page.locator('div.c-ne__text').fill(TEST_TEXT);
await page.locator('div.c-ne__text').press('Enter');
return;
}
/**
* @param {import('@playwright/test').Page} page
*/
async function dragAndDropEmbed(page) {
// Click button:has-text("Create")
await page.locator('button:has-text("Create")').click();
// Click li:has-text("Sine Wave Generator")
await page.locator('li:has-text("Sine Wave Generator")').click();
// Click form[name="mctForm"] >> text=My Items
await page.locator('form[name="mctForm"] >> text=My Items').click();
// Click text=OK
await page.locator('text=OK').click();
// Click text=Open MCT My Items >> span >> nth=3
await page.locator('text=Open MCT My Items >> span').nth(3).click();
// Click text=Unnamed CUSTOM_NAME
await Promise.all([
page.waitForNavigation(),
page.locator('text=Unnamed CUSTOM_NAME').click()
]);
await page.dragAndDrop(SINE_WAVE_GENERATOR, NOTEBOOK_DROP_AREA);
return;
}
/**
* @param {import('@playwright/test').Page} page
*/
async function lockPage(page) {
const commitButton = page.locator(COMMIT_BUTTON_TEXT);
await commitButton.click();
// confirmation dialog click
await page.locator('text=Lock Page').click();
// waiting for mutation of locked page
await new Promise((resolve, reject) => {
setTimeout(resolve, 1000);
});
return;
}
/**
* @param {import('@playwright/test').Page} page
*/
async function openContextMenuRestrictedNotebook(page) {
// Click text=Open MCT My Items (This expands the My Items folder to show it's chilren in the tree)
await page.locator('text=Open MCT My Items >> span').nth(3).click();
// Click a:has-text("Unnamed CUSTOM_NAME")
await page.locator(`a:has-text("Unnamed ${CUSTOM_NAME}")`).click({
button: 'right'
});
return;
}
test.describe('Restricted Notebook @addInit', () => {
test.beforeEach(async ({ page }) => {
await startAndAddRestrictedNotebookObject(page);
await startAndAddNotebookObject(page);
});
test('Can be renamed @addInit', async ({ page }) => {
await expect(page.locator('.l-browse-bar__object-name')).toContainText(`Unnamed ${CUSTOM_NAME}`);
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 }) => {
@@ -49,26 +146,24 @@ test.describe('Restricted Notebook', () => {
// notbook tree object exists
expect.soft(await restrictedNotebookTreeObject.count()).toEqual(1);
// Click Remove Text
// Click text=Remove
await page.locator('text=Remove').click();
// Click 'OK' on confirmation window and wait for save banner to appear
// Click text=OK
await Promise.all([
page.waitForNavigation(),
page.locator('text=OK').click(),
page.waitForSelector('.c-message-banner__message')
page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine?tc.mode=fixed&tc.startBound=1653671067340&tc.endBound=1653672867340&tc.timeSystem=utc&view=grid' }*/),
page.locator('text=OK').click()
]);
// has been deleted
expect(await restrictedNotebookTreeObject.count()).toEqual(0);
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(await commitButton.count()).toEqual(1);
const commitButton = page.locator(COMMIT_BUTTON_TEXT);
expect.soft(await commitButton.count()).toEqual(1);
});
});
@@ -76,21 +171,15 @@ test.describe('Restricted Notebook', () => {
test.describe('Restricted Notebook with at least one entry and with the page locked @addInit', () => {
test.beforeEach(async ({ page }) => {
await startAndAddRestrictedNotebookObject(page);
await startAndAddNotebookObject(page);
await enterTextEntry(page);
await lockPage(page);
// FIXME: Give ample time for the mutation to happen
// https://github.com/nasa/openmct/issues/5409
// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(1 * 1000);
// open sidebar
await page.locator('button.c-notebook__toggle-nav-button').click();
});
test('Locked page should now be in a locked state @addInit', async ({ page }, testInfo) => {
test.fixme(testInfo.project === 'chrome-beta', "Test is unreliable on chrome-beta");
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);
@@ -101,9 +190,11 @@ test.describe('Restricted Notebook with at least one entry and with the page loc
// no way to remove a restricted notebook with a locked page
await openContextMenuRestrictedNotebook(page);
const menuOptions = page.locator('.c-menu ul');
await expect(menuOptions).not.toContainText('Remove');
await expect.soft(menuOptions).not.toContainText('Remove');
});
test('Can still: add page, rename, add entry, delete unlocked pages @addInit', async ({ page }) => {
@@ -127,7 +218,7 @@ test.describe('Restricted Notebook with at least one entry and with the page loc
await enterTextEntry(page);
// expect new page to be lockable
const commitButton = page.locator('BUTTON:HAS-TEXT("COMMIT ENTRIES")');
const commitButton = page.locator(COMMIT_BUTTON_TEXT);
expect.soft(await commitButton.count()).toEqual(1);
// Click text=Unnamed PageTest Page >> button
@@ -142,14 +233,14 @@ test.describe('Restricted Notebook with at least one entry and with the page loc
// deleted page, should no longer exist
const deletedPageElement = page.locator(`text=${TEST_TEXT_NAME}`);
expect(await deletedPageElement.count()).toEqual(0);
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 startAndAddNotebookObject(page);
await dragAndDropEmbed(page);
});
@@ -158,7 +249,7 @@ test.describe('Restricted Notebook with a page locked and with an embed @addInit
await page.locator('.c-ne__embed__name .c-popup-menu-button').click(); // embed popup menu
const embedMenu = page.locator('body >> .c-menu');
await expect(embedMenu).toContainText('Remove This Embed');
await expect.soft(embedMenu).toContainText('Remove This Embed');
});
test('Disallows embeds to be deleted if page locked @addInit', async ({ page }) => {
@@ -167,89 +258,7 @@ test.describe('Restricted Notebook with a page locked and with an embed @addInit
await page.locator('.c-ne__embed__name .c-popup-menu-button').click(); // embed popup menu
const embedMenu = page.locator('body >> .c-menu');
await expect(embedMenu).not.toContainText('Remove This Embed');
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 page.locator('text=Lock Page').click();
}
/**
* @param {import('@playwright/test').Page} page
*/
async function openContextMenuRestrictedNotebook(page) {
const myItemsFolder = page.locator('text=Open MCT My Items >> span').nth(3);
const className = await myItemsFolder.getAttribute('class');
if (!className.includes('c-disclosure-triangle--expanded')) {
await myItemsFolder.click();
}
// Click a:has-text("Unnamed CUSTOM_NAME")
await page.locator(`a:has-text("Unnamed ${CUSTOM_NAME}")`).click({
button: 'right'
});
}

View File

@@ -29,57 +29,46 @@ 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
* @param {import('@playwright/test').Page} page
*/
async function createNotebookAndEntry(page, iterations = 1) {
async function createNotebookAndEntry(page) {
//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 li:has-text("Notebook")
await page.locator('li:has-text("Notebook")').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}`);
}
// 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();
}
/**
* 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);
async function createNotebookEntryAndTags(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();
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('text=Driving').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();
}
// 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();
// Click text=Science
await page.locator('text=Science').click();
}
test.describe('Tagging in Notebooks', () => {
@@ -148,58 +137,10 @@ test.describe('Tagging in Notebooks', () => {
await expect(page.locator('[aria-label="Search Result"]')).not.toContainText("Driving");
});
test('Tags persist across reload', async ({ page }) => {
await createNotebookEntryAndTags(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");
}
await page.reload();
await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Science");
await expect(page.locator('[aria-label="Notebook Entry"]')).toContainText("Driving");
});
});

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

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

View File

@@ -1,155 +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, browserName }) => {
test.fixme(browserName === 'firefox', 'Firefox failing due to console events being missed');
const errorLogs = [];
page.on("console", (message) => {
if (message.type() === 'warning' && message.text().includes('Missing domain object')) {
errorLogs.push(message.text());
}
});
//Make stacked plot
await makeStackedPlot(page);
//Gets local storage and deletes the last sine wave generator in the stacked plot
const localStorage = await page.evaluate(() => window.localStorage);
const parsedData = JSON.parse(localStorage.mct);
const keys = Object.keys(parsedData);
const lastKey = keys[keys.length - 1];
delete parsedData[lastKey];
//Sets local storage with missing object
await page.evaluate(
`window.localStorage.setItem('mct', '${JSON.stringify(parsedData)}')`
);
//Reloads page and clicks on stacked plot
await Promise.all([
page.reload(),
page.waitForLoadState('networkidle')
]);
//Verify Main section is there on load
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Stacked Plot');
await page.locator('text=Open MCT My Items >> span').nth(3).click();
await Promise.all([
page.waitForNavigation(),
page.locator('text=Unnamed Stacked Plot').first().click()
]);
//Check that there is only one stacked item plot with a plot, the missing one will be empty
await expect(page.locator(".c-plot--stacked-container:has(.gl-plot)")).toHaveCount(1);
//Verify that console.warn is thrown
expect(errorLogs).toHaveLength(1);
});
});
/**
* This is used the create a stacked plot object
* @private
*/
async function makeStackedPlot(page) {
// fresh page with time range from 2022-03-29 22:00:00.000Z to 2022-03-29 22:00:30.000Z
await page.goto('/', { waitUntil: 'networkidle' });
// create stacked plot
await page.locator('button.c-create-button').click();
await page.locator('li:has-text("Stacked Plot")').click();
await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle'}),
page.locator('text=OK').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
// save the stacked plot
await saveStackedPlot(page);
// create a sinewave generator
await createSineWaveGenerator(page);
// click on stacked plot
await page.locator('text=Open MCT My Items >> span').nth(3).click();
await Promise.all([
page.waitForNavigation(),
page.locator('text=Unnamed Stacked Plot').first().click()
]);
// create a second sinewave generator
await createSineWaveGenerator(page);
// click on stacked plot
await page.locator('text=Open MCT My Items >> span').nth(3).click();
await Promise.all([
page.waitForNavigation(),
page.locator('text=Unnamed Stacked Plot').first().click()
]);
}
/**
* This is used to save a stacked plot object
* @private
*/
async function saveStackedPlot(page) {
// save stacked plot
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
await Promise.all([
page.locator('text=Save and Finish Editing').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
//Wait until Save Banner is gone
await page.locator('.c-message-banner__close-button').click();
await page.waitForSelector('.c-message-banner__message', { state: 'detached' });
}
/**
* This is used to create a sine wave generator object
* @private
*/
async function createSineWaveGenerator(page) {
//Create sine wave generator
await page.locator('button.c-create-button').click();
await page.locator('li:has-text("Sine Wave Generator")').click();
await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle'}),
page.locator('text=OK').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
}

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

@@ -24,7 +24,7 @@ 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('unpauses when paused by button and user changes bounds', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5113'
@@ -71,34 +71,25 @@ test.describe('Telemetry Table', () => {
]);
// Click pause button
const pauseButton = page.locator('button.c-button.icon-pause');
const pauseButton = await page.locator('button.c-button.icon-pause');
await pauseButton.click();
const tableWrapper = page.locator('div.c-table-wrapper');
const tableWrapper = await page.locator('div.c-table-wrapper');
await expect(tableWrapper).toHaveClass(/is-paused/);
// Subtract 5 minutes from the current end bound datetime and set it
// Arbitrarily change end date to some time in the future
const endTimeInput = page.locator('input[type="text"].c-input--datetime').nth(1);
await endTimeInput.click();
let endDate = await endTimeInput.inputValue();
endDate = new Date(endDate);
endDate.setUTCMinutes(endDate.getUTCMinutes() - 5);
endDate = endDate.toISOString().replace(/T/, ' ');
endDate.setUTCDate(endDate.getUTCDate() + 1);
endDate = endDate.toISOString().replace(/T.*/, '');
await endTimeInput.fill('');
await endTimeInput.fill(endDate);
await page.keyboard.press('Enter');
await expect(tableWrapper).not.toHaveClass(/is-paused/);
// 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

@@ -140,7 +140,6 @@ async function triggerTimer3dotMenuAction(page, action) {
* @param {TimerViewAction} action
*/
async function triggerTimerViewAction(page, action) {
await page.locator('.c-timer').hover({trial: true});
const buttonTitle = buttonTitleFromAction(action);
await page.click(`button[title="${buttonTitle}"]`);
assertTimerStateAfterAction(page, action);

View File

@@ -0,0 +1,22 @@
{
"cookies": [],
"origins": [
{
"origin": "http://localhost:8080",
"localStorage": [
{
"name": "tcHistory",
"value": "{\"utc\":[{\"start\":1652301954635,\"end\":1652303754635}]}"
},
{
"name": "mct",
"value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"persisted\":1652303756008,\"modified\":1652303756007},\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\":{\"name\":\"Unnamed Condition Set\",\"type\":\"conditionSet\",\"identifier\":{\"key\":\"f64bea3b-58a7-4586-8c05-8b651e5f0bfd\",\"namespace\":\"\"},\"configuration\":{\"conditionTestData\":[],\"conditionCollection\":[{\"isDefault\":true,\"id\":\"73f2d9ae-d1f3-4561-b7fc-ecd5df557249\",\"configuration\":{\"name\":\"Default\",\"output\":\"Default\",\"trigger\":\"all\",\"criteria\":[]},\"summary\":\"Default condition\"}]},\"composition\":[],\"telemetry\":{},\"modified\":1652303755999,\"location\":\"mine\",\"persisted\":1652303756002}}"
},
{
"name": "mct-tree-expanded",
"value": "[]"
}
]
}
]
}

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")');
// Verify that Create Folder appears in the dropdown
await expect(page.locator(':nth-match(:text("Folder"), 2)')).toBeEnabled();
});
test('Verify that My Items Tree appears @ipad', async ({ page }) => {
//Test.slow annotation is currently broken. Needs to be fixed in https://github.com/nasa/openmct/issues/5374
test.slow();
//Go to baseURL
await page.goto('/');
//My Items to be visible
await expect(page.locator('a:has-text("My Items")')).toBeEnabled();
const locator = page.locator(':nth-match(:text("Folder"), 2)');
await expect(locator).toBeEnabled();
});

View File

@@ -38,8 +38,6 @@ 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 }) => {
@@ -54,23 +52,21 @@ test.beforeEach(async ({ context }) => {
});
});
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') });
test('Visual - Default Gauge is correct @addInit', async ({ page }) => {
await page.addInitScript({ path: path.join(__dirname, '../plugins/gauge', './addInitGauge.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.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, 'Restricted Notebook with CUSTOM_NAME');
await percySnapshot(page, 'Default Gauge');
});

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.
*/
const { test } = require('../../fixtures.js');
const { expect } = require('@playwright/test');
const { test, expect } = require('@playwright/test');
const percySnapshot = require('@percy/playwright');
const path = require('path');
const sinon = require('sinon');
@@ -97,11 +96,7 @@ test('Visual - Default Condition Set', async ({ page }) => {
await percySnapshot(page, 'Default Condition Set');
});
test.fixme('Visual - Default Condition Widget', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5349'
});
test('Visual - Default Condition Widget', async ({ page }) => {
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
@@ -211,22 +206,3 @@ test('Visual - Display Layout Icon is correct', async ({ page }) => {
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

@@ -52,6 +52,9 @@ test('Generate Visual Test Data @localStorage', async ({ page, context }) => {
//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();
@@ -66,12 +69,18 @@ test('Generate Visual Test Data @localStorage', async ({ page, context }) => {
//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();

View File

@@ -31,7 +31,7 @@ const STATUSES = [{
iconClassPoll: "icon-status-poll-question-mark"
}, {
key: "GO",
label: "Go",
label: "GO",
iconClass: "icon-check",
iconClassPoll: "icon-status-poll-question-mark",
statusClass: "s-status-ok",
@@ -39,7 +39,7 @@ const STATUSES = [{
statusFgColor: "#000"
}, {
key: "MAYBE",
label: "Maybe",
label: "MAYBE",
iconClass: "icon-alert-triangle",
iconClassPoll: "icon-status-poll-question-mark",
statusClass: "s-status-warning",
@@ -47,7 +47,7 @@ const STATUSES = [{
statusFgColor: "#000"
}, {
key: "NO_GO",
label: "No go",
label: "NO GO",
iconClass: "icon-circle-slash",
iconClassPoll: "icon-status-poll-question-mark",
statusClass: "s-status-error",

View File

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

View File

@@ -7,7 +7,7 @@
"@braintree/sanitize-url": "6.0.0",
"@percy/cli": "1.2.1",
"@percy/playwright": "1.0.4",
"@playwright/test": "1.23.0",
"@playwright/test": "1.21.1",
"@types/eventemitter3": "^1.0.0",
"@types/jasmine": "^4.0.1",
"@types/karma": "^6.3.2",
@@ -16,7 +16,6 @@
"babel-loader": "8.2.3",
"babel-plugin-istanbul": "6.1.1",
"comma-separated-values": "3.6.4",
"codecov":"3.8.3",
"copy-webpack-plugin": "11.0.0",
"cross-env": "7.0.3",
"css-loader": "4.0.0",
@@ -56,7 +55,6 @@
"moment-timezone": "0.5.34",
"node-bourbon": "4.2.3",
"painterro": "1.2.56",
"nyc":"15.1.0",
"plotly.js-basic-dist": "2.12.0",
"plotly.js-gl2d-dist": "2.12.0",
"printj": "1.3.1",
@@ -88,26 +86,21 @@
"build:coverage": "webpack --config webpack.coverage.js",
"build:watch": "webpack --config webpack.dev.js --watch",
"info": "npx envinfo --system --browsers --npmPackages --binaries --languages --markdown",
"test": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run",
"test:firefox": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run --browsers=FirefoxHeadless",
"test": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run",
"test:firefox": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run --browsers=FirefoxHeadless",
"test:debug": "cross-env NODE_ENV=debug karma start --no-single-run",
"test:e2e": "npx playwright test",
"test:e2e:ci": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome smoke branding default condition timeConductor clock exampleImagery persistence performance grandsearch notebook/tags",
"test:e2e: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:full": "npx playwright test --config=e2e/playwright-ci.config.js",
"test:perf": "npx playwright test --config=e2e/playwright-performance.config.js",
"test:watch": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --no-single-run",
"test:watch": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --no-single-run",
"jsdoc": "jsdoc -c jsdoc.json -R API.md -r -d dist/docs/api",
"update-about-dialog-copyright": "perl -pi -e 's/20\\d\\d\\-202\\d/2014\\-2022/gm' ./src/ui/layout/AboutDialog.vue",
"update-copyright-date": "npm run update-about-dialog-copyright && grep -lr --null --include=*.{js,scss,vue,ts,sh,html,md,frag} 'Copyright (c) 20' . | xargs -r0 perl -pi -e 's/Copyright\\s\\(c\\)\\s20\\d\\d\\-20\\d\\d/Copyright \\(c\\)\\ 2014\\-2022/gm'",
"otherdoc": "node docs/gendocs.js --in docs/src --out dist/docs --suppress-toc 'docs/src/index.md|docs/src/process/index.md'",
"docs": "npm run jsdoc ; npm run otherdoc",
"cov:e2e:report":"nyc report --reporter=lcovonly --report-dir=./coverage/e2e",
"cov:e2e:full:publish":"codecov --disable=gcov -f ./coverage/e2e/lcov.info -F e2e-full",
"cov:e2e:ci:publish":"codecov --disable=gcov -f ./coverage/e2e/lcov.info -F e2e-ci",
"cov:unit:publish":"codecov --disable=gcov -f ./coverage/unit/lcov.info -F unit",
"prepare": "npm run build:prod"
},
"repository": {

View File

@@ -96,167 +96,161 @@ define([
};
this.destroy = this.destroy.bind(this);
[
/**
* Tracks current selection state of the application.
* @private
*/
['selection', () => new Selection(this)],
/**
* Tracks current selection state of the application.
* @private
*/
this.selection = new Selection(this);
/**
* MCT's time conductor, which may be used to synchronize view contents
* for telemetry- or time-based views.
* @type {module:openmct.TimeConductor}
* @memberof module:openmct.MCT#
* @name conductor
*/
['time', () => new api.TimeAPI(this)],
/**
* MCT's time conductor, which may be used to synchronize view contents
* for telemetry- or time-based views.
* @type {module:openmct.TimeConductor}
* @memberof module:openmct.MCT#
* @name conductor
*/
this.time = new api.TimeAPI(this);
/**
* An interface for interacting with the composition of domain objects.
* The composition of a domain object is the list of other domain
* objects it "contains" (for instance, that should be displayed
* beneath it in the tree.)
*
* `composition` may be called as a function, in which case it acts
* as [`composition.get`]{@link module:openmct.CompositionAPI#get}.
*
* @type {module:openmct.CompositionAPI}
* @memberof module:openmct.MCT#
* @name composition
*/
['composition', () => new api.CompositionAPI(this)],
/**
* An interface for interacting with the composition of domain objects.
* The composition of a domain object is the list of other domain
* objects it "contains" (for instance, that should be displayed
* beneath it in the tree.)
*
* `composition` may be called as a function, in which case it acts
* as [`composition.get`]{@link module:openmct.CompositionAPI#get}.
*
* @type {module:openmct.CompositionAPI}
* @memberof module:openmct.MCT#
* @name composition
*/
this.composition = new api.CompositionAPI(this);
/**
* Registry for views of domain objects which should appear in the
* main viewing area.
*
* @type {module:openmct.ViewRegistry}
* @memberof module:openmct.MCT#
* @name objectViews
*/
['objectViews', () => new ViewRegistry()],
/**
* Registry for views of domain objects which should appear in the
* main viewing area.
*
* @type {module:openmct.ViewRegistry}
* @memberof module:openmct.MCT#
* @name objectViews
*/
this.objectViews = new ViewRegistry();
/**
* Registry for views which should appear in the Inspector area.
* These views will be chosen based on the selection state.
*
* @type {module:openmct.InspectorViewRegistry}
* @memberof module:openmct.MCT#
* @name inspectorViews
*/
['inspectorViews', () => new InspectorViewRegistry()],
/**
* Registry for views which should appear in the Inspector area.
* These views will be chosen based on the selection state.
*
* @type {module:openmct.InspectorViewRegistry}
* @memberof module:openmct.MCT#
* @name inspectorViews
*/
this.inspectorViews = new InspectorViewRegistry();
/**
* Registry for views which should appear in Edit Properties
* dialogs, and similar user interface elements used for
* modifying domain objects external to its regular views.
*
* @type {module:openmct.ViewRegistry}
* @memberof module:openmct.MCT#
* @name propertyEditors
*/
['propertyEditors', () => new ViewRegistry()],
/**
* Registry for views which should appear in Edit Properties
* dialogs, and similar user interface elements used for
* modifying domain objects external to its regular views.
*
* @type {module:openmct.ViewRegistry}
* @memberof module:openmct.MCT#
* @name propertyEditors
*/
this.propertyEditors = new ViewRegistry();
/**
* Registry for views which should appear in the toolbar area while
* editing. These views will be chosen based on the selection state.
*
* @type {module:openmct.ToolbarRegistry}
* @memberof module:openmct.MCT#
* @name toolbars
*/
['toolbars', () => new ToolbarRegistry()],
/**
* Registry for views which should appear in the status indicator area.
* @type {module:openmct.ViewRegistry}
* @memberof module:openmct.MCT#
* @name indicators
*/
this.indicators = new ViewRegistry();
/**
* Registry for domain object types which may exist within this
* instance of Open MCT.
*
* @type {module:openmct.TypeRegistry}
* @memberof module:openmct.MCT#
* @name types
*/
['types', () => new api.TypeRegistry()],
/**
* Registry for views which should appear in the toolbar area while
* editing. These views will be chosen based on the selection state.
*
* @type {module:openmct.ToolbarRegistry}
* @memberof module:openmct.MCT#
* @name toolbars
*/
this.toolbars = new ToolbarRegistry();
/**
* An interface for interacting with domain objects and the domain
* object hierarchy.
*
* @type {module:openmct.ObjectAPI}
* @memberof module:openmct.MCT#
* @name objects
*/
['objects', () => new api.ObjectAPI.default(this.types, this)],
/**
* Registry for domain object types which may exist within this
* instance of Open MCT.
*
* @type {module:openmct.TypeRegistry}
* @memberof module:openmct.MCT#
* @name types
*/
this.types = new api.TypeRegistry();
/**
* An interface for retrieving and interpreting telemetry data associated
* with a domain object.
*
* @type {module:openmct.TelemetryAPI}
* @memberof module:openmct.MCT#
* @name telemetry
*/
['telemetry', () => new api.TelemetryAPI.default(this)],
/**
* An interface for interacting with domain objects and the domain
* object hierarchy.
*
* @type {module:openmct.ObjectAPI}
* @memberof module:openmct.MCT#
* @name objects
*/
this.objects = new api.ObjectAPI.default(this.types, this);
/**
* An interface for creating new indicators and changing them dynamically.
*
* @type {module:openmct.IndicatorAPI}
* @memberof module:openmct.MCT#
* @name indicators
*/
['indicators', () => new api.IndicatorAPI(this)],
/**
* An interface for retrieving and interpreting telemetry data associated
* with a domain object.
*
* @type {module:openmct.TelemetryAPI}
* @memberof module:openmct.MCT#
* @name telemetry
*/
this.telemetry = new api.TelemetryAPI(this);
/**
* MCT's user awareness management, to enable user and
* role specific functionality.
* @type {module:openmct.UserAPI}
* @memberof module:openmct.MCT#
* @name user
*/
['user', () => new api.UserAPI(this)],
/**
* An interface for creating new indicators and changing them dynamically.
*
* @type {module:openmct.IndicatorAPI}
* @memberof module:openmct.MCT#
* @name indicators
*/
this.indicators = new api.IndicatorAPI(this);
['notifications', () => new api.NotificationAPI()],
/**
* MCT's user awareness management, to enable user and
* role specific functionality.
* @type {module:openmct.UserAPI}
* @memberof module:openmct.MCT#
* @name user
*/
this.user = new api.UserAPI(this);
['editor', () => new api.EditorAPI.default(this)],
this.notifications = new api.NotificationAPI();
['overlays', () => new OverlayAPI.default()],
this.editor = new api.EditorAPI.default(this);
['menus', () => new api.MenuAPI(this)],
this.overlays = new OverlayAPI.default();
['actions', () => new api.ActionsAPI(this)],
this.menus = new api.MenuAPI(this);
['status', () => new api.StatusAPI(this)],
this.actions = new api.ActionsAPI(this);
['priority', () => api.PriorityAPI],
this.status = new api.StatusAPI(this);
['router', () => new ApplicationRouter(this)],
this.priority = api.PriorityAPI;
['faults', () => new api.FaultManagementAPI.default(this)],
this.router = new ApplicationRouter(this);
this.faults = new api.FaultManagementAPI.default(this);
this.forms = new api.FormsAPI.default(this);
['forms', () => new api.FormsAPI.default(this)],
this.branding = BrandingAPI.default;
['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
});
});
/**
* MCT's annotation API that enables
* human-created comments and categorization linked to data products
* @type {module:openmct.AnnotationAPI}
* @memberof module:openmct.MCT#
* @name annotation
*/
this.annotation = new api.AnnotationAPI(this);
// Plugins that are installed by default
this.install(this.plugins.Plot());
@@ -287,7 +281,6 @@ define([
this.install(this.plugins.ObjectInterceptors());
this.install(this.plugins.DeviceClassifier());
this.install(this.plugins.UserIndicator());
this.install(this.plugins.Gauge());
}
MCT.prototype = Object.create(EventEmitter.prototype);

View File

@@ -172,19 +172,17 @@ export default class AnnotationAPI extends EventEmitter {
name: contentText,
domainObject: targetDomainObject,
annotationType,
tags: [tag],
tags: [],
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;
existingAnnotation = await this.create(annotationCreationArguments);
}
const tagArray = [tag, ...existingAnnotation.tags];
this.openmct.objects.mutate(existingAnnotation, 'tags', tagArray);
return existingAnnotation;
}
removeAnnotationTag(existingAnnotation, tagToRemove) {

View File

@@ -230,15 +230,10 @@ export default class ObjectAPI {
return result;
}).catch((result) => {
console.warn(`Failed to retrieve ${keystring}:`, result);
this.openmct.notifications.error(`Failed to retrieve object ${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;
});
@@ -388,13 +383,7 @@ export default class ObjectAPI {
}
}
return result.catch((error) => {
if (error instanceof this.errors.Conflict) {
this.openmct.notifications.error(`Conflict detected while saving ${this.makeKeyString(domainObject.identifier)}`);
}
throw error;
});
return result;
}
/**

View File

@@ -20,18 +20,122 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import TelemetryCollection from './TelemetryCollection';
import TelemetryRequestInterceptorRegistry from './TelemetryRequestInterceptor';
import CustomStringFormatter from '../../plugins/displayLayout/CustomStringFormatter';
import TelemetryMetadataManager from './TelemetryMetadataManager';
import TelemetryValueFormatter from './TelemetryValueFormatter';
import DefaultMetadataProvider from './DefaultMetadataProvider';
import objectUtils from 'objectUtils';
import _ from 'lodash';
const { TelemetryCollection } = require("./TelemetryCollection");
export default class TelemetryAPI {
define([
'../../plugins/displayLayout/CustomStringFormatter',
'./TelemetryMetadataManager',
'./TelemetryValueFormatter',
'./DefaultMetadataProvider',
'objectUtils',
'lodash'
], function (
CustomStringFormatter,
TelemetryMetadataManager,
TelemetryValueFormatter,
DefaultMetadataProvider,
objectUtils,
_
) {
/**
* A LimitEvaluator may be used to detect when telemetry values
* have exceeded nominal conditions.
*
* @interface LimitEvaluator
* @memberof module:openmct.TelemetryAPI~
*/
constructor(openmct) {
/**
* Check for any limit violations associated with a telemetry datum.
* @method evaluate
* @param {*} datum the telemetry datum to evaluate
* @param {TelemetryProperty} the property to check for limit violations
* @memberof module:openmct.TelemetryAPI~LimitEvaluator
* @returns {module:openmct.TelemetryAPI~LimitViolation} metadata about
* the limit violation, or undefined if a value is within limits
*/
/**
* A violation of limits defined for a telemetry property.
* @typedef LimitViolation
* @memberof {module:openmct.TelemetryAPI~}
* @property {string} cssClass the class (or space-separated classes) to
* apply to display elements for values which violate this limit
* @property {string} name the human-readable name for the limit violation
*/
/**
* A TelemetryFormatter converts telemetry values for purposes of
* display as text.
*
* @interface TelemetryFormatter
* @memberof module:openmct.TelemetryAPI~
*/
/**
* Retrieve the 'key' from the datum and format it accordingly to
* telemetry metadata in domain object.
*
* @method format
* @memberof module:openmct.TelemetryAPI~TelemetryFormatter#
*/
/**
* Describes a property which would be found in a datum of telemetry
* associated with a particular domain object.
*
* @typedef TelemetryProperty
* @memberof module:openmct.TelemetryAPI~
* @property {string} key the name of the property in the datum which
* contains this telemetry value
* @property {string} name the human-readable name for this property
* @property {string} [units] the units associated with this property
* @property {boolean} [temporal] true if this property is a timestamp, or
* may be otherwise used to order telemetry in a time-like
* fashion; default is false
* @property {boolean} [numeric] true if the values for this property
* can be interpreted plainly as numbers; default is true
* @property {boolean} [enumerated] true if this property may have only
* certain specific values; default is false
* @property {string} [values] for enumerated states, an ordered list
* of possible values
*/
/**
* Describes and bounds requests for telemetry data.
*
* @typedef TelemetryRequest
* @memberof module:openmct.TelemetryAPI~
* @property {string} sort the key of the property to sort by. This may
* be prefixed with a "+" or a "-" sign to sort in ascending
* or descending order respectively. If no prefix is present,
* ascending order will be used.
* @property {*} start the lower bound for values of the sorting property
* @property {*} end the upper bound for values of the sorting property
* @property {string[]} strategies symbolic identifiers for strategies
* (such as `minmax`) which may be recognized by providers;
* these will be tried in order until an appropriate provider
* is found
*/
/**
* Provides telemetry data. To connect to new data sources, new
* TelemetryProvider implementations should be
* [registered]{@link module:openmct.TelemetryAPI#addProvider}.
*
* @interface TelemetryProvider
* @memberof module:openmct.TelemetryAPI~
*/
/**
* An interface for retrieving telemetry data associated with a domain
* object.
*
* @interface TelemetryAPI
* @augments module:openmct.TelemetryAPI~TelemetryProvider
* @memberof module:openmct
*/
function TelemetryAPI(openmct) {
this.openmct = openmct;
this.formatMapCache = new WeakMap();
@@ -44,14 +148,12 @@ export default class TelemetryAPI {
this.requestProviders = [];
this.subscriptionProviders = [];
this.valueFormatterCache = new WeakMap();
this.requestInterceptorRegistry = new TelemetryRequestInterceptorRegistry();
}
abortAllRequests() {
TelemetryAPI.prototype.abortAllRequests = function () {
this.requestAbortControllers.forEach((controller) => controller.abort());
this.requestAbortControllers.clear();
}
};
/**
* Return Custom String Formatter
@@ -60,9 +162,9 @@ export default class TelemetryAPI {
* @param {string} format custom formatter string (eg: %.4f, &lts etc.)
* @returns {CustomStringFormatter}
*/
customStringFormatter(valueMetadata, format) {
return new CustomStringFormatter(this.openmct, valueMetadata, format);
}
TelemetryAPI.prototype.customStringFormatter = function (valueMetadata, format) {
return new CustomStringFormatter.default(this.openmct, valueMetadata, format);
};
/**
* Return true if the given domainObject is a telemetry object. A telemetry
@@ -72,9 +174,9 @@ export default class TelemetryAPI {
* @param {module:openmct.DomainObject} domainObject
* @returns {boolean} true if the object is a telemetry object.
*/
isTelemetryObject(domainObject) {
TelemetryAPI.prototype.isTelemetryObject = function (domainObject) {
return Boolean(this.findMetadataProvider(domainObject));
}
};
/**
* Check if this provider can supply telemetry data associated with
@@ -86,10 +188,10 @@ export default class TelemetryAPI {
* @returns {boolean} true if telemetry can be provided
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
*/
canProvideTelemetry(domainObject) {
TelemetryAPI.prototype.canProvideTelemetry = function (domainObject) {
return Boolean(this.findSubscriptionProvider(domainObject))
|| Boolean(this.findRequestProvider(domainObject));
}
|| Boolean(this.findRequestProvider(domainObject));
};
/**
* Register a telemetry provider with the telemetry service. This
@@ -99,7 +201,7 @@ export default class TelemetryAPI {
* @param {module:openmct.TelemetryAPI~TelemetryProvider} provider the new
* telemetry provider
*/
addProvider(provider) {
TelemetryAPI.prototype.addProvider = function (provider) {
if (provider.supportsRequest) {
this.requestProviders.unshift(provider);
}
@@ -115,54 +217,54 @@ export default class TelemetryAPI {
if (provider.supportsLimits) {
this.limitProviders.unshift(provider);
}
}
};
/**
* @private
*/
findSubscriptionProvider() {
TelemetryAPI.prototype.findSubscriptionProvider = function () {
const args = Array.prototype.slice.apply(arguments);
function supportsDomainObject(provider) {
return provider.supportsSubscribe.apply(provider, args);
}
return this.subscriptionProviders.filter(supportsDomainObject)[0];
}
};
/**
* @private
*/
findRequestProvider(domainObject) {
TelemetryAPI.prototype.findRequestProvider = function (domainObject) {
const args = Array.prototype.slice.apply(arguments);
function supportsDomainObject(provider) {
return provider.supportsRequest.apply(provider, args);
}
return this.requestProviders.filter(supportsDomainObject)[0];
}
};
/**
* @private
*/
findMetadataProvider(domainObject) {
TelemetryAPI.prototype.findMetadataProvider = function (domainObject) {
return this.metadataProviders.filter(function (p) {
return p.supportsMetadata(domainObject);
})[0];
}
};
/**
* @private
*/
findLimitEvaluator(domainObject) {
TelemetryAPI.prototype.findLimitEvaluator = function (domainObject) {
return this.limitProviders.filter(function (p) {
return p.supportsLimits(domainObject);
})[0];
}
};
/**
* @private
*/
standardizeRequestOptions(options) {
TelemetryAPI.prototype.standardizeRequestOptions = function (options) {
if (!Object.prototype.hasOwnProperty.call(options, 'start')) {
options.start = this.openmct.time.bounds().start;
}
@@ -174,47 +276,7 @@ export default class TelemetryAPI {
if (!Object.prototype.hasOwnProperty.call(options, 'domain')) {
options.domain = this.openmct.time.timeSystem().key;
}
}
/**
* Register a request interceptor that transforms a request via module:openmct.TelemetryAPI.request
* The request will be modifyed when it is received and will be returned in it's modified state
* The request will be transformed only if the interceptor is applicable to that domain object as defined by the RequestInterceptorDef
*
* @param {module:openmct.RequestInterceptorDef} requestInterceptorDef the request interceptor definition to add
* @method addRequestInterceptor
* @memberof module:openmct.TelemetryRequestInterceptorRegistry#
*/
addRequestInterceptor(requestInterceptorDef) {
this.requestInterceptorRegistry.addInterceptor(requestInterceptorDef);
}
/**
* Retrieve the request interceptors for a given domain object.
* @private
*/
#getInterceptorsForRequest(identifier, request) {
return this.requestInterceptorRegistry.getInterceptors(identifier, request);
}
/**
* Invoke interceptors if applicable for a given domain object.
*/
async applyRequestInterceptors(domainObject, request) {
const interceptors = this.#getInterceptorsForRequest(domainObject.identifier, request);
if (interceptors.length === 0) {
return request;
}
let modifiedRequest = { ...request };
for (let interceptor of interceptors) {
modifiedRequest = await interceptor.invoke(modifiedRequest);
}
return modifiedRequest;
}
};
/**
* Request telemetry collection for a domain object.
@@ -230,13 +292,13 @@ export default class TelemetryAPI {
* options for this telemetry collection request
* @returns {TelemetryCollection} a TelemetryCollection instance
*/
requestCollection(domainObject, options = {}) {
TelemetryAPI.prototype.requestCollection = function (domainObject, options = {}) {
return new TelemetryCollection(
this.openmct,
domainObject,
options
);
}
};
/**
* Request historical telemetry for a domain object.
@@ -253,7 +315,7 @@ export default class TelemetryAPI {
* @returns {Promise.<object[]>} a promise for an array of
* telemetry data
*/
async request(domainObject) {
TelemetryAPI.prototype.request = async function (domainObject) {
if (this.noRequestProviderForAllObjects) {
return Promise.resolve([]);
}
@@ -268,7 +330,6 @@ export default class TelemetryAPI {
this.requestAbortControllers.add(abortController);
this.standardizeRequestOptions(arguments[1]);
const provider = this.findRequestProvider.apply(this, arguments);
if (!provider) {
this.requestAbortControllers.delete(abortController);
@@ -276,7 +337,8 @@ export default class TelemetryAPI {
return this.handleMissingRequestProvider(domainObject);
}
arguments[1] = await this.applyRequestInterceptors(domainObject, arguments[1]);
const timeContext = this.openmct.time.getContextForView();
await timeContext.hasInitialTick();
return provider.request.apply(provider, arguments)
.catch((rejected) => {
@@ -289,7 +351,7 @@ export default class TelemetryAPI {
}).finally(() => {
this.requestAbortControllers.delete(abortController);
});
}
};
/**
* Subscribe to realtime telemetry for a specific domain object.
@@ -305,7 +367,7 @@ export default class TelemetryAPI {
* @returns {Function} a function which may be called to terminate
* the subscription
*/
subscribe(domainObject, callback, options) {
TelemetryAPI.prototype.subscribe = function (domainObject, callback, options) {
const provider = this.findSubscriptionProvider(domainObject);
if (!this.subscribeCache) {
@@ -342,7 +404,7 @@ export default class TelemetryAPI {
delete this.subscribeCache[keyString];
}
}.bind(this);
}
};
/**
* Get telemetry metadata for a given domain object. Returns a telemetry
@@ -351,7 +413,7 @@ export default class TelemetryAPI {
*
* @returns {TelemetryMetadataManager}
*/
getMetadata(domainObject) {
TelemetryAPI.prototype.getMetadata = function (domainObject) {
if (!this.metadataCache.has(domainObject)) {
const metadataProvider = this.findMetadataProvider(domainObject);
if (!metadataProvider) {
@@ -367,14 +429,14 @@ export default class TelemetryAPI {
}
return this.metadataCache.get(domainObject);
}
};
/**
* Return an array of valueMetadatas that are common to all supplied
* telemetry objects and match the requested hints.
*
*/
commonValuesForHints(metadatas, hints) {
TelemetryAPI.prototype.commonValuesForHints = function (metadatas, hints) {
const options = metadatas.map(function (metadata) {
const values = metadata.valuesForHints(hints);
@@ -394,14 +456,14 @@ export default class TelemetryAPI {
});
return _.sortBy(options, sortKeys);
}
};
/**
* Get a value formatter for a given valueMetadata.
*
* @returns {TelemetryValueFormatter}
*/
getValueFormatter(valueMetadata) {
TelemetryAPI.prototype.getValueFormatter = function (valueMetadata) {
if (!this.valueFormatterCache.has(valueMetadata)) {
this.valueFormatterCache.set(
valueMetadata,
@@ -410,7 +472,7 @@ export default class TelemetryAPI {
}
return this.valueFormatterCache.get(valueMetadata);
}
};
/**
* Get a value formatter for a given key.
@@ -418,9 +480,9 @@ export default class TelemetryAPI {
*
* @returns {Format}
*/
getFormatter(key) {
TelemetryAPI.prototype.getFormatter = function (key) {
return this.formatters.get(key);
}
};
/**
* Get a format map of all value formatters for a given piece of telemetry
@@ -428,7 +490,7 @@ export default class TelemetryAPI {
*
* @returns {Object<String, {TelemetryValueFormatter}>}
*/
getFormatMap(metadata) {
TelemetryAPI.prototype.getFormatMap = function (metadata) {
if (!metadata) {
return {};
}
@@ -443,14 +505,14 @@ export default class TelemetryAPI {
}
return this.formatMapCache.get(metadata);
}
};
/**
* Error Handling: Missing Request provider
*
* @returns Promise
*/
handleMissingRequestProvider(domainObject) {
TelemetryAPI.prototype.handleMissingRequestProvider = function (domainObject) {
this.noRequestProviderForAllObjects = this.requestProviders.every(requestProvider => {
const supportsRequest = requestProvider.supportsRequest.apply(requestProvider, arguments);
const hasRequestProvider = Object.prototype.hasOwnProperty.call(requestProvider, 'request') && typeof requestProvider.request === 'function';
@@ -470,18 +532,18 @@ export default class TelemetryAPI {
}
this.openmct.notifications.error(message);
console.warn(detailMessage);
console.error(detailMessage);
return Promise.resolve([]);
}
};
/**
* Register a new telemetry data formatter.
* @param {Format} format the
*/
addFormat(format) {
TelemetryAPI.prototype.addFormat = function (format) {
this.formatters.set(format.key, format);
}
};
/**
* Get a limit evaluator for this domain object.
@@ -499,9 +561,9 @@ export default class TelemetryAPI {
* @method limitEvaluator
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
*/
limitEvaluator(domainObject) {
TelemetryAPI.prototype.limitEvaluator = function (domainObject) {
return this.getLimitEvaluator(domainObject);
}
};
/**
* Get a limits for this domain object.
@@ -519,9 +581,9 @@ export default class TelemetryAPI {
* @method limits
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
*/
limitDefinition(domainObject) {
TelemetryAPI.prototype.limitDefinition = function (domainObject) {
return this.getLimits(domainObject);
}
};
/**
* Get a limit evaluator for this domain object.
@@ -539,7 +601,7 @@ export default class TelemetryAPI {
* @method limitEvaluator
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
*/
getLimitEvaluator(domainObject) {
TelemetryAPI.prototype.getLimitEvaluator = function (domainObject) {
const provider = this.findLimitEvaluator(domainObject);
if (!provider) {
return {
@@ -548,7 +610,7 @@ export default class TelemetryAPI {
}
return provider.getLimitEvaluator(domainObject);
}
};
/**
* Get a limit definitions for this domain object.
@@ -577,7 +639,7 @@ export default class TelemetryAPI {
* supported colors are purple, red, orange, yellow and cyan
* @memberof module:openmct.TelemetryAPI~TelemetryProvider#
*/
getLimits(domainObject) {
TelemetryAPI.prototype.getLimits = function (domainObject) {
const provider = this.findLimitEvaluator(domainObject);
if (!provider || !provider.getLimits) {
return {
@@ -588,104 +650,7 @@ export default class TelemetryAPI {
}
return provider.getLimits(domainObject);
}
}
};
/**
* A LimitEvaluator may be used to detect when telemetry values
* have exceeded nominal conditions.
*
* @interface LimitEvaluator
* @memberof module:openmct.TelemetryAPI~
*/
/**
* Check for any limit violations associated with a telemetry datum.
* @method evaluate
* @param {*} datum the telemetry datum to evaluate
* @param {TelemetryProperty} the property to check for limit violations
* @memberof module:openmct.TelemetryAPI~LimitEvaluator
* @returns {module:openmct.TelemetryAPI~LimitViolation} metadata about
* the limit violation, or undefined if a value is within limits
*/
/**
* A violation of limits defined for a telemetry property.
* @typedef LimitViolation
* @memberof {module:openmct.TelemetryAPI~}
* @property {string} cssClass the class (or space-separated classes) to
* apply to display elements for values which violate this limit
* @property {string} name the human-readable name for the limit violation
*/
/**
* A TelemetryFormatter converts telemetry values for purposes of
* display as text.
*
* @interface TelemetryFormatter
* @memberof module:openmct.TelemetryAPI~
*/
/**
* Retrieve the 'key' from the datum and format it accordingly to
* telemetry metadata in domain object.
*
* @method format
* @memberof module:openmct.TelemetryAPI~TelemetryFormatter#
*/
/**
* Describes a property which would be found in a datum of telemetry
* associated with a particular domain object.
*
* @typedef TelemetryProperty
* @memberof module:openmct.TelemetryAPI~
* @property {string} key the name of the property in the datum which
* contains this telemetry value
* @property {string} name the human-readable name for this property
* @property {string} [units] the units associated with this property
* @property {boolean} [temporal] true if this property is a timestamp, or
* may be otherwise used to order telemetry in a time-like
* fashion; default is false
* @property {boolean} [numeric] true if the values for this property
* can be interpreted plainly as numbers; default is true
* @property {boolean} [enumerated] true if this property may have only
* certain specific values; default is false
* @property {string} [values] for enumerated states, an ordered list
* of possible values
*/
/**
* Describes and bounds requests for telemetry data.
*
* @typedef TelemetryRequest
* @memberof module:openmct.TelemetryAPI~
* @property {string} sort the key of the property to sort by. This may
* be prefixed with a "+" or a "-" sign to sort in ascending
* or descending order respectively. If no prefix is present,
* ascending order will be used.
* @property {*} start the lower bound for values of the sorting property
* @property {*} end the upper bound for values of the sorting property
* @property {string[]} strategies symbolic identifiers for strategies
* (such as `minmax`) which may be recognized by providers;
* these will be tried in order until an appropriate provider
* is found
*/
/**
* Provides telemetry data. To connect to new data sources, new
* TelemetryProvider implementations should be
* [registered]{@link module:openmct.TelemetryAPI#addProvider}.
*
* @interface TelemetryProvider
* @memberof module:openmct.TelemetryAPI~
*/
/**
* An interface for retrieving telemetry data associated with a domain
* object.
*
* @interface TelemetryAPI
* @augments module:openmct.TelemetryAPI~TelemetryProvider
* @memberof module:openmct
*/
return TelemetryAPI;
});

View File

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

View File

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

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

@@ -344,6 +344,18 @@ class TimeContext extends EventEmitter {
return this.activeClock;
}
hasInitialTick() {
const waitForInitialTick = (resolve) => {
if (this.activeClock === undefined || this.activeClock.isInitialized()) {
resolve(true);
} else {
setTimeout(() => waitForInitialTick(resolve), 100);
}
};
return new Promise(waitForInitialTick);
}
/**
* Update bounds based on provided time and current offsets
* @param {number} timestamp A time from which bounds will be calculated

View File

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

View File

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

View File

@@ -178,26 +178,6 @@ export default {
this.requestDataFor(telemetryObject);
this.subscribeToObject(telemetryObject);
},
setTrace(key, name, axisMetadata, xValues, yValues) {
let trace = {
key,
name: name,
x: xValues,
y: yValues,
xAxisMetadata: {},
yAxisMetadata: axisMetadata.yAxisMetadata,
type: this.domainObject.configuration.useBar ? 'bar' : 'scatter',
mode: 'lines',
line: {
shape: this.domainObject.configuration.useInterpolation
},
marker: {
color: this.domainObject.configuration.barStyles.series[key].color
},
hoverinfo: this.domainObject.configuration.useBar ? 'skip' : 'x+y'
};
this.addTrace(trace, key);
},
addTrace(trace, key) {
if (!this.trace.length) {
this.trace = this.trace.concat([trace]);
@@ -256,15 +236,7 @@ export default {
refreshData(bounds, isTick) {
if (!isTick) {
const telemetryObjects = Object.values(this.telemetryObjects);
telemetryObjects.forEach((telemetryObject) => {
//clear existing data
const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);
const axisMetadata = this.getAxisMetadata(telemetryObject);
this.setTrace(key, telemetryObject.name, axisMetadata, [], []);
//request new data
this.requestDataFor(telemetryObject);
this.subscribeToObject(telemetryObject);
});
telemetryObjects.forEach(this.requestDataFor);
}
},
removeAllSubscriptions() {
@@ -348,7 +320,25 @@ export default {
});
}
this.setTrace(key, telemetryObject.name, axisMetadata, xValues, yValues);
let trace = {
key,
name: telemetryObject.name,
x: xValues,
y: yValues,
xAxisMetadata: xAxisMetadata,
yAxisMetadata: axisMetadata.yAxisMetadata,
type: this.domainObject.configuration.useBar ? 'bar' : 'scatter',
mode: 'lines',
line: {
shape: this.domainObject.configuration.useInterpolation
},
marker: {
color: this.domainObject.configuration.barStyles.series[key].color
},
hoverinfo: this.domainObject.configuration.useBar ? 'skip' : 'x+y'
};
this.addTrace(trace, key);
},
isDataInTimeRange(datum, key, telemetryObject) {
const timeSystemKey = this.timeContext.timeSystem().key;

View File

@@ -66,15 +66,12 @@ export default function BarGraphViewProvider(openmct) {
}
};
},
template: '<bar-graph-view ref="graphComponent" :options="options"></bar-graph-view>'
template: '<bar-graph-view :options="options"></bar-graph-view>'
});
},
destroy: function () {
component.$destroy();
component = undefined;
},
onClearData() {
component.$refs.graphComponent.refreshData();
}
};
}

View File

@@ -281,11 +281,11 @@ export default {
this.xKeyOptions.push(
metadataValues.reduce((previousValue, currentValue) => {
return {
name: previousValue?.name ? `${previousValue.name}, ${currentValue.name}` : `${currentValue.name}`,
name: `${previousValue.name}, ${currentValue.name}`,
value: currentValue.key,
isArrayValue: currentValue.isArrayValue
};
}, {name: ''})
})
);
}
@@ -316,16 +316,11 @@ export default {
}
} else {
if (this.yKey === undefined) {
if (metadataValues.length && metadataArrayValues.length === 0) {
yKeyOptionIndex = this.yKeyOptions.findIndex((option, index) => index !== xKeyOptionIndex);
if (yKeyOptionIndex > -1) {
update = true;
this.yKey = 'none';
} else {
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.yKey = this.yKeyOptions[yKeyOptionIndex].value;
this.yKeyLabel = this.yKeyOptions[yKeyOptionIndex].name;
}
}
}
@@ -341,8 +336,6 @@ export default {
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) => {

View File

@@ -28,9 +28,9 @@ export default function () {
return function install(openmct) {
openmct.types.addType(BAR_GRAPH_KEY, {
key: BAR_GRAPH_KEY,
name: "Graph",
name: "Graph (Bar or Line)",
cssClass: "icon-bar-chart",
description: "Visualize data as a bar or line graph.",
description: "View data as a bar graph. Can be added to Display Layouts.",
creatable: true,
initialize: function (domainObject) {
domainObject.composition = [];

View File

@@ -367,26 +367,19 @@ describe("the plugin", function () {
type: "test-object",
name: "Test Object",
telemetry: {
values: [
{
key: "some-key",
source: "some-key",
name: "Some attribute",
format: "enum",
enumerations: [
{
value: 0,
string: "OFF"
},
{
value: 1,
string: "ON"
}
],
hints: {
range: 1
}
}]
values: [{
key: "some-key",
name: "Some attribute",
hints: {
domain: 1
}
}, {
key: "some-other-key",
name: "Another attribute",
hints: {
range: 1
}
}]
}
};
const composition = openmct.composition.get(parent);

View File

@@ -300,11 +300,8 @@ export default class ConditionManager extends EventEmitter {
return this.compositionLoad.then(() => {
let latestTimestamp;
let conditionResults = {};
let nextLegOptions = {...options};
delete nextLegOptions.onPartialResponse;
const conditionRequests = this.conditions
.map(condition => condition.requestLADConditionResult(nextLegOptions));
.map(condition => condition.requestLADConditionResult(options));
return Promise.all(conditionRequests)
.then((results) => {

View File

@@ -152,7 +152,7 @@ export default {
},
unit() {
let value = this.item.value;
let unit = this.metadata ? this.metadata.value(value).unit : '';
let unit = this.metadata.value(value).unit;
return unit;
},
@@ -280,7 +280,7 @@ export default {
this.limitEvaluator = this.openmct.telemetry.limitEvaluator(this.domainObject);
this.formats = this.openmct.telemetry.getFormatMap(this.metadata);
const valueMetadata = this.metadata ? this.metadata.value(this.item.value) : {};
const valueMetadata = this.metadata.value(this.item.value);
this.customStringformatter = this.openmct.telemetry.customStringFormatter(valueMetadata, this.item.format);
this.telemetryCollection = this.openmct.telemetry.requestCollection(this.domainObject, {

View File

@@ -21,25 +21,23 @@
*****************************************************************************/
<template>
<div class="c-fault-mgmt-item-header c-fault-mgmt__list-header c-fault-mgmt__list">
<div class="c-fault-mgmt-item-header c-fault-mgmt__checkbox">
<div class="c-fault-mgmt__list-header c-fault-mgmt__list">
<div class="c-fault-mgmt__checkbox">
<input
type="checkbox"
:checked="isSelectAll"
@input="selectAll"
>
</div>
<div class="c-fault-mgmt-item-header c-fault-mgmt__list-header-results c-fault-mgmt__list-severity">
{{ totalFaultsCount }} Results
</div>
<div class="c-fault-mgmt__list-header-content">
<div class="c-fault-mgmt__list-content">
<div class="c-fault-mgmt__list-header-results"> {{ totalFaultsCount }} Results </div>
<div class="c-fault-mgmt__list-content-right">
<div class="c-fault-mgmt-item-header c-fault-mgmt__list-header-tripVal">Trip Value</div>
<div class="c-fault-mgmt-item-header c-fault-mgmt__list-header-liveVal">Live Value</div>
<div class="c-fault-mgmt-item-header c-fault-mgmt__list-header-trigTime">Trigger Time</div>
<div class="c-fault-mgmt__list-header-tripVal c-fault-mgmt__list-trigVal">Trip Value</div>
<div class="c-fault-mgmt__list-header-liveVal c-fault-mgmt__list-curVal">Live Value</div>
<div class="c-fault-mgmt__list-header-trigTime c-fault-mgmt__list-trigTime">Trigger Time</div>
</div>
</div>
<div class=" c-fault-mgmt-item-header c-fault-mgmt__list-header-action-wrapper">
<div class="c-fault-mgmt__list-action-wrapper">
<div class="c-fault-mgmt__list-header-sortButton c-fault-mgmt__list-action-button">
<SelectField
class="c-fault-mgmt-viewButton"

View File

@@ -23,55 +23,52 @@
<template>
<div
class="c-fault-mgmt__list data-selectable"
:class="classesFromState"
:class="[
{'is-selected': isSelected},
{'is-unacknowledged': !fault.acknowledged},
{'is-shelved': fault.shelved}
]"
>
<div class="c-fault-mgmt-item c-fault-mgmt__list-checkbox">
<div class="c-fault-mgmt__checkbox">
<input
type="checkbox"
:checked="isSelected"
@input="toggleSelected"
>
</div>
<div class="c-fault-mgmt-item">
<div
class="c-fault-mgmt__list-severity"
:title="fault.severity"
:class="[
'is-severity-' + severity
]"
>
</div>
<div
class="c-fault-mgmt__list-severity"
:title="fault.severity"
:class="[
'is-severity-' + severity
]"
>
</div>
<div class="c-fault-mgmt-item c-fault-mgmt__list-content">
<div class="c-fault-mgmt-item c-fault-mgmt__list-pathname">
<div class="c-fault-mgmt__list-content">
<div class="c-fault-mgmt__list-pathname">
<div class="c-fault-mgmt__list-path">{{ fault.namespace }}</div>
<div class="c-fault-mgmt__list-faultname">{{ fault.name }}</div>
</div>
<div class="c-fault-mgmt__list-content-right">
<div class="c-fault-mgmt-item c-fault-mgmt__list-trigVal">
<div
class="c-fault-mgmt-item__value"
:class="tripValueClassname"
title="Trip Value"
>{{ fault.triggerValueInfo.value }}</div>
<div
class="c-fault-mgmt__list-trigVal"
:class="tripValueClassname"
title="Trip Value"
>{{ fault.triggerValueInfo.value }}</div>
<div
class="c-fault-mgmt__list-curVal"
:class="liveValueClassname"
title="Live Value"
>
{{ fault.currentValueInfo.value }}
</div>
<div class="c-fault-mgmt-item c-fault-mgmt__list-curVal">
<div
class="c-fault-mgmt-item__value"
:class="liveValueClassname"
title="Live Value"
>{{ fault.currentValueInfo.value }}</div>
</div>
<div class="c-fault-mgmt-item c-fault-mgmt__list-trigTime">
<div
class="c-fault-mgmt-item__value"
title="Last Trigger Time"
>{{ fault.triggerTime }}
</div>
<div
class="c-fault-mgmt__list-trigTime"
>{{ fault.triggerTime }}
</div>
</div>
</div>
<div class="c-fault-mgmt-item c-fault-mgmt__list-action-wrapper">
<div class="c-fault-mgmt__list-action-wrapper">
<button
class="c-fault-mgmt__list-action-button l-browse-bar__actions c-icon-button icon-3-dots"
title="Disposition Actions"
@@ -80,6 +77,7 @@
</div>
</div>
</template>
<script>
const RANGE_CONDITION_CLASS = {
@@ -108,36 +106,6 @@ export default {
}
},
computed: {
classesFromState() {
const exclusiveStates = [
{
className: 'is-shelved',
test: () => this.fault.shelved
},
{
className: 'is-unacknowledged',
test: () => !this.fault.acknowledged && !this.fault.shelved
},
{
className: 'is-acknowledged',
test: () => this.fault.acknowledged && !this.fault.shelved
}
];
const classes = [];
if (this.isSelected) {
classes.push('is-selected');
}
const matchingState = exclusiveStates.find(stateDefinition => stateDefinition.test());
if (matchingState !== undefined) {
classes.push(matchingState.className);
}
return classes;
},
liveValueClassname() {
const currentValueInfo = this.fault?.currentValueInfo;
if (!currentValueInfo || currentValueInfo.monitoringResult === 'IN_LIMITS') {
@@ -181,7 +149,7 @@ export default {
const menuItems = [
{
cssClass: 'icon-check',
cssClass: 'icon-bell',
isDisabled: this.fault.acknowledged,
name: 'Acknowledge',
description: '',

View File

@@ -35,31 +35,25 @@
@shelveSelected="toggleShelveSelected"
/>
<div class="c-faults-list-view-header-item-container-wrapper">
<div class="c-faults-list-view-header-item-container">
<FaultManagementListHeader
class="header"
:selected-faults="Object.values(selectedFaults)"
:total-faults-count="filteredFaultsList.length"
@selectAll="selectAll"
@sortChanged="sortChanged"
/>
<FaultManagementListHeader
class="header"
:selected-faults="Object.values(selectedFaults)"
:total-faults-count="filteredFaultsList.length"
@selectAll="selectAll"
@sortChanged="sortChanged"
/>
<div class="c-faults-list-view-item-body">
<template v-if="filteredFaultsList.length > 0">
<FaultManagementListItem
v-for="fault of filteredFaultsList"
:key="fault.id"
:fault="fault"
:is-selected="isSelected(fault)"
@toggleSelected="toggleSelected"
@acknowledgeSelected="toggleAcknowledgeSelected"
@shelveSelected="toggleShelveSelected"
/>
</template>
</div>
</div>
</div>
<template v-if="filteredFaultsList.length > 0">
<FaultManagementListItem
v-for="fault of filteredFaultsList"
:key="fault.id"
:fault="fault"
:is-selected="isSelected(fault)"
@toggleSelected="toggleSelected"
@acknowledgeSelected="toggleAcknowledgeSelected"
@shelveSelected="toggleShelveSelected"
/>
</template>
</div>
</template>
@@ -96,19 +90,17 @@ export default {
computed: {
filteredFaultsList() {
const filterName = FILTER_ITEMS[this.filterIndex];
let list = this.faultsList;
// Exclude shelved alarms from all views except the Shelved view
if (filterName !== 'Shelved') {
list = list.filter(fault => fault.shelved !== true);
let list = this.faultsList.filter(fault => !fault.shelved);
if (filterName === 'Acknowledged') {
list = this.faultsList.filter(fault => fault.acknowledged);
}
if (filterName === 'Acknowledged') {
list = list.filter(fault => fault.acknowledged);
} else if (filterName === 'Unacknowledged') {
list = list.filter(fault => !fault.acknowledged);
} else if (filterName === 'Shelved') {
list = list.filter(fault => fault.shelved);
if (filterName === 'Unacknowledged') {
list = this.faultsList.filter(fault => !fault.acknowledged);
}
if (filterName === 'Shelved') {
list = this.faultsList.filter(fault => fault.shelved);
}
if (this.searchTerm.length > 0) {
@@ -203,7 +195,7 @@ export default {
{
key: 'comment',
control: 'textarea',
name: 'Optional comment',
name: 'Comment',
pattern: '\\S+',
required: false,
cssClass: 'l-input-lg',
@@ -245,7 +237,7 @@ export default {
{
key: 'comment',
control: 'textarea',
name: 'Optional comment',
name: 'Comment',
pattern: '\\S+',
required: false,
cssClass: 'l-input-lg',
@@ -254,7 +246,7 @@ export default {
{
key: 'shelveDuration',
control: 'select',
name: 'Shelve duration',
name: 'Shelve Duration',
options: FAULT_MANAGEMENT_SHELVE_DURATIONS_IN_MS,
required: false,
cssClass: 'l-input-lg',

View File

@@ -32,7 +32,7 @@ export default function FaultManagementPlugin() {
name: 'Fault Management',
creatable: false,
description: 'Fault Management View',
cssClass: 'icon-bell'
cssClass: 'icon-telemetry'
});
openmct.objectViews.addProvider(new FaultManagementViewProvider(openmct));

View File

@@ -23,7 +23,7 @@
<template>
<div class="c-fault-mgmt__toolbar">
<button
class="c-icon-button icon-check"
class="c-icon-button icon-bell"
title="Acknowledge selected faults"
:disabled="disableAcknowledge"
@click="acknowledgeSelected"

View File

@@ -21,13 +21,14 @@
*****************************************************************************/
<template>
<FaultManagementListView
:faults-list="faultsList"
/>
<div class="c-fault-mgmt">
<FaultManagementListView
:faults-list="faultsList"
/>
</div>
</template>
<script>
import FaultManagementListView from './FaultManagementListView.vue';
import { FAULT_MANAGEMENT_ALARMS, FAULT_MANAGEMENT_GLOBAL_ALARMS } from './constants';

View File

@@ -19,250 +19,216 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
$colorFaultItemFg: $colorBodyFg;
$colorFaultItemFgEmphasis: $colorBodyFgEm;
$colorFaultItemBg: pullForward($colorBodyBg, 5%);
/*********************************************** FAULT PROPERTIES */
.is-severity-critical{
@include glyphBefore($glyph-icon-alert-triangle);
color: $colorStatusError;
}
.is-severity-warning{
@include glyphBefore($glyph-icon-alert-rect);
color: $colorStatusAlert;
}
.is-severity-watch{
@include glyphBefore($glyph-icon-info);
color: $colorCommand;
}
.is-unacknowledged{
.c-fault-mgmt__list-severity{
@include pulse($animName: severityAnim, $dur: 200ms);
}
}
.is-selected {
background: $colorSelectedBg;
}
.is-shelved{
.c-fault-mgmt__list-content{
opacity: 50% !important;
font-style: italic;
}
.c-fault-mgmt__list-severity{
@include pulse($animName: shelvedAnim, $dur: 0ms);
}
}
/*********************************************** SEARCH */
.c-fault-mgmt__search-row {
.c-fault-mgmt__search-row{
display: flex;
align-items: center;
flex: 0 0 auto;
> * + * {
margin-left: 10px;
float: right;
}
}
.c-fault-mgmt-search {
.c-fault-mgmt-search{
width: 95%;
}
/*********************************************** TOOLBAR */
.c-fault-mgmt__toolbar {
display: flex;
.c-fault-mgmt__toolbar{
display: flex;
justify-content: center;
flex: 0 0 auto;
> * + * {
margin-left: $interiorMargin;
> * {
font-size: 1.25em;
}
}
/*********************************************** LIST VIEW */
.c-faults-list-view {
.c-faults-list-view {
display: flex;
flex-direction: column;
height: 100%;
> * + * {
margin-top: $interiorMargin;
margin-top: $interiorMargin;
}
}
.c-faults-list-view-header-item-container {
display: grid;
width: 100%;
grid-template-columns: max-content max-content repeat(5,minmax(max-content, 20%)) max-content;
grid-row-gap: $interiorMargin;
&-wrapper {
flex: 1 1 auto;
padding-right: $interiorMargin; // Fend of from scrollbar
overflow-y: auto;
}
/*********************************************** FAULT ITEM */
.c-fault-mgmt__list{
background: rgba($colorBodyFg, 0.1);
margin-bottom: 5px;
padding: 4px;
display: flex;
align-items: center;
.--width-less-than-600 & {
grid-template-columns: max-content max-content 1fr 1fr max-content;
}
}
.c-faults-list-view-item-body {
display: contents;
}
/*********************************************** LIST */
.c-fault-mgmt__list {
display: contents;
color: $colorFaultItemFg;
&-checkbox{
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
}
&-severity {
font-size: 2em;
&.is-severity-critical {
@include glyphBefore($glyph-icon-alert-triangle);
color: $colorStatusError;
}
&.is-severity-warning {
@include glyphBefore($glyph-icon-alert-rect);
color: $colorStatusAlert;
}
&.is-severity-watch {
@include glyphBefore($glyph-icon-info);
color: $colorCommand;
}
}
&-content {
display: contents;
.--width-less-than-600 & {
display: flex;
flex-wrap: wrap;
grid-column: span 2;
}
}
&-pathname {
padding-right: $interiorMarginLg;
overflow-wrap: anywhere;
min-width: 100px;
}
&-path {
font-size: .85em;
> * {
margin-left: $interiorMargin;
}
&-severity{
font-size: 2em;
margin-left: $interiorMarginLg;
}
&-pathname{
flex-wrap: wrap;
flex: 1 1 auto;
}
&-path{
font-size: .75em;
}
&-faultname{
font-weight: bold;
font-size: 1.3em;
margin-left: $interiorMargin;
}
&-content-right {
display: contents;
&-content{
display: flex;
flex-wrap: wrap;
flex: 1 1 auto;
align-items: center;
}
&-trigTime {
grid-column: 6 / span 2;
&-content-right{
margin-left: auto;
display: flex;
flex-wrap: wrap;
}
&-action-wrapper {
text-align: right;
flex: 0 0 auto;
align-items: stretch;
&-trigVal, &-curVal, &-trigTime{
@include ellipsize;
border-radius: $controlCr;
padding: $interiorMargin;
width: 80px;
margin-right: $interiorMarginLg;
}
&-action-button {
&-trigVal {
@include isLimit();
background: rgba($colorBodyFg, 0.25);
}
&-curVal {
@include isLimit();
background: rgba($colorBodyFg, 0.25);
&-alert{
background: $colorWarningHi;
}
}
&-trigTime{
width: auto;
}
&-action-wrapper{
display: flex;
align-content: right;
width: 100px;
}
&-action-button{
flex: 0 0 auto;
margin-left: auto;
text-align: right;
}
// STATES
&.is-unacknowledged {
color: $colorFaultItemFgEmphasis;
.c-fault-mgmt__list-severity {
@include pulse($animName: severityAnim, $dur: 200ms);
}
}
&.is-acknowledged,
&.is-shelved {
.c-fault-mgmt__list-severity {
&:before {
opacity: 60%;
//font-size: 1.5em;
}
&:after {
color: $colorFaultItemFgEmphasis;
display: block;
font-family: symbolsfont;
position: absolute;
//text-shadow: black 0 0 2px;
right: -3px;
bottom: -3px;
transform-origin: right bottom;
transform: scale(0.6);
}
}
}
&.is-shelved {
.c-fault-mgmt__list-pathname {
font-style: italic;
}
}
&.is-acknowledged .c-fault-mgmt__list-severity:after {
content: $glyph-icon-check;
}
&.is-shelved .c-fault-mgmt__list-severity:after {
content: $glyph-icon-timer;
justify-content: right;
}
}
/*********************************************** LIST HEADER */
.c-fault-mgmt__list-header {
display: contents;
.c-fault-mgmt__list-header{
display: flex;
background: rgba($colorBodyFg, .23);
border-radius: $controlCr;
align-items: center;
* {
margin: 0px;
border-radius: 0px;
&-tripVal, &-liveVal, &-trigTime{
background: none;
}
.--width-less-than-600 & {
.c-fault-mgmt__list-content-right {
display:none;
}
&-trigTime{
width: 160px;
}
&-sortButton{
flex: 0 0 auto;
margin-left: auto;
justify-content: right;
display: flex;
align-content: right;
width: 100px;
}
&-content {
display: contents;
}
}
&-results {
grid-column: 2 / span 2;
font-size: 1em;
height: auto;
}
.is-severity-critical{
@include glyphBefore($glyph-icon-alert-triangle);
color: $colorStatusError;
}
&-action-wrapper {
grid-column: 7 / span 2;
.is-severity-warning{
@include glyphBefore($glyph-icon-alert-rect);
color: $colorStatusAlert;
}
.--width-less-than-600 & {
grid-column: 4 / span 2;
}
.is-severity-watch{
@include glyphBefore($glyph-icon-info);
color: $colorCommand;
}
.is-unacknowledged{
.c-fault-mgmt__list-severity{
@include pulse($animName: severityAnim, $dur: 200ms);
}
}
/*********************************************** GRID ITEM */
.c-fault-mgmt-item {
$p: $interiorMargin;
padding: $p;
background: $colorFaultItemBg;
white-space: nowrap;
.is-selected {
background: $colorSelectedBg;
}
&-header {
$c: $colorBodyBg;
background: $c;
border-bottom: 5px solid $c; // Creates illusion of "space" beneath header
min-height: 30px; // Needed to align cells
padding: $p;
position: sticky;
top: 0;
z-index: 2;
.is-shelved{
.c-fault-mgmt__list-content{
opacity: 60% !important;
font-style: italic;
}
&__value {
@include isLimit();
background: rgba($colorBodyFg, 0.1);
padding: $p;
border-radius: $controlCr;
display: inline-flex;
}
.is-selected & {
background: $colorSelectedBg;
.c-fault-mgmt__list-severity{
@include pulse($animName: shelvedAnim, $dur: 0ms);
}
}

View File

@@ -169,7 +169,6 @@
</g>
<g class="c-dial__text">
<text
v-if="displayUnits"
x="50%"
y="70%"
text-anchor="middle"

View File

@@ -40,7 +40,7 @@
<div class="c-form__row">
<span class="req-indicator req">
</span>
<label>Minimum value</label>
<label>Range minimum value</label>
<input
ref="min"
v-model.number="min"
@@ -53,7 +53,7 @@
<div class="c-form__row">
<span class="req-indicator">
</span>
<label>Low limit</label>
<label>Range low limit</label>
<input
ref="limitLow"
v-model.number="limitLow"
@@ -64,26 +64,26 @@
</div>
<div class="c-form__row">
<span class="req-indicator">
<span class="req-indicator req">
</span>
<label>High limit</label>
<label>Range maximum value</label>
<input
ref="limitHigh"
v-model.number="limitHigh"
data-field-name="limitHigh"
ref="max"
v-model.number="max"
data-field-name="max"
type="number"
@input="onChange"
>
</div>
<div class="c-form__row">
<span class="req-indicator req">
<span class="req-indicator">
</span>
<label>Maximum value</label>
<label>Range high limit</label>
<input
ref="max"
v-model.number="max"
data-field-name="max"
ref="limitHigh"
v-model.number="limitHigh"
data-field-name="limitHigh"
type="number"
@input="onChange"
>

View File

@@ -210,10 +210,9 @@
border-radius: $controlCr;
display: flex;
align-items: flex-start;
flex-direction: row;
justify-content: space-between;
padding: $interiorMargin;
width: max-content;
width: min-content;
> * + * {
margin-left: $interiorMargin;
@@ -339,6 +338,7 @@
&__input {
display: flex;
align-items: center;
width: 100%;
&:before {
color: rgba($colorMenuFg, 0.5);
@@ -353,16 +353,13 @@
&--filters {
// Styles specific to the brightness and contrast controls
.c-image-controls {
&__controls {
width: 80px; // About the minimum this element can be; cannot size based on % due to markup structure
}
.c-image-controls {
&__sliders {
display: flex;
flex: 1 1 auto;
flex-direction: column;
width: 100%;
min-width: 80px;
> * + * {
margin-top: 11px;

View File

@@ -76,10 +76,7 @@ export default {
dataRemoved(dataToRemove) {
this.imageHistory = this.imageHistory.filter(existingDatum => {
const shouldKeep = dataToRemove.some(datumToRemove => {
const existingDatumTimestamp = this.parseTime(existingDatum);
const datumToRemoveTimestamp = this.parseTime(datumToRemove);
return (existingDatumTimestamp !== datumToRemoveTimestamp);
return (existingDatum.utc !== datumToRemove.utc);
});
return shouldKeep;

View File

@@ -373,30 +373,39 @@ describe("The Imagery View Layouts", () => {
return Vue.nextTick();
});
it("on mount should show the the most recent image", async () => {
it("on mount should show the the most recent image", () => {
//Looks like we need Vue.nextTick here so that computed properties settle down
await Vue.nextTick();
const imageInfo = getImageInfo(parent);
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 1].timeId)).not.toEqual(-1);
return Vue.nextTick(() => {
const imageInfo = getImageInfo(parent);
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 1].timeId)).not.toEqual(-1);
});
});
it("on mount should show the any image layers", async () => {
it("on mount should show the any image layers", (done) => {
//Looks like we need Vue.nextTick here so that computed properties settle down
await Vue.nextTick();
const layerEls = parent.querySelectorAll('.js-layer-image');
console.log(layerEls);
expect(layerEls.length).toEqual(1);
Vue.nextTick().then(() => {
Vue.nextTick(() => {
const layerEls = parent.querySelectorAll('.js-layer-image');
console.log(layerEls);
expect(layerEls.length).toEqual(1);
done();
});
});
});
it("should show the clicked thumbnail as the main image", async () => {
it("should show the clicked thumbnail as the main image", (done) => {
//Looks like we need Vue.nextTick here so that computed properties settle down
await Vue.nextTick();
const target = imageTelemetry[5].url;
parent.querySelectorAll(`img[src='${target}']`)[0].click();
await Vue.nextTick();
const imageInfo = getImageInfo(parent);
Vue.nextTick(() => {
const target = imageTelemetry[5].url;
parent.querySelectorAll(`img[src='${target}']`)[0].click();
Vue.nextTick(() => {
const imageInfo = getImageInfo(parent);
expect(imageInfo.url.indexOf(imageTelemetry[5].timeId)).not.toEqual(-1);
expect(imageInfo.url.indexOf(imageTelemetry[5].timeId)).not.toEqual(-1);
done();
});
});
});
xit("should show that an image is new", (done) => {
@@ -415,60 +424,71 @@ describe("The Imagery View Layouts", () => {
});
});
it("should show that an image is not new", async () => {
await Vue.nextTick();
const target = imageTelemetry[4].url;
parent.querySelectorAll(`img[src='${target}']`)[0].click();
it("should show that an image is not new", (done) => {
Vue.nextTick(() => {
const target = imageTelemetry[4].url;
parent.querySelectorAll(`img[src='${target}']`)[0].click();
await Vue.nextTick();
const imageIsNew = isNew(parent);
Vue.nextTick(() => {
const imageIsNew = isNew(parent);
expect(imageIsNew).toBeFalse();
expect(imageIsNew).toBeFalse();
done();
});
});
});
it("should navigate via arrow keys", async () => {
await Vue.nextTick();
const keyOpts = {
element: parent.querySelector('.c-imagery'),
key: 'ArrowLeft',
keyCode: 37,
type: 'keyup'
};
it("should navigate via arrow keys", (done) => {
Vue.nextTick(() => {
let keyOpts = {
element: parent.querySelector('.c-imagery'),
key: 'ArrowLeft',
keyCode: 37,
type: 'keyup'
};
simulateKeyEvent(keyOpts);
simulateKeyEvent(keyOpts);
await Vue.nextTick();
const imageInfo = getImageInfo(parent);
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 2].timeId)).not.toEqual(-1);
Vue.nextTick(() => {
const imageInfo = getImageInfo(parent);
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 2].timeId)).not.toEqual(-1);
done();
});
});
});
it("should navigate via numerous arrow keys", async () => {
await Vue.nextTick();
const element = parent.querySelector('.c-imagery');
const type = 'keyup';
const leftKeyOpts = {
element,
type,
key: 'ArrowLeft',
keyCode: 37
};
const rightKeyOpts = {
element,
type,
key: 'ArrowRight',
keyCode: 39
};
it("should navigate via numerous arrow keys", (done) => {
Vue.nextTick(() => {
let element = parent.querySelector('.c-imagery');
let type = 'keyup';
let leftKeyOpts = {
element,
type,
key: 'ArrowLeft',
keyCode: 37
};
let rightKeyOpts = {
element,
type,
key: 'ArrowRight',
keyCode: 39
};
// left thrice
simulateKeyEvent(leftKeyOpts);
simulateKeyEvent(leftKeyOpts);
simulateKeyEvent(leftKeyOpts);
// right once
simulateKeyEvent(rightKeyOpts);
// left thrice
simulateKeyEvent(leftKeyOpts);
simulateKeyEvent(leftKeyOpts);
simulateKeyEvent(leftKeyOpts);
// right once
simulateKeyEvent(rightKeyOpts);
await Vue.nextTick();
const imageInfo = getImageInfo(parent);
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 3].timeId)).not.toEqual(-1);
Vue.nextTick(() => {
const imageInfo = getImageInfo(parent);
expect(imageInfo.url.indexOf(imageTelemetry[COUNT - 3].timeId)).not.toEqual(-1);
done();
});
});
});
it ('shows an auto scroll button when scroll to left', (done) => {
Vue.nextTick(() => {

View File

@@ -59,7 +59,6 @@
<div
:id="entry.id"
class="c-ne__text c-ne__input"
aria-label="Notebook Entry Input"
tabindex="0"
contenteditable="true"
@focus="editingEntry()"

View File

@@ -80,7 +80,7 @@
&__content {
$m: $interiorMargin;
display: grid;
grid-template-columns: max-content 1fr;
grid-template-columns: min-content 1fr;
grid-column-gap: $m;
grid-row-gap: $m;

View File

@@ -57,7 +57,6 @@
</template>
<script>
const DEFAULT_POLL_QUESTION = 'NO POLL QUESTION';
export default {
inject: ['openmct', 'indicator', 'configuration'],
@@ -76,7 +75,7 @@ export default {
allRoles: [],
role: '--',
pollQuestionUpdated: '--',
currentPollQuestion: DEFAULT_POLL_QUESTION,
currentPollQuestion: '--',
selectedStatus: undefined,
allStatuses: []
};

View File

@@ -34,7 +34,7 @@
<div class="c-status-poll__section c-status-poll-panel__content c-spq">
<!-- Grid layout -->
<div class="c-spq__label">Current poll:</div>
<div class="c-spq__label">Current:</div>
<div class="c-spq__value c-status-poll-panel__poll-question">{{ currentPollQuestion }}</div>
<template v-if="statusCountViewModel.length > 0">
@@ -43,7 +43,6 @@
<div
v-for="entry in statusCountViewModel"
:key="entry.status.key"
:title="entry.status.label"
class="c-status-poll-report__count"
:style="[{
background: entry.status.statusBgColor,
@@ -70,7 +69,6 @@
>
<button
class="c-button"
title="Publish a new poll question and reset previous responses"
@click="updatePollQuestion"
>Update</button>
</div>
@@ -80,7 +78,6 @@
</template>
<script>
import _ from 'lodash';
export default {
inject: ['openmct', 'indicator', 'configuration'],
@@ -121,9 +118,6 @@ export default {
this.openmct.user.status.off('statusChange', this.fetchStatusSummary);
this.openmct.user.status.off('pollQuestionChange', this.setPollQuestion);
},
created() {
this.fetchStatusSummary = _.debounce(this.fetchStatusSummary);
},
methods: {
async fetchCurrentPoll() {
const pollQuestion = await this.openmct.user.status.getPollQuestion();

View File

@@ -45,7 +45,8 @@ export default function CouchDocument(id, model, rev, markDeleted) {
"category": "domain object",
"type": model.type,
"owner": "admin",
"name": model.name
"name": model.name,
"created": Date.now()
},
"model": model
};

View File

@@ -199,11 +199,6 @@ class CouchObjectProvider {
}
let response = null;
if (!this.isObservingObjectChanges()) {
this.#observeObjectChanges();
}
try {
response = await fetch(this.url + '/' + subPath, fetchOptions);
const { status } = response;
@@ -215,13 +210,9 @@ class CouchObjectProvider {
// Network error, CouchDB unreachable.
if (response === null) {
this.indicator.setIndicatorToState(DISCONNECTED);
console.error(error.message);
throw new Error(`CouchDB Error - No response"`);
} else {
console.error(error.message);
throw error;
}
console.error(error.message);
}
}
@@ -383,8 +374,6 @@ class CouchObjectProvider {
return this.request(ALL_DOCS, 'POST', query, signal).then((response) => {
if (response && response.rows !== undefined) {
return response.rows.reduce((map, row) => {
//row.doc === null if the document does not exist.
//row.doc === undefined if the document is not found.
if (row.doc !== undefined) {
map[row.key] = this.#getModel(row.doc);
}
@@ -482,6 +471,9 @@ class CouchObjectProvider {
this.observers[keyString] = this.observers[keyString].filter(observer => observer !== callback);
if (this.observers[keyString].length === 0) {
delete this.observers[keyString];
if (Object.keys(this.observers).length === 0 && this.isObservingObjectChanges()) {
this.stopObservingObjectChanges();
}
}
}
};
@@ -506,6 +498,7 @@ class CouchObjectProvider {
} else {
this.#initiateSharedWorkerFetchChanges(sseURL.toString());
}
}
/**
@@ -532,24 +525,18 @@ class CouchObjectProvider {
onEventError(error) {
console.error('Error on feed', error);
const { readyState } = error.target;
this.#updateIndicatorStatus(readyState);
}
onEventOpen(event) {
const { readyState } = event.target;
this.#updateIndicatorStatus(readyState);
if (Object.keys(this.observers).length > 0) {
this.#observeObjectChanges();
}
}
onEventMessage(event) {
const { readyState } = event.target;
const eventData = JSON.parse(event.data);
const identifier = {
namespace: this.namespace,
key: eventData.id
};
const keyString = this.openmct.objects.makeKeyString(identifier);
this.#updateIndicatorStatus(readyState);
let observersForObject = this.observers[keyString];
if (observersForObject) {
@@ -572,18 +559,17 @@ class CouchObjectProvider {
this.stopObservingObjectChanges = () => {
controller.abort();
couchEventSource.removeEventListener('message', this.onEventMessage.bind(this));
couchEventSource.removeEventListener('message', this.onEventMessage);
delete this.stopObservingObjectChanges;
};
console.debug('⇿ Opening CouchDB change feed connection ⇿');
couchEventSource = new EventSource(url);
couchEventSource.onerror = this.onEventError.bind(this);
couchEventSource.onopen = this.onEventOpen.bind(this);
couchEventSource.onerror = this.onEventError;
// start listening for events
couchEventSource.addEventListener('message', this.onEventMessage.bind(this));
couchEventSource.addEventListener('message', this.onEventMessage);
console.debug('⇿ Opened connection ⇿');
}
@@ -601,31 +587,6 @@ class CouchObjectProvider {
return intermediateResponse;
}
/**
* Update the indicator status based on the readyState of the EventSource
* @private
*/
#updateIndicatorStatus(readyState) {
let message;
switch (readyState) {
case EventSource.CONNECTING:
message = 'pending';
break;
case EventSource.OPEN:
message = 'open';
break;
case EventSource.CLOSED:
message = 'close';
break;
default:
message = 'unknown';
break;
}
const indicatorState = this.#messageToIndicatorState(message);
this.indicator.setIndicatorToState(indicatorState);
}
/**
* @private
*/
@@ -653,8 +614,8 @@ class CouchObjectProvider {
this.objectQueue[key].pending = true;
const queued = this.objectQueue[key].dequeue();
let document = new CouchDocument(key, queued.model);
document.metadata.created = Date.now();
this.request(key, "PUT", document).then((response) => {
console.log('create check response', key);
this.#checkResponse(response, queued.intermediateResponse, key);
}).catch(error => {
queued.intermediateResponse.reject(error);

View File

@@ -152,10 +152,7 @@ describe('the plugin', () => {
mockDomainObject.id = mockDomainObject.identifier.key;
const fakeUpdateEvent = {
data: JSON.stringify(mockDomainObject),
target: {
readyState: EventSource.CONNECTED
}
data: JSON.stringify(mockDomainObject)
};
// eslint-disable-next-line require-await
@@ -173,6 +170,7 @@ describe('the plugin', () => {
expect(provider.create).toHaveBeenCalled();
expect(provider.observe).toHaveBeenCalled();
expect(provider.isObservingObjectChanges).toHaveBeenCalled();
expect(provider.isObservingObjectChanges.calls.mostRecent().returnValue).toBe(true);
//Set modified timestamp it detects a change and persists the updated model.
mockDomainObject.modified = mockDomainObject.persisted + 1;
@@ -183,7 +181,6 @@ describe('the plugin', () => {
expect(updatedResult).toBeTrue();
expect(provider.update).toHaveBeenCalled();
expect(provider.fetchChanges).toHaveBeenCalled();
expect(provider.isObservingObjectChanges.calls.mostRecent().returnValue).toBe(true);
sharedWorkerCallback(fakeUpdateEvent);
expect(provider.onEventMessage).toHaveBeenCalled();

View File

@@ -25,7 +25,7 @@ const exportPNG = {
name: 'Export as PNG',
key: 'export-as-png',
description: 'Export This View\'s Data as PNG',
cssClass: 'icon-download',
cssClass: 'c-icon-button icon-download',
group: 'view',
invoke(objectPath, view) {
view.getViewContext().exportPNG();
@@ -36,7 +36,7 @@ const exportJPG = {
name: 'Export as JPG',
key: 'export-as-jpg',
description: 'Export This View\'s Data as JPG',
cssClass: 'icon-download',
cssClass: 'c-icon-button icon-download',
group: 'view',
invoke(objectPath, view) {
view.getViewContext().exportJPG();

View File

@@ -135,21 +135,17 @@ export default {
},
setUpXAxisOptions() {
const xAxisKey = this.xAxis.get('key');
this.xKeyOptions = [];
if (this.seriesModel.metadata) {
this.xKeyOptions = this.seriesModel.metadata
.valuesForHints(['domain'])
.map(function (o) {
return {
name: o.name,
key: o.key
};
});
}
this.xKeyOptions = this.seriesModel.metadata
.valuesForHints(['domain'])
.map(function (o) {
return {
name: o.name,
key: o.key
};
});
this.xAxisLabel = this.xAxis.get('label');
this.selectedXKeyOptionKey = this.xKeyOptions.length > 0 ? this.getXKeyOption(xAxisKey).key : xAxisKey;
this.selectedXKeyOptionKey = this.getXKeyOption(xAxisKey).key;
},
onTickWidthChange(width) {
this.$emit('tickWidthChanged', width);

View File

@@ -120,25 +120,21 @@ export default {
}
},
setUpYAxisOptions() {
this.yKeyOptions = [];
if (this.seriesModel.metadata) {
this.yKeyOptions = this.seriesModel.metadata
.valuesForHints(['range'])
.map(function (o) {
return {
name: o.name,
key: o.key
};
});
}
this.yKeyOptions = this.seriesModel.metadata
.valuesForHints(['range'])
.map(function (o) {
return {
name: o.name,
key: o.key
};
});
// set yAxisLabel if none is set yet
if (this.yAxisLabel === 'none') {
let yKey = this.seriesModel.model.yKey;
let yKeyModel = this.yKeyOptions.filter(o => o.key === yKey)[0];
this.yAxisLabel = yKeyModel ? yKeyModel.name : '';
this.yAxisLabel = yKeyModel.name;
}
},
toggleYAxisLabel() {

View File

@@ -34,12 +34,6 @@ export default class Model extends EventEmitter {
*/
constructor(options) {
super();
Object.defineProperty(this, '_events', {
value: this._events,
enumerable: false,
configurable: false,
writable: true
});
//need to do this as we're already extending EventEmitter
eventHelpers.extend(this);

View File

@@ -197,27 +197,25 @@ export default {
},
methods: {
initConfiguration() {
if (this.config) {
this.label = this.config.yAxis.get('label');
this.autoscale = this.config.yAxis.get('autoscale');
this.logMode = this.config.yAxis.get('logMode');
this.autoscalePadding = this.config.yAxis.get('autoscalePadding');
const range = this.config.yAxis.get('range');
if (range) {
this.rangeMin = range.min;
this.rangeMax = range.max;
}
this.position = this.config.legend.get('position');
this.hideLegendWhenSmall = this.config.legend.get('hideLegendWhenSmall');
this.expandByDefault = this.config.legend.get('expandByDefault');
this.valueToShowWhenCollapsed = this.config.legend.get('valueToShowWhenCollapsed');
this.showTimestampWhenExpanded = this.config.legend.get('showTimestampWhenExpanded');
this.showValueWhenExpanded = this.config.legend.get('showValueWhenExpanded');
this.showMinimumWhenExpanded = this.config.legend.get('showMinimumWhenExpanded');
this.showMaximumWhenExpanded = this.config.legend.get('showMaximumWhenExpanded');
this.showUnitsWhenExpanded = this.config.legend.get('showUnitsWhenExpanded');
this.label = this.config.yAxis.get('label');
this.autoscale = this.config.yAxis.get('autoscale');
this.logMode = this.config.yAxis.get('logMode');
this.autoscalePadding = this.config.yAxis.get('autoscalePadding');
const range = this.config.yAxis.get('range');
if (range) {
this.rangeMin = range.min;
this.rangeMax = range.max;
}
this.position = this.config.legend.get('position');
this.hideLegendWhenSmall = this.config.legend.get('hideLegendWhenSmall');
this.expandByDefault = this.config.legend.get('expandByDefault');
this.valueToShowWhenCollapsed = this.config.legend.get('valueToShowWhenCollapsed');
this.showTimestampWhenExpanded = this.config.legend.get('showTimestampWhenExpanded');
this.showValueWhenExpanded = this.config.legend.get('showValueWhenExpanded');
this.showMinimumWhenExpanded = this.config.legend.get('showMinimumWhenExpanded');
this.showMaximumWhenExpanded = this.config.legend.get('showMaximumWhenExpanded');
this.showUnitsWhenExpanded = this.config.legend.get('showUnitsWhenExpanded');
},
getConfig() {
this.configId = this.openmct.objects.makeKeyString(this.domainObject.identifier);
@@ -225,12 +223,10 @@ export default {
return configStore.get(this.configId);
},
registerListeners() {
if (this.config) {
this.config.series.forEach(this.addSeries, this);
this.config.series.forEach(this.addSeries, this);
this.listenTo(this.config.series, 'add', this.addSeries, this);
this.listenTo(this.config.series, 'remove', this.resetAllSeries, this);
}
this.listenTo(this.config.series, 'add', this.addSeries, this);
this.listenTo(this.config.series, 'remove', this.resetAllSeries, this);
},
addSeries(series, index) {

View File

@@ -27,7 +27,6 @@ import OverlayPlotCompositionPolicy from './overlayPlot/OverlayPlotCompositionPo
import StackedPlotCompositionPolicy from './stackedPlot/StackedPlotCompositionPolicy';
import PlotViewActions from "./actions/ViewActions";
import StackedPlotsInspectorViewProvider from "./inspector/StackedPlotsInspectorViewProvider";
import stackedPlotConfigurationInterceptor from "./stackedPlot/stackedPlotConfigurationInterceptor";
export default function () {
return function install(openmct) {
@@ -65,8 +64,6 @@ export default function () {
priority: 890
});
stackedPlotConfigurationInterceptor(openmct);
openmct.objectViews.addProvider(new StackedPlotViewProvider(openmct));
openmct.objectViews.addProvider(new OverlayPlotViewProvider(openmct));
openmct.objectViews.addProvider(new PlotViewProvider(openmct));

View File

@@ -134,7 +134,6 @@ export default {
//If this object is not persistable, then package it with it's parent
const object = this.getPlotObject();
const getProps = this.getProps;
const isMissing = openmct.objects.isMissing(object);
let viewContainer = document.createElement('div');
this.$el.append(viewContainer);
@@ -159,7 +158,6 @@ export default {
onCursorGuideChange,
onGridLinesChange,
setStatus,
isMissing,
loading: true
};
},
@@ -168,7 +166,7 @@ export default {
this.loading = loaded;
}
},
template: '<div v-if="!isMissing" ref="plotWrapper" class="l-view-section u-style-receiver js-style-receiver" :class="{\'s-status-timeconductor-unsynced\': status && status === \'timeconductor-unsynced\'}"><progress-bar v-show="loading !== false" class="c-telemetry-table__progress-bar" :model="{progressPerc: undefined}" /><mct-plot :init-grid-lines="gridLines" :init-cursor-guide="cursorGuide" :plot-tick-width="plotTickWidth" :limit-line-labels="limitLineLabels" :color-palette="colorPalette" :options="options" @plotTickWidth="onTickWidthChange" @lockHighlightPoint="onLockHighlightPointUpdated" @highlights="onHighlightsUpdated" @configLoaded="onConfigLoaded" @cursorGuide="onCursorGuideChange" @gridLines="onGridLinesChange" @statusUpdated="setStatus" @loadingUpdated="loadingUpdated"/></div>'
template: '<div ref="plotWrapper" class="l-view-section u-style-receiver js-style-receiver" :class="{\'s-status-timeconductor-unsynced\': status && status === \'timeconductor-unsynced\'}"><progress-bar v-show="loading !== false" class="c-telemetry-table__progress-bar" :model="{progressPerc: undefined}" /><mct-plot :init-grid-lines="gridLines" :init-cursor-guide="cursorGuide" :plot-tick-width="plotTickWidth" :limit-line-labels="limitLineLabels" :color-palette="colorPalette" :options="options" @plotTickWidth="onTickWidthChange" @lockHighlightPoint="onLockHighlightPointUpdated" @highlights="onHighlightsUpdated" @configLoaded="onConfigLoaded" @cursorGuide="onCursorGuideChange" @gridLines="onGridLinesChange" @statusUpdated="setStatus" @loadingUpdated="loadingUpdated"/></div>'
});
this.setSelection();
@@ -222,13 +220,6 @@ export default {
//If the object has a configuration, allow initialization of the config from it's persisted config
return this.childObject;
} else {
//If object is missing, warn and return object
if (this.openmct.objects.isMissing(this.childObject)) {
console.warn('Missing domain object');
return this.childObject;
}
// If the object does not have configuration, initialize the series config with the persisted config from the stacked plot
const configId = this.openmct.objects.makeKeyString(this.childObject.identifier);
let config = configStore.get(configId);

View File

@@ -20,7 +20,6 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import DefaultClock from '../../utils/clock/DefaultClock';
import remoteClockRequestInterceptor from './requestInterceptor';
/**
* A {@link openmct.TimeAPI.Clock} that updates the temporal bounds of the
@@ -50,14 +49,6 @@ export default class RemoteClock extends DefaultClock {
this.lastTick = 0;
this.openmct.telemetry.addRequestInterceptor(
remoteClockRequestInterceptor(
this.openmct,
this.identifier,
this.#waitForReady.bind(this)
)
);
this._processDatum = this._processDatum.bind(this);
}
@@ -121,6 +112,7 @@ export default class RemoteClock extends DefaultClock {
if (time > this.lastTick) {
this.tick(time);
this.initialized = true;
}
}
@@ -138,25 +130,4 @@ export default class RemoteClock extends DefaultClock {
return timeFormatter.parse(datum);
};
}
/**
* Waits for the clock to have a non-default tick value.
*
* @private
*/
#waitForReady() {
const waitForInitialTick = (resolve) => {
if (this.lastTick > 0) {
const offsets = this.openmct.time.clockOffsets();
resolve({
start: this.lastTick + offsets.start,
end: this.lastTick + offsets.end
});
} else {
setTimeout(() => waitForInitialTick(resolve), 100);
}
};
return new Promise(waitForInitialTick);
}
}

View File

@@ -1,46 +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.
*****************************************************************************/
function remoteClockRequestInterceptor(openmct, remoteClockIdentifier, waitForBounds) {
let remoteClockLoaded = false;
return {
appliesTo: () => {
// Get the activeClock from the Global Time Context
const { activeClock } = openmct.time.getContextForView();
return activeClock !== undefined
&& activeClock.key === 'remote-clock'
&& !remoteClockLoaded;
},
invoke: async (request) => {
const { start, end } = await waitForBounds();
remoteClockLoaded = true;
request.start = start;
request.end = end;
return request;
}
};
}
export default remoteClockRequestInterceptor;

View File

@@ -78,10 +78,6 @@ class StaticModelProvider {
}
parseTreeLeaf(leafKey, leafValue, idMap, namespace) {
if (leafValue === null || leafValue === undefined) {
return leafValue;
}
const hasChild = typeof leafValue === 'object';
if (hasChild) {
return this.parseBranchedLeaf(leafValue, idMap, namespace);

View File

@@ -1 +1 @@
{"openmct":{"a9122832-4b6e-43ea-8219-5359c14c5de8":{"composition":["483c00d4-bb1d-4b42-b29a-c58e06b322a0","d2ac3ae4-0af2-49fe-81af-adac09936215"],"name":"import-provider-test","type":"folder","notes":null,"modified":1508522673278,"location":"mine","persisted":1508522673278},"483c00d4-bb1d-4b42-b29a-c58e06b322a0":{"telemetry":{"period":10,"amplitude":1,"offset":0,"dataRateInHz":1,"values":[{"key":"utc","name":"Time","format":"utc","hints":{"domain":1,"priority":0},"source":"utc"},{"key":"yesterday","name":"Yesterday","format":"utc","hints":{"domain":2,"priority":1},"source":"yesterday"},{"key":"sin","name":"Sine","hints":{"range":1,"priority":2},"source":"sin"},{"key":"cos","name":"Cosine","hints":{"range":2,"priority":3},"source":"cos"}]},"name":"SWG-10","type":"generator","modified":1508522652874,"location":"a9122832-4b6e-43ea-8219-5359c14c5de8","persisted":1508522652874},"d2ac3ae4-0af2-49fe-81af-adac09936215":{"composition":["483c00d4-bb1d-4b42-b29a-c58e06b322a0","20273193-f069-49e9-b4f7-b97a87ed755d"],"name":"Layout","type":"layout","configuration":{"layout":{"panels":{"483c00d4-bb1d-4b42-b29a-c58e06b322a0":{"position":[0,0],"dimensions":[17,8]},"20273193-f069-49e9-b4f7-b97a87ed755d":{"position":[0,8],"dimensions":[17,1],"hasFrame":false}}}},"modified":1508522745580,"location":"a9122832-4b6e-43ea-8219-5359c14c5de8","persisted":1508522745580},"20273193-f069-49e9-b4f7-b97a87ed755d":{"layoutGrid":[64,16],"composition":["483c00d4-bb1d-4b42-b29a-c58e06b322a0"],"name":"FP Test","type":"telemetry.fixed","configuration":{"fixed-display":{"elements":[{"type":"fixed.telemetry","x":0,"y":0,"id":"483c00d4-bb1d-4b42-b29a-c58e06b322a0","stroke":"transparent","color":"","titled":true,"width":8,"height":2,"useGrid":true,"size":"24px"}]}},"modified":1508522717619,"location":"d2ac3ae4-0af2-49fe-81af-adac09936215","persisted":1508522717619}},"rootId":"a9122832-4b6e-43ea-8219-5359c14c5de8"}
{"openmct":{"a9122832-4b6e-43ea-8219-5359c14c5de8":{"composition":["483c00d4-bb1d-4b42-b29a-c58e06b322a0","d2ac3ae4-0af2-49fe-81af-adac09936215"],"name":"import-provider-test","type":"folder","notes":"test data for import provider.","modified":1508522673278,"location":"mine","persisted":1508522673278},"483c00d4-bb1d-4b42-b29a-c58e06b322a0":{"telemetry":{"period":10,"amplitude":1,"offset":0,"dataRateInHz":1,"values":[{"key":"utc","name":"Time","format":"utc","hints":{"domain":1,"priority":0},"source":"utc"},{"key":"yesterday","name":"Yesterday","format":"utc","hints":{"domain":2,"priority":1},"source":"yesterday"},{"key":"sin","name":"Sine","hints":{"range":1,"priority":2},"source":"sin"},{"key":"cos","name":"Cosine","hints":{"range":2,"priority":3},"source":"cos"}]},"name":"SWG-10","type":"generator","modified":1508522652874,"location":"a9122832-4b6e-43ea-8219-5359c14c5de8","persisted":1508522652874},"d2ac3ae4-0af2-49fe-81af-adac09936215":{"composition":["483c00d4-bb1d-4b42-b29a-c58e06b322a0","20273193-f069-49e9-b4f7-b97a87ed755d"],"name":"Layout","type":"layout","configuration":{"layout":{"panels":{"483c00d4-bb1d-4b42-b29a-c58e06b322a0":{"position":[0,0],"dimensions":[17,8]},"20273193-f069-49e9-b4f7-b97a87ed755d":{"position":[0,8],"dimensions":[17,1],"hasFrame":false}}}},"modified":1508522745580,"location":"a9122832-4b6e-43ea-8219-5359c14c5de8","persisted":1508522745580},"20273193-f069-49e9-b4f7-b97a87ed755d":{"layoutGrid":[64,16],"composition":["483c00d4-bb1d-4b42-b29a-c58e06b322a0"],"name":"FP Test","type":"telemetry.fixed","configuration":{"fixed-display":{"elements":[{"type":"fixed.telemetry","x":0,"y":0,"id":"483c00d4-bb1d-4b42-b29a-c58e06b322a0","stroke":"transparent","color":"","titled":true,"width":8,"height":2,"useGrid":true,"size":"24px"}]}},"modified":1508522717619,"location":"d2ac3ae4-0af2-49fe-81af-adac09936215","persisted":1508522717619}},"rootId":"a9122832-4b6e-43ea-8219-5359c14c5de8"}

View File

@@ -225,7 +225,9 @@ define(
sortBy(sortOptions) {
if (arguments.length > 0) {
this.sortOptions = sortOptions;
performance.mark('table:row:sort:start');
this.rows = _.orderBy(this.rows, (row) => row.getParsedValue(sortOptions.key), sortOptions.direction);
performance.mark('table:row:sort:stop');
this.emit('sort');
}

View File

@@ -612,6 +612,7 @@ export default {
this.calculateScrollbarWidth();
},
sortBy(columnKey) {
performance.mark('table:sort');
// If sorting by the same column, flip the sort direction.
if (this.sortOptions.key === columnKey) {
if (this.sortOptions.direction === 'asc') {
@@ -668,6 +669,7 @@ export default {
this.setHeight();
},
rowsAdded(rows) {
performance.mark('row:added');
this.setHeight();
let sizingRow;
@@ -689,6 +691,7 @@ export default {
this.updateVisibleRows();
},
rowsRemoved(rows) {
performance.mark('row:removed');
this.setHeight();
this.updateVisibleRows();
},

View File

@@ -135,7 +135,7 @@ describe("the plugin", () => {
let tableInstance;
let mockClock;
beforeEach(async () => {
beforeEach(() => {
openmct.time.timeSystem('utc', {
start: 0,
end: 4
@@ -210,8 +210,16 @@ describe("the plugin", () => {
'some-other-key': 'some-other-value 3'
}
];
let telemetryPromiseResolve;
let telemetryPromise = new Promise((resolve) => {
telemetryPromiseResolve = resolve;
});
historicalProvider.request = () => Promise.resolve(testTelemetry);
historicalProvider.request = () => {
telemetryPromiseResolve(testTelemetry);
return telemetryPromise;
};
openmct.router.path = [testTelemetryObject];
@@ -222,7 +230,7 @@ describe("the plugin", () => {
tableInstance = tableView.getTable();
await Vue.nextTick();
return telemetryPromise.then(() => Vue.nextTick());
});
afterEach(() => {
@@ -247,10 +255,13 @@ describe("the plugin", () => {
});
it("Renders a row for every telemetry datum returned", async () => {
it("Renders a row for every telemetry datum returned", (done) => {
let rows = element.querySelectorAll('table.c-telemetry-table__body tr');
await Vue.nextTick();
expect(rows.length).toBe(3);
Vue.nextTick(() => {
expect(rows.length).toBe(3);
done();
});
});
it("Renders a column for every item in telemetry metadata", () => {
@@ -262,7 +273,7 @@ describe("the plugin", () => {
expect(headers[3].innerText).toBe('Another attribute');
});
it("Supports column reordering via drag and drop", async () => {
it("Supports column reordering via drag and drop", () => {
let columns = element.querySelectorAll('tr.c-telemetry-table__headers__labels th');
let fromColumn = columns[0];
let toColumn = columns[1];
@@ -281,43 +292,54 @@ describe("the plugin", () => {
toColumn.dispatchEvent(dragOverEvent);
toColumn.dispatchEvent(dropEvent);
await Vue.nextTick();
columns = element.querySelectorAll('tr.c-telemetry-table__headers__labels th');
let firstColumn = columns[0];
let secondColumn = columns[1];
let firstColumnText = firstColumn.querySelector('span.c-telemetry-table__headers__label').innerText;
let secondColumnText = secondColumn.querySelector('span.c-telemetry-table__headers__label').innerText;
expect(fromColumnText).not.toEqual(firstColumnText);
expect(fromColumnText).toEqual(secondColumnText);
expect(toColumnText).not.toEqual(secondColumnText);
expect(toColumnText).toEqual(firstColumnText);
return Vue.nextTick().then(() => {
columns = element.querySelectorAll('tr.c-telemetry-table__headers__labels th');
let firstColumn = columns[0];
let secondColumn = columns[1];
let firstColumnText = firstColumn.querySelector('span.c-telemetry-table__headers__label').innerText;
let secondColumnText = secondColumn.querySelector('span.c-telemetry-table__headers__label').innerText;
expect(fromColumnText).not.toEqual(firstColumnText);
expect(fromColumnText).toEqual(secondColumnText);
expect(toColumnText).not.toEqual(secondColumnText);
expect(toColumnText).toEqual(firstColumnText);
});
});
it("Supports filtering telemetry by regular text search", async () => {
it("Supports filtering telemetry by regular text search", () => {
tableInstance.tableRows.setColumnFilter("some-key", "1");
await Vue.nextTick();
let filteredRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
expect(filteredRowElements.length).toEqual(1);
tableInstance.tableRows.setColumnFilter("some-key", "");
await Vue.nextTick();
return Vue.nextTick().then(() => {
let filteredRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
let allRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
expect(allRowElements.length).toEqual(3);
expect(filteredRowElements.length).toEqual(1);
tableInstance.tableRows.setColumnFilter("some-key", "");
return Vue.nextTick().then(() => {
let allRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
expect(allRowElements.length).toEqual(3);
});
});
});
it("Supports filtering using Regex", async () => {
it("Supports filtering using Regex", () => {
tableInstance.tableRows.setColumnRegexFilter("some-key", "^some-value$");
await Vue.nextTick();
let filteredRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
expect(filteredRowElements.length).toEqual(0);
return Vue.nextTick().then(() => {
let filteredRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
tableInstance.tableRows.setColumnRegexFilter("some-key", "^some-value");
await Vue.nextTick();
let allRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
expect(filteredRowElements.length).toEqual(0);
expect(allRowElements.length).toEqual(3);
tableInstance.tableRows.setColumnRegexFilter("some-key", "^some-value");
return Vue.nextTick().then(() => {
let allRowElements = element.querySelectorAll('table.c-telemetry-table__body tr');
expect(allRowElements.length).toEqual(3);
});
});
});
it("displays the correct number of column headers when the configuration is mutated", async () => {
@@ -380,7 +402,7 @@ describe("the plugin", () => {
expect(element.querySelector('div.c-table.is-paused')).not.toBeNull();
const currentBounds = openmct.time.bounds();
await Vue.nextTick();
const newBounds = {
start: currentBounds.start,
end: currentBounds.end - 3
@@ -388,10 +410,17 @@ describe("the plugin", () => {
// Manually change the time bounds
openmct.time.bounds(newBounds);
await Vue.nextTick();
// Verify table is no longer paused
expect(element.querySelector('div.c-table.is-paused')).toBeNull();
await Vue.nextTick();
// Verify table displays the correct number of rows within the new bounds
const tableRows = element.querySelectorAll('table.c-telemetry-table__body > tbody > tr');
expect(tableRows.length).toEqual(2);
});
it("Unpauses the table on user bounds change if paused by button", async () => {
@@ -399,18 +428,19 @@ describe("the plugin", () => {
// Pause by button
viewContext.togglePauseByButton();
await Vue.nextTick();
// Verify table is paused
expect(element.querySelector('div.c-table.is-paused')).not.toBeNull();
const currentBounds = openmct.time.bounds();
await Vue.nextTick();
const newBounds = {
start: currentBounds.start,
end: currentBounds.end - 1
end: currentBounds.end - 3
};
// Manually change the time bounds
openmct.time.bounds(newBounds);
@@ -418,6 +448,12 @@ describe("the plugin", () => {
// Verify table is no longer paused
expect(element.querySelector('div.c-table.is-paused')).toBeNull();
await Vue.nextTick();
// Verify table displays the correct number of rows within the new bounds
const tableRows = element.querySelectorAll('table.c-telemetry-table__body > tbody > tr');
expect(tableRows.length).toEqual(2);
});
it("Does not unpause the table on tick", async () => {

View File

@@ -40,6 +40,7 @@ export default class LocalClock extends DefaultClock {
this.period = period;
this.timeoutHandle = undefined;
this.lastTick = Date.now();
this.initialized = true;
}
start() {

View File

@@ -223,16 +223,15 @@ export default {
},
resizeSoView() {
let cW = this.$refs.soView.offsetWidth;
let widths = [220, 600];
let wClass = '';
for (let width of widths) {
if (cW < width) {
wClass = wClass.concat(' ', CSS_WIDTH_LESS_STR, width);
}
if (cW < 220) {
wClass = CSS_WIDTH_LESS_STR + '220';
} else if (cW < 600) {
wClass = CSS_WIDTH_LESS_STR + '600';
}
this.widthClass = wClass.trimStart();
this.widthClass = wClass;
}
}
};

View File

@@ -11,7 +11,6 @@
margin-bottom: $interiorMarginSm;
overflow: hidden;
padding: 3px;
@include smallerControlButtons; // Make button in frame headers a bit smaller
.c-object-label {
font-size: 1.05em;
@@ -133,6 +132,8 @@
}
}
@include smallerControlButtons;
&.has-complex-content {
> .c-so-view__view-large { display: block; }
}

View File

@@ -2,7 +2,7 @@
// <a> tag and draggable element that holds type icon and name.
// Used mostly in trees and lists
display: flex;
align-items: center;
align-items: baseline; // Provides better vertical alignment than center
flex: 0 1 auto;
overflow: hidden;
white-space: nowrap;

View File

@@ -107,12 +107,7 @@ export default {
this.preview();
} else {
const objectPath = this.result.originalPath;
let resultUrl = objectPathToUrl(this.openmct, objectPath);
// get rid of ROOT if extant
if (resultUrl.includes('/ROOT')) {
resultUrl = resultUrl.split('/ROOT').join('');
}
const resultUrl = objectPathToUrl(this.openmct, objectPath);
this.openmct.router.navigate(resultUrl);
}
},

View File

@@ -50,10 +50,6 @@ class ApplicationRouter extends EventEmitter {
this.started = false;
this.setHash = _.debounce(this.setHash.bind(this), 300);
openmct.once('destroy', () => {
this.destroy();
});
}
// Public Methods

View File

@@ -38,6 +38,7 @@ export default class DefaultClock extends EventEmitter {
this.cssClass = 'icon-clock';
this.name = 'Clock';
this.description = "A default clock for openmct.";
this.initialized = false;
}
tick(tickValue) {
@@ -86,4 +87,7 @@ export default class DefaultClock extends EventEmitter {
return this.lastTick;
}
isInitialized() {
return this.initialized;
}
}

View File

@@ -7,19 +7,12 @@ const webpack = require('webpack');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const {VueLoaderPlugin} = require('vue-loader');
let gitRevision = 'error-retrieving-revision';
let gitBranch = 'error-retrieving-branch';
try {
gitRevision = require('child_process')
.execSync('git rev-parse HEAD')
.toString().trim();
gitBranch = require('child_process')
.execSync('git rev-parse --abbrev-ref HEAD')
.toString().trim();
} catch (err) {
console.warn(err);
}
const gitRevision = require('child_process')
.execSync('git rev-parse HEAD')
.toString().trim();
const gitBranch = require('child_process')
.execSync('git rev-parse --abbrev-ref HEAD')
.toString().trim();
/** @type {import('webpack').Configuration} */
const config = {
@@ -101,13 +94,8 @@ const config = {
{
loader: 'css-loader'
},
{
loader: 'resolve-url-loader'
},
{
loader: 'sass-loader',
options: {sourceMap: true }
}
'resolve-url-loader',
'sass-loader'
]
},
{

View File

@@ -2,12 +2,12 @@
// instrumentation using babel-plugin-istanbul (see babel.coverage.js)
const config = require('./webpack.dev');
const path = require('path');
const vueLoaderRule = config.module.rules.find(r => r.use === 'vue-loader');
// eslint-disable-next-line no-undef
const CI = process.env.CI === 'true';
config.devtool = CI ? false : undefined;
const path = require('path');
config.devtool = false;
const vueLoaderRule = config.module.rules.find(r => r.use === 'vue-loader');
vueLoaderRule.use = {
loader: 'vue-loader'
@@ -34,7 +34,6 @@ config.module.rules.push({
use: {
loader: 'babel-loader',
options: {
// eslint-disable-next-line no-undef
configFile: path.resolve(process.cwd(), 'babel.coverage.js')
}
}