Compare commits

..

4 Commits

42 changed files with 181 additions and 299 deletions

View File

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

View File

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

31
app.js
View File

@@ -12,7 +12,6 @@ const express = require('express');
const app = express();
const fs = require('fs');
const request = require('request');
const __DEV__ = process.env.NODE_ENV === 'development';
// Defaults
options.port = options.port || options.p || 8080;
@@ -50,18 +49,14 @@ class WatchRunPlugin {
}
const webpack = require('webpack');
let webpackConfig;
if (__DEV__) {
webpackConfig = require('./webpack.dev');
webpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin());
webpackConfig.entry.openmct = [
'webpack-hot-middleware/client?reload=true',
webpackConfig.entry.openmct
];
webpackConfig.plugins.push(new WatchRunPlugin());
} else {
webpackConfig = require('./webpack.coverage');
}
const webpackConfig = 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) {

View File

@@ -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: [
{

View File

@@ -12,10 +12,10 @@ const config = {
testIgnore: '**/*.perf.spec.js',
timeout: 30 * 1000,
webServer: {
command: 'cross-env NODE_ENV=test npm run start',
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: [
{

View File

@@ -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',

View File

@@ -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'],

View File

@@ -36,7 +36,7 @@ test.describe('Branding tests', () => {
await page.click('.l-shell__app-logo');
// Verify that the NASA Logo Appears
await expect(page.locator('.c-about__image')).toBeVisible();
await expect(await page.locator('.c-about__image')).toBeVisible();
// Modify the Build information in 'about' Modal
const versionInformationLocator = page.locator('ul.t-info.l-info.s-info');

View File

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

View File

@@ -55,13 +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
@@ -73,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([
@@ -84,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 }) => {
@@ -110,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([
@@ -134,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

View File

@@ -172,8 +172,7 @@ test.describe('Example Imagery Object', () => {
});
test('Can use the reset button to reset the image', async ({ page }, testInfo) => {
test.slow(testInfo.project === 'chrome-beta', "This test is slow in chrome-beta");
test('Can use the reset button to reset the image', async ({ page }) => {
// wait for zoom animation to finish
await page.locator(backgroundImageSelector).hover({trial: true});
@@ -192,17 +191,16 @@ test.describe('Example Imagery Object', () => {
expect.soft(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
expect.soft(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);
await zoomResetBtn.click();
// wait for zoom animation to finish
// FIXME: The zoom is flakey, sometimes not returning to original dimensions
// https://github.com/nasa/openmct/issues/5491
await expect.poll(async () => {
await zoomResetBtn.click();
const boundingBox = await page.locator(backgroundImageSelector).boundingBox();
await page.locator(backgroundImageSelector).hover({trial: true});
return boundingBox;
}, {
timeout: 10 * 1000
}).toEqual(initialBoundingBox);
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 }) => {

View File

@@ -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({

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -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'});
}

View File

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

View File

@@ -52,6 +52,9 @@ test('Generate Visual Test Data @localStorage', async ({ page, context }) => {
//Wait for Save Banner to appear1
page.waitForSelector('.c-message-banner__message')
]);
//Wait until Save Banner is gone
await page.locator('.c-message-banner__close-button').click();
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
// save (exit edit mode)
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
@@ -66,12 +69,18 @@ test('Generate Visual Test Data @localStorage', async ({ page, context }) => {
//Add a 5000 ms Delay
await page.locator('[aria-label="Loading Delay \\(ms\\)"]').fill('5000');
// Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184
await page.click('form[name="mctForm"] a:has-text("Overlay Plot")');
await Promise.all([
page.waitForNavigation(),
page.locator('text=OK').click(),
//Wait for Save Banner to appear1
page.waitForSelector('.c-message-banner__message')
]);
//Wait until Save Banner is gone
await page.locator('.c-message-banner__close-button').click();
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
// focus the overlay plot
await page.locator('text=Open MCT My Items >> span').nth(3).click();

View File

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

View File

@@ -88,8 +88,8 @@
"build:coverage": "webpack --config webpack.coverage.js",
"build:watch": "webpack --config webpack.dev.js --watch",
"info": "npx envinfo --system --browsers --npmPackages --binaries --languages --markdown",
"test": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run",
"test:firefox": "cross-env NODE_ENV=test NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run --browsers=FirefoxHeadless",
"test": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run",
"test:firefox": "cross-env NODE_OPTIONS=\"--max_old_space_size=4096\" karma start --single-run --browsers=FirefoxHeadless",
"test:debug": "cross-env NODE_ENV=debug karma start --no-single-run",
"test:e2e": "npx playwright test",
"test:e2e:ci": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome smoke branding default condition timeConductor clock exampleImagery persistence performance grandsearch notebook/tags",
@@ -98,7 +98,7 @@
"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'",

View File

@@ -230,7 +230,6 @@ export default class ObjectAPI {
return result;
}).catch((result) => {
console.warn(`Failed to retrieve ${keystring}:`, result);
this.openmct.notifications.error(`Failed to retrieve object ${keystring}`);
delete this.cache[keystring];
@@ -388,13 +387,7 @@ export default class ObjectAPI {
}
}
return result.catch((error) => {
if (error instanceof this.errors.Conflict) {
this.openmct.notifications.error(`Conflict detected while saving ${this.makeKeyString(domainObject.identifier)}`);
}
throw error;
});
return result;
}
/**

View File

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

View File

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

View File

@@ -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;
}
}
}

View File

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

View File

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

View File

@@ -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') {

View File

@@ -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) {

View File

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

View File

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

View File

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

View File

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

View File

@@ -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>

View File

@@ -217,11 +217,9 @@ class CouchObjectProvider {
this.indicator.setIndicatorToState(DISCONNECTED);
console.error(error.message);
throw new Error(`CouchDB Error - No response"`);
} else {
console.error(error.message);
throw error;
}
console.error(error.message);
}
}
@@ -655,6 +653,7 @@ class CouchObjectProvider {
let document = new CouchDocument(key, queued.model);
document.metadata.created = Date.now();
this.request(key, "PUT", document).then((response) => {
console.log('create check response', key);
this.#checkResponse(response, queued.intermediateResponse, key);
}).catch(error => {
queued.intermediateResponse.reject(error);

View File

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

View File

@@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
export default function stackedPlotConfigurationInterceptor(openmct) {
export default function plotInterceptor(openmct) {
openmct.objects.addGetInterceptor({
appliesTo: (identifier, domainObject) => {

View File

@@ -27,7 +27,7 @@ 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";
import plotInterceptor from "./plotInterceptor";
export default function () {
return function install(openmct) {
@@ -65,7 +65,7 @@ export default function () {
priority: 890
});
stackedPlotConfigurationInterceptor(openmct);
plotInterceptor(openmct);
openmct.objectViews.addProvider(new StackedPlotViewProvider(openmct));
openmct.objectViews.addProvider(new OverlayPlotViewProvider(openmct));

View File

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

View File

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

View File

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

View File

@@ -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'