Compare commits
36 Commits
mutation-p
...
de-reactif
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee80c5c14e | ||
|
|
eacbac6aad | ||
|
|
69153fe8f0 | ||
|
|
51196530fd | ||
|
|
fefa46ce7e | ||
|
|
e08ab8ef24 | ||
|
|
7011877e64 | ||
|
|
34ecc08238 | ||
|
|
a07c043a29 | ||
|
|
2999a5135e | ||
|
|
2766452b38 | ||
|
|
f3cdf69288 | ||
|
|
a040bb30c2 | ||
|
|
0a2e0a4e65 | ||
|
|
e8df2bd437 | ||
|
|
ccd2a8b64c | ||
|
|
2bd35bb2a5 | ||
|
|
28dbd724d6 | ||
|
|
5a1c329c66 | ||
|
|
00a5cbd2fd | ||
|
|
a2d698d5c1 | ||
|
|
5685a5b393 | ||
|
|
164f39695e | ||
|
|
c384cf67da | ||
|
|
417b225505 | ||
|
|
e5e93f311c | ||
|
|
39e6d9c90c | ||
|
|
60d021ef82 | ||
|
|
59880955a2 | ||
|
|
b51ed7e844 | ||
|
|
7bbaec4006 | ||
|
|
c0f24b3925 | ||
|
|
4e79725897 | ||
|
|
0674c9fc33 | ||
|
|
de1b877954 | ||
|
|
4db2f547d9 |
3
.github/workflows/e2e-pr.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/e2e-visual.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '16'
|
||||
- run: npx playwright@1.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
|
||||
|
||||
2
.github/workflows/prcop-config.json
vendored
@@ -3,7 +3,7 @@
|
||||
{
|
||||
"name": "descriptionRegexp",
|
||||
"config": {
|
||||
"regexp": "[x|X]] Testing instructions",
|
||||
"regexp": "x] Testing instructions",
|
||||
"errorMessage": ":police_officer: PR Description does not confirm that associated issue(s) contain Testing instructions"
|
||||
}
|
||||
},
|
||||
|
||||
32
app.js
@@ -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 || 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 = process.env.CI ? require('./webpack.coverage.js') : 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) {
|
||||
@@ -89,4 +82,3 @@ app.get('/', function (req, res) {
|
||||
app.listen(options.port, options.host, function () {
|
||||
console.log('Open MCT application running at %s:%s', options.host, options.port);
|
||||
});
|
||||
|
||||
|
||||
122
e2e/README.md
@@ -1,122 +0,0 @@
|
||||
# e2e testing
|
||||
|
||||
This document captures information specific to the e2e testing of Open MCT. For general information about testing, please see [the Open MCT README](https://github.com/nasa/openmct/blob/master/README.md#tests).
|
||||
|
||||
## Overview
|
||||
|
||||
This document is designed to capture on the What, Why, and How's of writing and running e2e tests in Open MCT.
|
||||
|
||||
### About e2e testing
|
||||
|
||||
e2e testing is an industry-standard approach to automating the testing of web-based UIs such as Open MCT. Broadly speaking, e2e tests differentiate themselves from unit tests by preferring replication of real user interactions over execution of raw JavaScript functions.
|
||||
|
||||
Historically, the abstraction necessary to replicate real user behavior meant that:
|
||||
|
||||
- e2e tests were "expensive" due to how much code each test executed. The closer a test replicates the user, the more code is needed run during test execution. Unit tests could run smaller units of code more effeciently.
|
||||
- e2e tests were flaky due to network conditions or the underlying protocols associated with testing a browser.
|
||||
- e2e frameworks relied on a browser communication standard which lacked the observability and controls necessary needed to reach the code paths possible with unit and integration tests.
|
||||
- e2e frameworks provided insufficient debug information on test failure
|
||||
|
||||
However, as the web ecosystem has matured to the point where mission-critical UIs can be written for the web (Open MCT), the e2e testing tools have matured as well. There are now fewer "trade-offs" when choosing to write an e2e test over any other type of test.
|
||||
|
||||
Modern e2e frameworks:
|
||||
|
||||
- Bypass the surface layer of the web-application-under-test and use a raw debugging protocol to observe and control application and browser state.
|
||||
- These new browser-internal protocols enable near-instant, bi-directional communication between test code and the browser, speeding up test execution and making the tests as reliable as the application itself.
|
||||
- Provide test debug tooling which enables developers to pinpoint failure
|
||||
|
||||
Furthermore, the abstraction necessary to run e2e tests as a user enables them to be extended to run within a variety of contexts. This matches the extensible design of Open MCT.
|
||||
|
||||
A single e2e test in Open MCT is extended to run:
|
||||
|
||||
- Against a matrix of browser versions.
|
||||
- Against a matrix of OS platforms.
|
||||
- Against a local development version of Open MCT.
|
||||
- A version of Open MCT loaded as a dependency (VIPER, VISTA, etc)
|
||||
- Against a variety of data sources or telemetry endpoints.
|
||||
|
||||
### Why Playwright?
|
||||
|
||||
[Playwright](https://playwright.dev/) was chosen as our e2e framework because it solves a few VIPER Mission needs:
|
||||
1. First-class support for Automated Performance Testing
|
||||
2. Official Chrome, Chrome Canary, and iPad Capabilities
|
||||
3. Support for Browserless.io
|
||||
4. Ability to generate code coverage reports
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Getting started with Playwright
|
||||
|
||||
### Getting started with Open MCT's implementation of Playwright
|
||||
|
||||
## Types of Testing
|
||||
|
||||
### (TBD) Visual Testing
|
||||
|
||||
- Visual tests leverage [Percy](https://percy.io/).
|
||||
- Visual tests should be written within the `./tests/visual` folder so that they can be ignored during git clones to avoid leaking credentials when executing percy cli
|
||||
|
||||
#### (TBD) How to write a good visual test
|
||||
|
||||
### (TBD) Snapshot Testing
|
||||
|
||||
<https://playwright.dev/docs/test-snapshots>
|
||||
|
||||
### (TBD) Mobile Testing
|
||||
|
||||
### (TBD) Performance Testing
|
||||
|
||||
### (FUTURE) Component Testing
|
||||
|
||||
- Component testing is currrently possible in Playwright but not enabled on this project. For more, please see: <https://playwright.dev/docs/test-components>
|
||||
|
||||
## Architecture, Test Design and Best Practices
|
||||
|
||||
### (TBD) Architecture
|
||||
|
||||
#### (TBD) Continuous Integration
|
||||
|
||||
- Test maturation
|
||||
- Difference between full and e2e-ci suites
|
||||
- Platforms
|
||||
|
||||
### (TBD) Multi-browser and Multi-operating system
|
||||
|
||||
- Where is it tested
|
||||
- What's supported
|
||||
|
||||
### (TBD) Test Design
|
||||
|
||||
- Re-usable tests for VISTA, VIPER, etc.
|
||||
|
||||
#### Annotations
|
||||
|
||||
- Annotations are a great way of organizing tests outside of a file structure.
|
||||
- Current list of annotations:
|
||||
- `@ipad` - Mobile execution possible with Playwright's iPad support.
|
||||
- `@gds` - Executes a GDS Test Case. Used to track in VIPER Mission.
|
||||
- `@addInit` - Initializes the browser with an injected and artificial state. Useful for non-default plugins.
|
||||
- `@localStorage` - Captures or generates session storage to manipulate browser state. Useful for excluding in tests which require a persistent backend (i.e. CouchDB).
|
||||
- `@snapshot` - Uses Playwright's snapshot functionality to record a copy of the DOM for direct comparison. Must be run inside of a container.
|
||||
|
||||
### (TBD) Best Practices
|
||||
|
||||
### (TBD) Reporting
|
||||
|
||||
### (TBD) Code Coverage
|
||||
|
||||
Code coverage is collected during test execution and reported with [nyc](https://github.com/istanbuljs/nyc) and [codecov.io](https://about.codecov.io/)
|
||||
|
||||
## Other
|
||||
|
||||
### FAQ
|
||||
|
||||
- How does this help NASA missions?
|
||||
- When should I write an e2e test instead of a unit test?
|
||||
- When should I write a functional vs visual test?
|
||||
- How is Open MCT extending default Playwright functionality?
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
- Why is my test failing on CI and not locally?
|
||||
- How can I view the failing tests on CI?
|
||||
@@ -1,18 +0,0 @@
|
||||
/**
|
||||
* Wait for all animations within the given element and subtrees to finish
|
||||
* See: https://github.com/microsoft/playwright/issues/15660#issuecomment-1184911658
|
||||
* @param {import('@playwright/test').Locator} locator
|
||||
*/
|
||||
function waitForAnimations(locator) {
|
||||
return locator
|
||||
.evaluate((element) =>
|
||||
Promise.all(
|
||||
element
|
||||
.getAnimations({ subtree: true })
|
||||
.map((animation) => animation.finished)));
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
module.exports = {
|
||||
waitForAnimations
|
||||
};
|
||||
@@ -4,8 +4,6 @@
|
||||
|
||||
// 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 = {
|
||||
@@ -14,20 +12,20 @@ const config = {
|
||||
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',
|
||||
command: 'npm run start',
|
||||
url: 'http://localhost: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: [
|
||||
{
|
||||
|
||||
@@ -12,10 +12,10 @@ const config = {
|
||||
testIgnore: '**/*.perf.spec.js',
|
||||
timeout: 30 * 1000,
|
||||
webServer: {
|
||||
command: 'cross-env NODE_ENV=test npm run start',
|
||||
command: 'npm run start',
|
||||
url: 'http://localhost: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: [
|
||||
{
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
// 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
|
||||
@@ -11,15 +9,15 @@ const config = {
|
||||
timeout: 60 * 1000,
|
||||
workers: 1, //Only run in serial with 1 worker
|
||||
webServer: {
|
||||
command: 'cross-env NODE_ENV=test npm run start',
|
||||
command: 'npm run start',
|
||||
url: 'http://localhost: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',
|
||||
|
||||
@@ -9,7 +9,7 @@ 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',
|
||||
command: 'npm run start',
|
||||
url: 'http://localhost: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'],
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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!
|
||||
]);
|
||||
|
||||
});
|
||||
});
|
||||
@@ -55,14 +55,16 @@ 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);
|
||||
await page.close();
|
||||
});
|
||||
|
||||
test.afterAll(async ({ browser }) => {
|
||||
await browser.close();
|
||||
});
|
||||
//Load localStorage for subsequent tests
|
||||
test.use({ storageState: './e2e/test-data/recycled_local_storage.json' });
|
||||
//Begin suite of tests again localStorage
|
||||
@@ -74,7 +76,7 @@ test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
|
||||
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([
|
||||
@@ -85,7 +87,7 @@ 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 }) => {
|
||||
@@ -111,18 +113,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([
|
||||
@@ -135,18 +137,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();
|
||||
});
|
||||
test('condition set object can be deleted by Search Tree Actions menu on @localStorage', async ({ page }) => {
|
||||
//Navigate to baseURL
|
||||
|
||||
@@ -28,7 +28,6 @@ but only assume that example imagery is present.
|
||||
|
||||
const { test } = require('../../../fixtures.js');
|
||||
const { expect } = require('@playwright/test');
|
||||
const { waitForAnimations } = require('../../../commonActions.js');
|
||||
|
||||
const backgroundImageSelector = '.c-imagery__main-image__background-image';
|
||||
|
||||
@@ -82,16 +81,7 @@ test.describe('Example Imagery Object', () => {
|
||||
expect(imageMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width);
|
||||
expect(imageMouseZoomedOut.height).toBeLessThan(imageMouseZoomedIn.height);
|
||||
expect(imageMouseZoomedOut.width).toBeLessThan(imageMouseZoomedIn.width);
|
||||
});
|
||||
|
||||
test('Can adjust image brightness/contrast by dragging the sliders', async ({ page, browserName }) => {
|
||||
test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox');
|
||||
// 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('Can use alt+drag to move around image once zoomed in', async ({ page }) => {
|
||||
@@ -160,65 +150,76 @@ test.describe('Example Imagery Object', () => {
|
||||
});
|
||||
|
||||
test('Can use + - buttons to zoom on the image', async ({ page }) => {
|
||||
// Get initial image dimensions
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
const zoomInBtn = page.locator('.t-btn-zoom-in').nth(0);
|
||||
const zoomOutBtn = page.locator('.t-btn-zoom-out').nth(0);
|
||||
const initialBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
|
||||
// Zoom in twice via button
|
||||
await zoomIntoImageryByButton(page);
|
||||
await zoomIntoImageryByButton(page);
|
||||
|
||||
// Get and assert zoomed in image dimensions
|
||||
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();
|
||||
expect(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
|
||||
expect(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);
|
||||
|
||||
// Zoom out once via button
|
||||
await zoomOutOfImageryByButton(page);
|
||||
|
||||
// Get and assert zoomed out image dimensions
|
||||
await zoomOutBtn.click();
|
||||
// wait for zoom animation to finish
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
const zoomedOutBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
expect(zoomedOutBoundingBox.height).toBeLessThan(zoomedInBoundingBox.height);
|
||||
expect(zoomedOutBoundingBox.width).toBeLessThan(zoomedInBoundingBox.width);
|
||||
|
||||
// Zoom out again via button, assert against the initial image dimensions
|
||||
await zoomOutOfImageryByButton(page);
|
||||
const finalBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
expect(finalBoundingBox).toEqual(initialBoundingBox);
|
||||
});
|
||||
|
||||
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");
|
||||
// Get initial image dimensions
|
||||
test('Can use the reset button to reset the image', async ({ page }) => {
|
||||
// wait for zoom animation to finish
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
|
||||
const zoomInBtn = page.locator('.t-btn-zoom-in').nth(0);
|
||||
const zoomResetBtn = page.locator('.t-btn-zoom-reset').nth(0);
|
||||
const initialBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
|
||||
// Zoom in twice via button
|
||||
await zoomIntoImageryByButton(page);
|
||||
await zoomIntoImageryByButton(page);
|
||||
await zoomInBtn.click();
|
||||
// wait for zoom animation to finish
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
await zoomInBtn.click();
|
||||
// wait for zoom animation to finish
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
|
||||
// Get and assert zoomed in image dimensions
|
||||
const zoomedInBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
expect.soft(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
|
||||
expect.soft(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);
|
||||
|
||||
// Reset pan and zoom and assert against initial image dimensions
|
||||
await resetImageryPanAndZoom(page);
|
||||
const finalBoundingBox = await page.locator(backgroundImageSelector).boundingBox();
|
||||
expect(finalBoundingBox).toEqual(initialBoundingBox);
|
||||
await zoomResetBtn.click();
|
||||
// wait for zoom animation to finish
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
|
||||
const resetBoundingBox = await page.locator(backgroundImageSelector).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 pausePlayButton = page.locator('.c-button.pause-play');
|
||||
// wait for zoom animation to finish
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
|
||||
// open the time conductor drop down
|
||||
await page.locator('button:has-text("Fixed Timespan")').click();
|
||||
|
||||
// Click local clock
|
||||
await page.locator('[data-testid="conductor-modeOption-realtime"]').click();
|
||||
await expect.soft(pausePlayButton).not.toHaveClass(/is-paused/);
|
||||
|
||||
// Zoom in via button
|
||||
await zoomIntoImageryByButton(page);
|
||||
await expect(pausePlayButton).not.toHaveClass(/is-paused/);
|
||||
await expect.soft(pausePlayButton).not.toHaveClass(/is-paused/);
|
||||
const zoomInBtn = page.locator('.t-btn-zoom-in').nth(0);
|
||||
await zoomInBtn.click();
|
||||
// wait for zoom animation to finish
|
||||
await page.locator(backgroundImageSelector).hover({trial: true});
|
||||
|
||||
return expect(pausePlayButton).not.toHaveClass(/is-paused/);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -246,8 +247,10 @@ test('Example Imagery in Display layout', async ({ page, browserName }) => {
|
||||
await page.click('text=Example Imagery');
|
||||
|
||||
// Clear and set Image load delay to minimum value
|
||||
// FIXME: Update the value to 5000 ms when this bug is fixed.
|
||||
// See: https://github.com/nasa/openmct/issues/5265
|
||||
await page.locator('input[type="number"]').fill('');
|
||||
await page.locator('input[type="number"]').fill('5000');
|
||||
await page.locator('input[type="number"]').fill('0');
|
||||
|
||||
// Click text=OK
|
||||
await Promise.all([
|
||||
@@ -331,19 +334,6 @@ test('Example Imagery in Display layout', async ({ page, browserName }) => {
|
||||
|
||||
// Verify selected image is still displayed
|
||||
await expect(selectedImage).toBeVisible();
|
||||
|
||||
// Unpause imagery
|
||||
await page.locator('.pause-play').click();
|
||||
|
||||
//Get background-image url from background-image css prop
|
||||
await assertBackgroundImageUrlFromBackgroundCss(page);
|
||||
|
||||
// Open the image filter menu
|
||||
await page.locator('[role=toolbar] button[title="Brightness and contrast"]').click();
|
||||
|
||||
// Drag the brightness and contrast sliders around and assert filter values
|
||||
await dragBrightnessSliderAndAssertFilterValues(page);
|
||||
await dragContrastSliderAndAssertFilterValues(page);
|
||||
});
|
||||
|
||||
test.describe('Example imagery thumbnails resize in display layouts', () => {
|
||||
@@ -434,11 +424,6 @@ test.describe('Example imagery thumbnails resize in display layouts', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// 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('Clicking on the left arrow should pause the imagery and go to previous image');
|
||||
// test.fixme('If the imagery view is in pause mode, images still come in');
|
||||
// test.fixme('If the imagery view is not in pause mode, it should be updated when new images come in');
|
||||
test.describe('Example Imagery in Flexible layout', () => {
|
||||
test('Example Imagery in Flexible layout', async ({ page, browserName }) => {
|
||||
test.fixme(browserName === 'firefox', 'This test needs to be updated to work with firefox');
|
||||
@@ -649,6 +634,22 @@ async function assertBackgroundImageBrightness(page, expected) {
|
||||
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
|
||||
*/
|
||||
@@ -748,70 +749,3 @@ async function mouseZoomIn(page) {
|
||||
expect(imageMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height);
|
||||
expect(imageMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the '+' button to zoom in. Hovers first if the toolbar is not visible
|
||||
* and waits for the zoom animation to finish afterwards.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function zoomIntoImageryByButton(page) {
|
||||
// FIXME: There should only be one set of imagery buttons, but there are two?
|
||||
const zoomInBtn = page.locator("[role='toolbar'][aria-label='Image controls'] .t-btn-zoom-in").nth(0);
|
||||
const backgroundImage = page.locator(backgroundImageSelector);
|
||||
if (!(await zoomInBtn.isVisible())) {
|
||||
await backgroundImage.hover({trial: true});
|
||||
}
|
||||
|
||||
await zoomInBtn.click();
|
||||
await waitForAnimations(backgroundImage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the '-' button to zoom out. Hovers first if the toolbar is not visible
|
||||
* and waits for the zoom animation to finish afterwards.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function zoomOutOfImageryByButton(page) {
|
||||
// FIXME: There should only be one set of imagery buttons, but there are two?
|
||||
const zoomOutBtn = page.locator("[role='toolbar'][aria-label='Image controls'] .t-btn-zoom-out").nth(0);
|
||||
const backgroundImage = page.locator(backgroundImageSelector);
|
||||
if (!(await zoomOutBtn.isVisible())) {
|
||||
await backgroundImage.hover({trial: true});
|
||||
}
|
||||
|
||||
await zoomOutBtn.click();
|
||||
await waitForAnimations(backgroundImage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the reset button to reset image pan and zoom. Hovers first if the toolbar is not visible
|
||||
* and waits for the zoom animation to finish afterwards.
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function resetImageryPanAndZoom(page) {
|
||||
// FIXME: There should only be one set of imagery buttons, but there are two?
|
||||
const panZoomResetBtn = page.locator("[role='toolbar'][aria-label='Image controls'] .t-btn-zoom-reset").nth(0);
|
||||
const backgroundImage = page.locator(backgroundImageSelector);
|
||||
if (!(await panZoomResetBtn.isVisible())) {
|
||||
await backgroundImage.hover({trial: true});
|
||||
}
|
||||
|
||||
await panZoomResetBtn.click();
|
||||
await waitForAnimations(backgroundImage);
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ test.describe('Restricted Notebook', () => {
|
||||
});
|
||||
|
||||
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 }) => {
|
||||
@@ -52,15 +52,16 @@ test.describe('Restricted Notebook', () => {
|
||||
// Click Remove Text
|
||||
await page.locator('text=Remove').click();
|
||||
|
||||
// Click 'OK' on confirmation window and wait for save banner to appear
|
||||
//Wait until Save Banner is gone
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
page.locator('text=OK').click(),
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
|
||||
// has been deleted
|
||||
expect(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 }) => {
|
||||
@@ -68,7 +69,7 @@ test.describe('Restricted Notebook', () => {
|
||||
await enterTextEntry(page);
|
||||
|
||||
const commitButton = page.locator('button:has-text("Commit Entries")');
|
||||
expect(await commitButton.count()).toEqual(1);
|
||||
expect.soft(await commitButton.count()).toEqual(1);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -80,17 +81,11 @@ test.describe('Restricted Notebook with at least one entry and with the page loc
|
||||
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 +96,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 }) => {
|
||||
@@ -142,7 +139,7 @@ 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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -158,7 +155,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,7 +164,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');
|
||||
});
|
||||
|
||||
});
|
||||
@@ -235,18 +232,28 @@ async function lockPage(page) {
|
||||
await commitButton.click();
|
||||
|
||||
//Wait until Lock Banner is visible
|
||||
await page.locator('text=Lock Page').click();
|
||||
await Promise.all([
|
||||
page.locator('text=Lock Page').click(),
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
// Close Lock Banner
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
|
||||
//artifically wait to avoid mutation delay TODO: https://github.com/nasa/openmct/issues/5409
|
||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||
await page.waitForTimeout(1 * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('@playwright/test').Page} page
|
||||
*/
|
||||
async function openContextMenuRestrictedNotebook(page) {
|
||||
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 text=Open MCT My Items (This expands the My Items folder to show it's chilren in the tree)
|
||||
await page.locator('text=Open MCT My Items >> span').nth(3).click();
|
||||
|
||||
//artifically wait to avoid mutation delay TODO: https://github.com/nasa/openmct/issues/5409
|
||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||
await page.waitForTimeout(1 * 1000);
|
||||
|
||||
// Click a:has-text("Unnamed CUSTOM_NAME")
|
||||
await page.locator(`a:has-text("Unnamed ${CUSTOM_NAME}")`).click({
|
||||
|
||||
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 18 KiB |
@@ -28,12 +28,11 @@ 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');
|
||||
test('Displays empty div for missing stacked plot item', async ({ page }) => {
|
||||
const errorLogs = [];
|
||||
|
||||
page.on("console", (message) => {
|
||||
if (message.type() === 'warning' && message.text().includes('Missing domain object')) {
|
||||
if (message.type() === 'warning') {
|
||||
errorLogs.push(message.text());
|
||||
}
|
||||
});
|
||||
@@ -72,7 +71,7 @@ test.describe('Handle missing object for plots', () => {
|
||||
//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);
|
||||
await expect(errorLogs).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -95,6 +94,10 @@ async function makeStackedPlot(page) {
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
|
||||
//Wait until Save Banner is gone
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
||||
|
||||
// save the stacked plot
|
||||
await saveStackedPlot(page);
|
||||
|
||||
@@ -152,4 +155,7 @@ async function createSineWaveGenerator(page) {
|
||||
//Wait for Save Banner to appear
|
||||
page.waitForSelector('.c-message-banner__message')
|
||||
]);
|
||||
//Wait until Save Banner is gone
|
||||
await page.locator('.c-message-banner__close-button').click();
|
||||
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
|
||||
}
|
||||
|
||||
@@ -1,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
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -31,7 +31,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 { test } = require('@playwright/test');
|
||||
const percySnapshot = require('@percy/playwright');
|
||||
const path = require('path');
|
||||
const sinon = require('sinon');
|
||||
@@ -56,7 +56,7 @@ 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') });
|
||||
await page.addInitScript({ path: path.join(__dirname, '../plugins/notebook', './addRestrictedNotebook.js') });
|
||||
//Go to baseURL
|
||||
await page.goto('/', { waitUntil: 'networkidle' });
|
||||
//Click the Create button
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -24,8 +24,7 @@
|
||||
This test suite is dedicated to tests which verify search functionality.
|
||||
*/
|
||||
|
||||
const { test } = require('../../fixtures.js');
|
||||
const { expect } = require('@playwright/test');
|
||||
const { test, expect } = require('@playwright/test');
|
||||
const percySnapshot = require('@percy/playwright');
|
||||
|
||||
/**
|
||||
|
||||
@@ -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",
|
||||
|
||||
30
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "openmct",
|
||||
"version": "2.1.0-SNAPSHOT",
|
||||
"version": "2.0.5",
|
||||
"description": "The Open MCT core platform",
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "7.18.2",
|
||||
@@ -13,7 +13,7 @@
|
||||
"@types/karma": "^6.3.2",
|
||||
"@types/lodash": "^4.14.178",
|
||||
"@types/mocha": "^9.1.0",
|
||||
"babel-loader": "8.2.5",
|
||||
"babel-loader": "8.2.3",
|
||||
"babel-plugin-istanbul": "6.1.1",
|
||||
"comma-separated-values": "3.6.4",
|
||||
"codecov":"3.8.3",
|
||||
@@ -23,10 +23,10 @@
|
||||
"d3-axis": "3.0.0",
|
||||
"d3-scale": "3.3.0",
|
||||
"d3-selection": "3.0.0",
|
||||
"eslint": "8.18.0",
|
||||
"eslint": "8.13.0",
|
||||
"eslint-plugin-compat": "4.0.2",
|
||||
"eslint-plugin-playwright": "0.9.0",
|
||||
"eslint-plugin-vue": "9.1.1",
|
||||
"eslint-plugin-vue": "9.1.0",
|
||||
"eslint-plugin-you-dont-need-lodash-underscore": "6.12.0",
|
||||
"eventemitter3": "1.2.0",
|
||||
"express": "4.13.1",
|
||||
@@ -34,7 +34,7 @@
|
||||
"git-rev-sync": "3.0.2",
|
||||
"html2canvas": "1.4.1",
|
||||
"imports-loader": "0.8.0",
|
||||
"jasmine-core": "4.2.0",
|
||||
"jasmine-core": "4.1.1",
|
||||
"jsdoc": "3.5.5",
|
||||
"karma": "6.3.20",
|
||||
"karma-chrome-launcher": "3.1.1",
|
||||
@@ -42,7 +42,7 @@
|
||||
"karma-coverage": "2.2.0",
|
||||
"karma-coverage-istanbul-reporter": "3.0.3",
|
||||
"karma-firefox-launcher": "2.1.2",
|
||||
"karma-jasmine": "5.1.0",
|
||||
"karma-jasmine": "4.0.1",
|
||||
"karma-junit-reporter": "2.0.1",
|
||||
"karma-sourcemap-loader": "0.3.8",
|
||||
"karma-spec-reporter": "0.0.34",
|
||||
@@ -50,20 +50,20 @@
|
||||
"lighthouse": "9.6.1",
|
||||
"location-bar": "3.0.1",
|
||||
"lodash": "4.17.21",
|
||||
"mini-css-extract-plugin": "2.6.1",
|
||||
"moment": "2.29.4",
|
||||
"mini-css-extract-plugin": "2.6.0",
|
||||
"moment": "2.29.3",
|
||||
"moment-duration-format": "2.3.2",
|
||||
"moment-timezone": "0.5.34",
|
||||
"node-bourbon": "4.2.3",
|
||||
"painterro": "1.2.56",
|
||||
"nyc":"15.1.0",
|
||||
"painterro": "1.2.78",
|
||||
"plotly.js-basic-dist": "2.12.0",
|
||||
"plotly.js-gl2d-dist": "2.12.0",
|
||||
"printj": "1.3.1",
|
||||
"request": "2.88.2",
|
||||
"resolve-url-loader": "5.0.0",
|
||||
"sass": "1.52.2",
|
||||
"sass-loader": "13.0.2",
|
||||
"sass-loader": "12.6.0",
|
||||
"sinon": "14.0.0",
|
||||
"style-loader": "^1.0.1",
|
||||
"uuid": "8.3.2",
|
||||
@@ -72,7 +72,7 @@
|
||||
"vue-loader": "15.9.8",
|
||||
"vue-template-compiler": "2.6.14",
|
||||
"webpack": "5.68.0",
|
||||
"webpack-cli": "4.10.0",
|
||||
"webpack-cli": "4.9.2",
|
||||
"webpack-dev-middleware": "5.3.3",
|
||||
"webpack-hot-middleware": "2.25.1",
|
||||
"webpack-merge": "5.8.0"
|
||||
@@ -88,17 +88,17 @@
|
||||
"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 visual smoke branding default condition timeConductor clock persistence performance grandsearch tags",
|
||||
"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: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'",
|
||||
|
||||
@@ -196,7 +196,7 @@ define([
|
||||
* @memberof module:openmct.MCT#
|
||||
* @name telemetry
|
||||
*/
|
||||
['telemetry', () => new api.TelemetryAPI.default(this)],
|
||||
['telemetry', () => new api.TelemetryAPI(this)],
|
||||
|
||||
/**
|
||||
* An interface for creating new indicators and changing them dynamically.
|
||||
@@ -253,8 +253,7 @@ define([
|
||||
Object.defineProperty(this, apiName, {
|
||||
value: apiObject,
|
||||
enumerable: false,
|
||||
configurable: false,
|
||||
writable: true
|
||||
configurable: false
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -52,8 +52,8 @@ class MutableDomainObject {
|
||||
// Property should not be serialized
|
||||
enumerable: false
|
||||
},
|
||||
_callbacksForPaths: {
|
||||
value: {},
|
||||
_observers: {
|
||||
value: [],
|
||||
// Property should not be serialized
|
||||
enumerable: false
|
||||
},
|
||||
@@ -64,31 +64,15 @@ class MutableDomainObject {
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* BRAND new approach
|
||||
* - Register a listener on $_synchronize_model
|
||||
* - The $_synchronize_model event provides the path. Figure out whether the mutated path is equal to, or a parent of the observed path.
|
||||
* - If so, trigger callback with new value
|
||||
* - As an optimization, ONLY trigger if value has actually changed. Could be deferred until later?
|
||||
*/
|
||||
|
||||
$observe(path, callback) {
|
||||
let callbacksForPath = this._callbacksForPaths[path];
|
||||
if (callbacksForPath === undefined) {
|
||||
callbacksForPath = [];
|
||||
this._callbacksForPaths[path] = callbacksForPath;
|
||||
}
|
||||
let fullPath = qualifiedEventName(this, path);
|
||||
let eventOff =
|
||||
this._globalEventEmitter.off.bind(this._globalEventEmitter, fullPath, callback);
|
||||
|
||||
callbacksForPath.push(callback);
|
||||
|
||||
return function unlisten() {
|
||||
let index = callbacksForPath.indexOf(callback);
|
||||
callbacksForPath.splice(index, 1);
|
||||
if (callbacksForPath.length === 0) {
|
||||
delete this._callbacksForPaths[path];
|
||||
}
|
||||
}.bind(this);
|
||||
this._globalEventEmitter.on(fullPath, callback);
|
||||
this._observers.push(eventOff);
|
||||
|
||||
return eventOff;
|
||||
}
|
||||
$set(path, value) {
|
||||
_.set(this, path, value);
|
||||
@@ -104,14 +88,25 @@ class MutableDomainObject {
|
||||
this._globalEventEmitter.emit(ANY_OBJECT_EVENT, this);
|
||||
//Emit wildcard event, with path so that callback knows what changed
|
||||
this._globalEventEmitter.emit(qualifiedEventName(this, '*'), this, path, value);
|
||||
|
||||
//Emit events specific to properties affected
|
||||
let parentPropertiesList = path.split('.');
|
||||
for (let index = parentPropertiesList.length; index > 0; index--) {
|
||||
let parentPropertyPath = parentPropertiesList.slice(0, index).join('.');
|
||||
this._globalEventEmitter.emit(qualifiedEventName(this, parentPropertyPath), _.get(this, parentPropertyPath));
|
||||
}
|
||||
|
||||
//TODO: Emit events for listeners of child properties when parent changes.
|
||||
// Do it at observer time - also register observers for parent attribute path.
|
||||
}
|
||||
|
||||
$refresh(model) {
|
||||
const clone = JSON.parse(JSON.stringify(this));
|
||||
//TODO: Currently we are updating the entire object.
|
||||
// In the future we could update a specific property of the object using the 'path' parameter.
|
||||
this._globalEventEmitter.emit(qualifiedEventName(this, '$_synchronize_model'), model);
|
||||
|
||||
//Emit wildcard event
|
||||
this._globalEventEmitter.emit(qualifiedEventName(this, '*'), this, '*', this, clone);
|
||||
//Emit wildcard event, with path so that callback knows what changed
|
||||
this._globalEventEmitter.emit(qualifiedEventName(this, '*'), this);
|
||||
}
|
||||
|
||||
$on(event, callback) {
|
||||
@@ -119,53 +114,23 @@ class MutableDomainObject {
|
||||
|
||||
return () => this._instanceEventEmitter.off(event, callback);
|
||||
}
|
||||
$updateListenersOnPath(updatedModel, mutatedPath, newValue, oldModel) {
|
||||
const isRefresh = mutatedPath === '*';
|
||||
|
||||
Object.entries(this._callbacksForPaths).forEach(([observedPath, callbacks]) => {
|
||||
if (isChildOf(observedPath, mutatedPath)
|
||||
|| isParentOf(observedPath, mutatedPath)) {
|
||||
let newValueOfObservedPath;
|
||||
|
||||
if (observedPath === '*') {
|
||||
newValueOfObservedPath = updatedModel;
|
||||
|
||||
} else {
|
||||
newValueOfObservedPath = _.get(updatedModel, observedPath);
|
||||
}
|
||||
|
||||
if (isRefresh && observedPath !== '*') {
|
||||
const oldValueOfObservedPath = _.get(oldModel, observedPath);
|
||||
if (!_.isEqual(newValueOfObservedPath, oldValueOfObservedPath)) {
|
||||
callbacks.forEach(callback => callback(newValueOfObservedPath));
|
||||
}
|
||||
} else {
|
||||
//Assumed to be different if result of mutation.
|
||||
callbacks.forEach(callback => callback(newValueOfObservedPath));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
$synchronizeModel(updatedObject) {
|
||||
let clone = JSON.parse(JSON.stringify(updatedObject));
|
||||
utils.refresh(this, clone);
|
||||
}
|
||||
$destroy() {
|
||||
Object.keys(this._callbacksForPaths).forEach(key => delete this._callbacksForPaths[key]);
|
||||
while (this._observers.length > 0) {
|
||||
const observer = this._observers.pop();
|
||||
observer();
|
||||
}
|
||||
|
||||
this._instanceEventEmitter.emit('$_destroy');
|
||||
this._globalEventEmitter.off(qualifiedEventName(this, '$_synchronize_model'), this.$synchronizeModel);
|
||||
this._globalEventEmitter.off(qualifiedEventName(this, '*'), this.$updateListenersOnPath);
|
||||
}
|
||||
|
||||
static createMutable(object, mutationTopic) {
|
||||
let mutable = Object.create(new MutableDomainObject(mutationTopic));
|
||||
Object.assign(mutable, object);
|
||||
|
||||
mutable.$updateListenersOnPath = mutable.$updateListenersOnPath.bind(mutable);
|
||||
mutable.$synchronizeModel = mutable.$synchronizeModel.bind(mutable);
|
||||
|
||||
mutable._globalEventEmitter.on(qualifiedEventName(mutable, '$_synchronize_model'), mutable.$synchronizeModel);
|
||||
mutable._globalEventEmitter.on(qualifiedEventName(mutable, '*'), mutable.$updateListenersOnPath);
|
||||
mutable.$observe('$_synchronize_model', (updatedObject) => {
|
||||
let clone = JSON.parse(JSON.stringify(updatedObject));
|
||||
utils.refresh(mutable, clone);
|
||||
});
|
||||
|
||||
return mutable;
|
||||
}
|
||||
@@ -182,12 +147,4 @@ function qualifiedEventName(object, eventName) {
|
||||
return [keystring, eventName].join(':');
|
||||
}
|
||||
|
||||
function isChildOf(observedPath, mutatedPath) {
|
||||
return Boolean(mutatedPath === '*' || observedPath?.startsWith(mutatedPath));
|
||||
}
|
||||
|
||||
function isParentOf(observedPath, mutatedPath) {
|
||||
return Boolean(observedPath === '*' || mutatedPath?.startsWith(observedPath));
|
||||
}
|
||||
|
||||
export default MutableDomainObject;
|
||||
|
||||
@@ -233,11 +233,7 @@ export default class ObjectAPI {
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import ObjectAPI from './ObjectAPI.js';
|
||||
import { createOpenMct, resetApplicationState } from '../../utils/testing';
|
||||
|
||||
fdescribe("The Object API", () => {
|
||||
describe("The Object API", () => {
|
||||
let objectAPI;
|
||||
let typeRegistry;
|
||||
let openmct = {};
|
||||
@@ -287,167 +287,53 @@ fdescribe("The Object API", () => {
|
||||
mutableSecondInstance.$destroy();
|
||||
});
|
||||
|
||||
describe('on mutation', () => {
|
||||
it('to stay synchronized', function () {
|
||||
objectAPI.mutate(mutable, 'otherAttribute', 'new-attribute-value');
|
||||
expect(mutableSecondInstance.otherAttribute).toBe('new-attribute-value');
|
||||
});
|
||||
it('to stay synchronized when mutated', function () {
|
||||
objectAPI.mutate(mutable, 'otherAttribute', 'new-attribute-value');
|
||||
expect(mutableSecondInstance.otherAttribute).toBe('new-attribute-value');
|
||||
});
|
||||
|
||||
it('to indicate when a property changes', function () {
|
||||
let mutationCallback = jasmine.createSpy('mutation-callback');
|
||||
let unlisten;
|
||||
it('to indicate when a property changes', function () {
|
||||
let mutationCallback = jasmine.createSpy('mutation-callback');
|
||||
let unlisten;
|
||||
|
||||
return new Promise(function (resolve) {
|
||||
mutationCallback.and.callFake(resolve);
|
||||
unlisten = objectAPI.observe(mutableSecondInstance, 'otherAttribute', mutationCallback);
|
||||
objectAPI.mutate(mutable, 'otherAttribute', 'some-new-value');
|
||||
}).then(function () {
|
||||
expect(mutationCallback).toHaveBeenCalledWith('some-new-value');
|
||||
unlisten();
|
||||
});
|
||||
});
|
||||
|
||||
it('to indicate when a child property has changed', function () {
|
||||
let embeddedKeyCallback = jasmine.createSpy('embeddedKeyCallback');
|
||||
let embeddedObjectCallback = jasmine.createSpy('embeddedObjectCallback');
|
||||
let objectAttributeCallback = jasmine.createSpy('objectAttribute');
|
||||
let listeners = [];
|
||||
|
||||
return new Promise(function (resolve) {
|
||||
objectAttributeCallback.and.callFake(resolve);
|
||||
|
||||
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject.embeddedKey', embeddedKeyCallback));
|
||||
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject', embeddedObjectCallback));
|
||||
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute', objectAttributeCallback));
|
||||
|
||||
objectAPI.mutate(mutable, 'objectAttribute.embeddedObject.embeddedKey', 'updated-embedded-value');
|
||||
}).then(function () {
|
||||
expect(embeddedKeyCallback).toHaveBeenCalledWith('updated-embedded-value');
|
||||
expect(embeddedObjectCallback).toHaveBeenCalledWith({
|
||||
embeddedKey: 'updated-embedded-value'
|
||||
});
|
||||
expect(objectAttributeCallback).toHaveBeenCalledWith({
|
||||
embeddedObject: {
|
||||
embeddedKey: 'updated-embedded-value'
|
||||
}
|
||||
});
|
||||
|
||||
listeners.forEach(listener => listener());
|
||||
});
|
||||
});
|
||||
|
||||
it('to indicate when a parent property has changed', function () {
|
||||
let embeddedKeyCallback = jasmine.createSpy('embeddedKeyCallback');
|
||||
let embeddedObjectCallback = jasmine.createSpy('embeddedObjectCallback');
|
||||
let objectAttributeCallback = jasmine.createSpy('objectAttribute');
|
||||
let listeners = [];
|
||||
|
||||
return new Promise(function (resolve) {
|
||||
objectAttributeCallback.and.callFake(resolve);
|
||||
|
||||
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject.embeddedKey', embeddedKeyCallback));
|
||||
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject', embeddedObjectCallback));
|
||||
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute', objectAttributeCallback));
|
||||
|
||||
objectAPI.mutate(mutable, 'objectAttribute.embeddedObject', 'updated-embedded-value');
|
||||
}).then(function () {
|
||||
expect(embeddedKeyCallback).toHaveBeenCalledWith(undefined);
|
||||
expect(embeddedObjectCallback).toHaveBeenCalledWith('updated-embedded-value');
|
||||
expect(objectAttributeCallback).toHaveBeenCalledWith({
|
||||
embeddedObject: 'updated-embedded-value'
|
||||
});
|
||||
|
||||
listeners.forEach(listener => listener());
|
||||
});
|
||||
return new Promise(function (resolve) {
|
||||
mutationCallback.and.callFake(resolve);
|
||||
unlisten = objectAPI.observe(mutableSecondInstance, 'otherAttribute', mutationCallback);
|
||||
objectAPI.mutate(mutable, 'otherAttribute', 'some-new-value');
|
||||
}).then(function () {
|
||||
expect(mutationCallback).toHaveBeenCalledWith('some-new-value');
|
||||
unlisten();
|
||||
});
|
||||
});
|
||||
|
||||
describe('on refresh', () => {
|
||||
let refreshModel;
|
||||
it('to indicate when a child property has changed', function () {
|
||||
let embeddedKeyCallback = jasmine.createSpy('embeddedKeyCallback');
|
||||
let embeddedObjectCallback = jasmine.createSpy('embeddedObjectCallback');
|
||||
let objectAttributeCallback = jasmine.createSpy('objectAttribute');
|
||||
let listeners = [];
|
||||
|
||||
beforeEach(() => {
|
||||
refreshModel = JSON.parse(JSON.stringify(mutable));
|
||||
});
|
||||
return new Promise(function (resolve) {
|
||||
objectAttributeCallback.and.callFake(resolve);
|
||||
|
||||
it('to stay synchronized', function () {
|
||||
refreshModel.otherAttribute = 'new-attribute-value';
|
||||
mutable.$refresh(refreshModel);
|
||||
expect(mutableSecondInstance.otherAttribute).toBe('new-attribute-value');
|
||||
});
|
||||
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject.embeddedKey', embeddedKeyCallback));
|
||||
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject', embeddedObjectCallback));
|
||||
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute', objectAttributeCallback));
|
||||
|
||||
it('to indicate when a property changes', function () {
|
||||
let mutationCallback = jasmine.createSpy('mutation-callback');
|
||||
let unlisten;
|
||||
|
||||
return new Promise(function (resolve) {
|
||||
mutationCallback.and.callFake(resolve);
|
||||
unlisten = objectAPI.observe(mutableSecondInstance, 'otherAttribute', mutationCallback);
|
||||
refreshModel.otherAttribute = 'some-new-value';
|
||||
mutable.$refresh(refreshModel);
|
||||
}).then(function () {
|
||||
expect(mutationCallback).toHaveBeenCalledWith('some-new-value');
|
||||
unlisten();
|
||||
objectAPI.mutate(mutable, 'objectAttribute.embeddedObject.embeddedKey', 'updated-embedded-value');
|
||||
}).then(function () {
|
||||
expect(embeddedKeyCallback).toHaveBeenCalledWith('updated-embedded-value');
|
||||
expect(embeddedObjectCallback).toHaveBeenCalledWith({
|
||||
embeddedKey: 'updated-embedded-value'
|
||||
});
|
||||
});
|
||||
|
||||
it('to indicate when a child property has changed', function () {
|
||||
let embeddedKeyCallback = jasmine.createSpy('embeddedKeyCallback');
|
||||
let embeddedObjectCallback = jasmine.createSpy('embeddedObjectCallback');
|
||||
let objectAttributeCallback = jasmine.createSpy('objectAttribute');
|
||||
let listeners = [];
|
||||
|
||||
return new Promise(function (resolve) {
|
||||
objectAttributeCallback.and.callFake(resolve);
|
||||
|
||||
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject.embeddedKey', embeddedKeyCallback));
|
||||
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject', embeddedObjectCallback));
|
||||
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute', objectAttributeCallback));
|
||||
|
||||
refreshModel.objectAttribute.embeddedObject.embeddedKey = 'updated-embedded-value';
|
||||
mutable.$refresh(refreshModel);
|
||||
}).then(function () {
|
||||
expect(embeddedKeyCallback).toHaveBeenCalledWith('updated-embedded-value');
|
||||
expect(embeddedObjectCallback).toHaveBeenCalledWith({
|
||||
expect(objectAttributeCallback).toHaveBeenCalledWith({
|
||||
embeddedObject: {
|
||||
embeddedKey: 'updated-embedded-value'
|
||||
});
|
||||
expect(objectAttributeCallback).toHaveBeenCalledWith({
|
||||
embeddedObject: {
|
||||
embeddedKey: 'updated-embedded-value'
|
||||
}
|
||||
});
|
||||
|
||||
listeners.forEach(listener => listener());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('to indicate when a parent property has changed', function () {
|
||||
let embeddedKeyCallback = jasmine.createSpy('embeddedKeyCallback');
|
||||
let embeddedObjectCallback = jasmine.createSpy('embeddedObjectCallback');
|
||||
let objectAttributeCallback = jasmine.createSpy('objectAttribute');
|
||||
let listeners = [];
|
||||
|
||||
return new Promise(function (resolve) {
|
||||
objectAttributeCallback.and.callFake(resolve);
|
||||
|
||||
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject.embeddedKey', embeddedKeyCallback));
|
||||
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute.embeddedObject', embeddedObjectCallback));
|
||||
listeners.push(objectAPI.observe(mutableSecondInstance, 'objectAttribute', objectAttributeCallback));
|
||||
|
||||
refreshModel.objectAttribute.embeddedObject = 'updated-embedded-value';
|
||||
|
||||
mutable.$refresh(refreshModel);
|
||||
}).then(function () {
|
||||
expect(embeddedKeyCallback).toHaveBeenCalledWith(undefined);
|
||||
expect(embeddedObjectCallback).toHaveBeenCalledWith('updated-embedded-value');
|
||||
expect(objectAttributeCallback).toHaveBeenCalledWith({
|
||||
embeddedObject: 'updated-embedded-value'
|
||||
});
|
||||
|
||||
listeners.forEach(listener => listener());
|
||||
});
|
||||
listeners.forEach(listener => listener());
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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, <s 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 = 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,8 +337,6 @@ export default class TelemetryAPI {
|
||||
return this.handleMissingRequestProvider(domainObject);
|
||||
}
|
||||
|
||||
arguments[1] = await this.applyRequestInterceptors(domainObject, arguments[1]);
|
||||
|
||||
return provider.request.apply(provider, arguments)
|
||||
.catch((rejected) => {
|
||||
if (rejected.name !== 'AbortError') {
|
||||
@@ -289,7 +348,7 @@ export default class TelemetryAPI {
|
||||
}).finally(() => {
|
||||
this.requestAbortControllers.delete(abortController);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Subscribe to realtime telemetry for a specific domain object.
|
||||
@@ -305,7 +364,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 +401,7 @@ export default class TelemetryAPI {
|
||||
delete this.subscribeCache[keyString];
|
||||
}
|
||||
}.bind(this);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get telemetry metadata for a given domain object. Returns a telemetry
|
||||
@@ -351,7 +410,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 +426,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 +453,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 +469,7 @@ export default class TelemetryAPI {
|
||||
}
|
||||
|
||||
return this.valueFormatterCache.get(valueMetadata);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a value formatter for a given key.
|
||||
@@ -418,9 +477,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 +487,7 @@ export default class TelemetryAPI {
|
||||
*
|
||||
* @returns {Object<String, {TelemetryValueFormatter}>}
|
||||
*/
|
||||
getFormatMap(metadata) {
|
||||
TelemetryAPI.prototype.getFormatMap = function (metadata) {
|
||||
if (!metadata) {
|
||||
return {};
|
||||
}
|
||||
@@ -443,14 +502,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';
|
||||
@@ -473,15 +532,15 @@ export default class TelemetryAPI {
|
||||
console.warn(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 +558,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 +578,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 +598,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 +607,7 @@ export default class TelemetryAPI {
|
||||
}
|
||||
|
||||
return provider.getLimitEvaluator(domainObject);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a limit definitions for this domain object.
|
||||
@@ -577,7 +636,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 +647,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;
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
*
|
||||
@@ -127,8 +127,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,6 +169,7 @@ export default class TelemetryCollection extends EventEmitter {
|
||||
* @private
|
||||
*/
|
||||
_processNewTelemetry(telemetryData) {
|
||||
performance.mark('tlm:process:start');
|
||||
if (telemetryData === undefined) {
|
||||
return;
|
||||
}
|
||||
@@ -388,6 +388,7 @@ export default class TelemetryCollection extends EventEmitter {
|
||||
* @todo handle subscriptions more granually
|
||||
*/
|
||||
_reset() {
|
||||
performance.mark('tlm:reset');
|
||||
this.boundedTelemetry = [];
|
||||
this.futureBuffer = [];
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -197,7 +197,7 @@ export default {
|
||||
}
|
||||
},
|
||||
setUnit() {
|
||||
this.unit = this.valueMetadata ? this.valueMetadata.unit : '';
|
||||
this.unit = this.valueMetadata.unit || '';
|
||||
},
|
||||
firstNonDomainAttribute(metadata) {
|
||||
return metadata
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ describe("The LAD Table", () => {
|
||||
// add another telemetry object as composition in lad table to test multi rows
|
||||
mockObj.ladTable.composition.push(anotherTelemetryObj.identifier);
|
||||
|
||||
beforeEach(async () => {
|
||||
beforeEach(async (done) => {
|
||||
let telemetryRequestResolve;
|
||||
let telemetryObjectResolve;
|
||||
let anotherTelemetryObjectResolve;
|
||||
@@ -204,6 +204,8 @@ describe("The LAD Table", () => {
|
||||
|
||||
await Promise.all([telemetryRequestPromise, telemetryObjectPromise, anotherTelemetryObjectPromise]);
|
||||
await Vue.nextTick();
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it("should show one row per object in the composition", () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -23,7 +23,12 @@
|
||||
<template>
|
||||
<div
|
||||
class="c-fault-mgmt__list data-selectable"
|
||||
:class="classesFromState"
|
||||
:class="[
|
||||
{'is-selected': isSelected},
|
||||
{'is-unacknowledged': !fault.acknowledged},
|
||||
{'is-shelved': fault.shelved},
|
||||
{'is-acknowledged': fault.acknowledged}
|
||||
]"
|
||||
>
|
||||
<div class="c-fault-mgmt-item c-fault-mgmt__list-checkbox">
|
||||
<input
|
||||
@@ -108,36 +113,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') {
|
||||
|
||||
@@ -96,19 +96,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) {
|
||||
|
||||
@@ -169,7 +169,6 @@
|
||||
</g>
|
||||
<g class="c-dial__text">
|
||||
<text
|
||||
v-if="displayUnits"
|
||||
x="50%"
|
||||
y="70%"
|
||||
text-anchor="middle"
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -49,7 +49,6 @@ describe("the plugin", () => {
|
||||
let parentObject;
|
||||
let parentObjectPath;
|
||||
let changedParentObject;
|
||||
let unobserve;
|
||||
beforeEach((done) => {
|
||||
parentObject = {
|
||||
name: 'mock folder',
|
||||
@@ -74,7 +73,7 @@ describe("the plugin", () => {
|
||||
});
|
||||
});
|
||||
|
||||
unobserve = openmct.objects.observe(parentObject, '*', (newObject) => {
|
||||
openmct.objects.observe(parentObject, '*', (newObject) => {
|
||||
changedParentObject = newObject;
|
||||
|
||||
done();
|
||||
@@ -82,9 +81,6 @@ describe("the plugin", () => {
|
||||
|
||||
newFolderAction.invoke(parentObjectPath);
|
||||
});
|
||||
afterEach(() => {
|
||||
unobserve();
|
||||
});
|
||||
|
||||
it('creates a new folder object', () => {
|
||||
expect(openmct.objects.save).toHaveBeenCalled();
|
||||
|
||||
@@ -296,17 +296,12 @@ export default {
|
||||
window.addEventListener('orientationchange', this.formatSidebar);
|
||||
window.addEventListener('hashchange', this.setSectionAndPageFromUrl);
|
||||
this.filterAndSortEntries();
|
||||
this.unlistenToEntryChanges = this.openmct.objects.observe(this.domainObject, "configuration.entries", () => this.filterAndSortEntries());
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.unlisten) {
|
||||
this.unlisten();
|
||||
}
|
||||
|
||||
if (this.unlistenToEntryChanges) {
|
||||
this.unlistenToEntryChanges();
|
||||
}
|
||||
|
||||
window.removeEventListener('orientationchange', this.formatSidebar);
|
||||
window.removeEventListener('hashchange', this.setSectionAndPageFromUrl);
|
||||
},
|
||||
|
||||
@@ -233,13 +233,6 @@ export default {
|
||||
},
|
||||
mounted() {
|
||||
this.dropOnEntry = this.dropOnEntry.bind(this);
|
||||
this.$on('tags-updated', async () => {
|
||||
const user = await this.openmct.user.getCurrentUser();
|
||||
this.entry.modified = Date.now();
|
||||
this.entry.modifiedBy = user.getId();
|
||||
|
||||
this.$emit('updateEntry', this.entry);
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
async addNewEmbed(objectPath) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -215,8 +215,6 @@ class CouchObjectProvider {
|
||||
// Network error, CouchDB unreachable.
|
||||
if (response === null) {
|
||||
this.indicator.setIndicatorToState(DISCONNECTED);
|
||||
console.error(error.message);
|
||||
throw new Error(`CouchDB Error - No response"`);
|
||||
}
|
||||
|
||||
console.error(error.message);
|
||||
@@ -381,8 +379,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);
|
||||
}
|
||||
@@ -651,7 +647,6 @@ 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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
export default function stackedPlotConfigurationInterceptor(openmct) {
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -138,25 +129,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ describe("the RemoteClock plugin", () => {
|
||||
parse: (datum) => datum.key
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
beforeEach((done) => {
|
||||
openmct.install(openmct.plugins.RemoteClock(TIME_TELEMETRY_ID));
|
||||
|
||||
let clocks = openmct.time.getAllClocks();
|
||||
@@ -113,7 +113,9 @@ describe("the RemoteClock plugin", () => {
|
||||
end: OFFSET_END
|
||||
});
|
||||
|
||||
await Promise.all([objectPromiseResolve, requestPromise]);
|
||||
Promise.all([objectPromiseResolve, requestPromise])
|
||||
.then(done)
|
||||
.catch(done);
|
||||
});
|
||||
|
||||
it('is available and sets up initial values and listeners', () => {
|
||||
|
||||
@@ -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;
|
||||
@@ -78,7 +78,7 @@ class StaticModelProvider {
|
||||
}
|
||||
|
||||
parseTreeLeaf(leafKey, leafValue, idMap, namespace) {
|
||||
if (leafValue === null || leafValue === undefined) {
|
||||
if (!leafValue) {
|
||||
return leafValue;
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -78,15 +78,13 @@ describe("the plugin", () => {
|
||||
|
||||
describe('when invoked', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach((done) => {
|
||||
openmct.overlays.overlay = function (options) {};
|
||||
|
||||
spyOn(openmct.overlays, 'overlay');
|
||||
|
||||
viewDatumAction.invoke(mockObjectPath, mockView);
|
||||
});
|
||||
|
||||
it('creates an overlay', () => {
|
||||
expect(openmct.overlays.overlay).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -133,11 +133,8 @@ export default {
|
||||
this.addedTags.push(newTagValue);
|
||||
this.userAddingTag = true;
|
||||
},
|
||||
async tagRemoved(tagToRemove) {
|
||||
const result = await this.openmct.annotation.removeAnnotationTag(this.annotation, tagToRemove);
|
||||
this.$emit('tags-updated');
|
||||
|
||||
return result;
|
||||
tagRemoved(tagToRemove) {
|
||||
return this.openmct.annotation.removeAnnotationTag(this.annotation, tagToRemove);
|
||||
},
|
||||
async tagAdded(newTag) {
|
||||
const annotationWasCreated = this.annotation === null || this.annotation === undefined;
|
||||
@@ -149,7 +146,6 @@ export default {
|
||||
|
||||
this.tagsChanged(this.annotation.tags);
|
||||
this.userAddingTag = false;
|
||||
this.$emit('tags-updated');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -2,12 +2,10 @@
|
||||
// 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');
|
||||
|
||||
const vueLoaderRule = config.module.rules.find(r => r.use === 'vue-loader');
|
||||
|
||||
vueLoaderRule.use = {
|
||||
loader: 'vue-loader'
|
||||
|
||||
@@ -6,17 +6,6 @@ const webpack = require('webpack');
|
||||
|
||||
module.exports = merge(common, {
|
||||
mode: 'development',
|
||||
watchOptions: {
|
||||
// Since we use require.context, webpack is watching the entire directory.
|
||||
// We need to exclude any files we don't want webpack to watch.
|
||||
// See: https://webpack.js.org/configuration/watch/#watchoptions-exclude
|
||||
ignored: [
|
||||
'**/{node_modules,dist,docs,e2e}', // All files in node_modules, dist, docs, e2e,
|
||||
'**/{*.yml,Procfile,webpack*.js,babel*.js,package*.json,tsconfig.json,jsdoc.json}', // Config files
|
||||
'**/*.{sh,md,png,ttf,woff,svg}', // Non source files
|
||||
'**/.*' // dotfiles and dotfolders
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"vue": path.join(__dirname, "node_modules/vue/dist/vue.js")
|
||||
|
||||