Compare commits

..

1 Commits

Author SHA1 Message Date
Nikhil Mandlik
3b7d4e8d0c [Build] Add support for node18 2022-04-19 14:51:15 -07:00
186 changed files with 1077 additions and 8383 deletions

View File

@@ -2,7 +2,7 @@ version: 2.1
executors:
pw-focal-development:
docker:
- image: mcr.microsoft.com/playwright:v1.21.1-focal
- image: mcr.microsoft.com/playwright:v1.19.2-focal
environment:
NODE_ENV: development # Needed to ensure 'dist' folder created and devDependencies installed
parameters:
@@ -23,7 +23,7 @@ commands:
- node/install:
install-npm: true
node-version: << parameters.node-version >>
- run: npm install --prefer-offline --no-audit --progress=false
- run: npm install
restore_cache_cmd:
description: "Custom command for restoring cache with the ability to bust cache. When BUST_CACHE is set to true, jobs will not restore cache"
parameters:
@@ -64,7 +64,7 @@ commands:
- run: curl -Os https://uploader.codecov.io/latest/linux/codecov;chmod +x codecov;./codecov
orbs:
node: circleci/node@4.9.0
browser-tools: circleci/browser-tools@1.3.0
browser-tools: circleci/browser-tools@1.2.3
jobs:
npm-audit:
parameters:
@@ -128,30 +128,16 @@ jobs:
suite:
type: string
executor: pw-focal-development
parallelism: 4
steps:
- build_and_install:
node-version: <<parameters.node-version>>
- run: SHARD="$((${CIRCLE_NODE_INDEX}+1))"; npm run test:e2e:<<parameters.suite>> -- --shard=${SHARD}/${CIRCLE_NODE_TOTAL}
- run: npx playwright install
- run: npm run test:e2e:<<parameters.suite>>
- store_test_results:
path: test-results/results.xml
- store_artifacts:
path: test-results
- generate_and_store_version_and_filesystem_artifacts
perf-test:
parameters:
node-version:
type: string
executor: pw-focal-development
steps:
- build_and_install:
node-version: <<parameters.node-version>>
- run: npm run test:perf
- store_test_results:
path: test-results/results.xml
- store_artifacts:
path: test-results
- generate_and_store_version_and_filesystem_artifacts
workflows:
overall-circleci-commit-status: #These jobs run on every commit
jobs:
@@ -164,6 +150,10 @@ workflows:
browser: ChromeHeadless
post-steps:
- upload_code_covio
- unit-test:
name: node16-chrome
node-version: lts/gallium
browser: ChromeHeadless
- unit-test:
name: node18-chrome
node-version: "18"
@@ -172,8 +162,6 @@ workflows:
name: e2e-ci
node-version: lts/gallium
suite: ci
- perf-test:
node-version: lts/gallium
the-nightly: #These jobs do not run on PRs, but against master at night
jobs:
- unit-test:

View File

@@ -29,7 +29,6 @@ module.exports = {
"you-dont-need-lodash-underscore/omit": "off",
"you-dont-need-lodash-underscore/throttle": "off",
"you-dont-need-lodash-underscore/flatten": "off",
"you-dont-need-lodash-underscore/get": "off",
"no-bitwise": "error",
"curly": "error",
"eqeqeq": "error",

View File

@@ -27,7 +27,7 @@ assignees: ''
#### Environment
<!--- If encountered on local machine, execute the following:
<!--- npx envinfo --system --browsers --npmPackages --binaries --markdown -->
<!--- npx envinfo --system --browsers --npmPackages --binaries --languages --markdown -->
* Open MCT Version: <!--- date of build, version, or SHA -->
* Deployment Type: <!--- npm dev? VIPER Dev? openmct-yamcs? -->
* OS:
@@ -40,8 +40,6 @@ assignees: ''
- [ ] Is there a workaround available?
- [ ] Does this impact a critical component?
- [ ] Is this just a visual bug with no functional impact?
- [ ] Does this block the execution of e2e tests?
- [ ] Does this have an impact on Performance?
#### Additional Information
<!--- Include any screenshots, gifs, or logs which will expedite triage -->

View File

@@ -16,7 +16,7 @@ Closes <!--- Insert Issue Number(s) this PR addresses. Start by typing # will op
* [ ] Unit tests included and/or updated with changes?
* [ ] Command line build passes?
* [ ] Has this been smoke tested?
* [ ] Testing instructions included in associated issue OR is this a dependency/testcase change?
* [ ] Testing instructions included in associated issue?
### Reviewer Checklist

View File

@@ -32,12 +32,12 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
uses: github/codeql-action/init@v1
with:
languages: javascript
- name: Autobuild
uses: github/codeql-action/autobuild@v2
uses: github/codeql-action/autobuild@v1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
uses: github/codeql-action/analyze@v1

View File

@@ -30,7 +30,7 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version: '16'
- run: npx playwright@1.21.1 install
- run: npx playwright@1.19.2 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.21.1 install
- run: npx playwright@1.19.2 install
- run: npm install
- name: Run the e2e visual tests
run: npm run test:e2e:visual

View File

@@ -1,12 +1,4 @@
/* eslint-disable no-undef */
module.exports = {
"extends": ["plugin:playwright/playwright-test"],
"overrides": [
{
"files": ["tests/visual/*.spec.js"],
"rules": {
"playwright/no-wait-for-timeout": "off"
}
}
]
"extends": ["plugin:playwright/playwright-test"]
};

View File

@@ -1,27 +0,0 @@
/* eslint-disable no-undef */
// This file extends the base functionality of the playwright test framework
const base = require('@playwright/test');
const { expect } = require('@playwright/test');
exports.test = base.test.extend({
page: async ({ baseURL, page }, use) => {
const messages = [];
page.on('console', msg => messages.push(`[${msg.type()}] ${msg.text()}`));
await use(page);
await expect.soft(messages.toString()).not.toContain('[error]');
},
browser: async ({ playwright, browser }, use, workerInfo) => {
// Use browserless if configured
if (workerInfo.project.name.match(/browserless/)) {
const vBrowser = await playwright.chromium.connectOverCDP({
endpointURL: 'ws://localhost:3003'
});
await use(vBrowser);
} else {
// Use Local Browser for testing.
await use(browser);
}
}
});

View File

@@ -2,41 +2,38 @@
// playwright.config.js
// @ts-check
// eslint-disable-next-line no-unused-vars
const { devices } = require('@playwright/test');
/** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = {
retries: 1,
retries: 2,
testDir: 'tests',
testIgnore: '**/*.perf.spec.js', //Ignore performance tests and define in playwright-perfromance.config.js
timeout: 60 * 1000,
timeout: 90 * 1000,
webServer: {
command: 'npm run start',
port: 8080,
timeout: 200 * 1000,
reuseExistingServer: !process.env.CI
},
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: 'on-first-retry'
screenshot: 'on',
trace: 'on',
video: 'on'
},
projects: [
{
name: 'chrome',
use: {
browserName: 'chromium'
browserName: 'chromium',
...devices['Desktop Chrome']
}
},
{
name: 'MMOC',
grepInvert: /@snapshot/,
use: {
browserName: 'chromium',
viewport: {
@@ -55,11 +52,8 @@ const config = {
],
reporter: [
['list'],
['html', {
open: 'never',
outputFolder: '../test-results/html/'
}],
['junit', { outputFile: 'test-results/results.xml' }],
['allure-playwright'],
['github']
]
};

View File

@@ -2,14 +2,12 @@
// playwright.config.js
// @ts-check
// eslint-disable-next-line no-unused-vars
const { devices } = require('@playwright/test');
/** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = {
retries: 0,
testDir: 'tests',
testIgnore: '**/*.perf.spec.js',
timeout: 30 * 1000,
webServer: {
command: 'npm run start',
@@ -23,20 +21,20 @@ const config = {
baseURL: 'http://localhost:8080/',
headless: false,
ignoreHTTPSErrors: true,
screenshot: 'only-on-failure',
trace: 'retain-on-failure',
video: 'retain-on-failure'
screenshot: 'on',
trace: 'on',
video: 'on'
},
projects: [
{
name: 'chrome',
use: {
browserName: 'chromium'
browserName: 'chromium',
...devices['Desktop Chrome']
}
},
{
name: 'MMOC',
grepInvert: /@snapshot/,
use: {
browserName: 'chromium',
viewport: {
@@ -55,10 +53,7 @@ const config = {
],
reporter: [
['list'],
['html', {
open: 'on-failure',
outputFolder: '../test-results'
}]
['allure-playwright']
]
};

View File

@@ -1,41 +0,0 @@
/* eslint-disable no-undef */
// playwright.config.js
// @ts-check
/** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = {
retries: 0,
testDir: 'tests/performance/',
timeout: 30 * 1000,
workers: 1, //Only run in serial with 1 worker
webServer: {
command: 'npm run start',
port: 8080,
timeout: 200 * 1000,
reuseExistingServer: !process.env.CI
},
use: {
browserName: "chromium",
baseURL: 'http://localhost:8080/',
headless: Boolean(process.env.CI), //Only if running locally
ignoreHTTPSErrors: true,
screenshot: 'off',
trace: 'off',
video: 'off'
},
projects: [
{
name: 'chrome',
use: {
browserName: 'chromium'
}
}
],
reporter: [
['list'],
['junit', { outputFile: 'test-results/results.xml' }],
['json', { outputFile: 'test-results/results.json' }]
]
};
module.exports = config;

View File

@@ -4,10 +4,10 @@
/** @type {import('@playwright/test').PlaywrightTestConfig} */
const config = {
retries: 0, // visual tests should never retry due to snapshot comparison errors
testDir: 'tests/visual',
retries: 0,
testDir: 'tests',
timeout: 90 * 1000,
workers: 1, // visual tests should never run in parallel due to test pollution
workers: 1,
webServer: {
command: 'npm run start',
port: 8080,
@@ -17,7 +17,7 @@ const config = {
use: {
browserName: "chromium",
baseURL: 'http://localhost:8080/',
headless: true, // this needs to remain headless to avoid visual changes due to GPU
headless: true,
ignoreHTTPSErrors: true,
screenshot: 'on',
trace: 'off',
@@ -25,7 +25,8 @@ const config = {
},
reporter: [
['list'],
['junit', { outputFile: 'test-results/results.xml' }]
['junit', { outputFile: 'test-results/results.xml' }],
['allure-playwright']
]
};

View File

@@ -1 +0,0 @@
{"openmct":{"21338566-d472-4377-aed1-21b79272c8de":{"identifier":{"key":"21338566-d472-4377-aed1-21b79272c8de","namespace":""},"name":"Performance Display Layout","type":"layout","composition":[{"key":"644c2e47-2903-475f-8a4a-6be1588ee02f","namespace":""}],"configuration":{"items":[{"width":32,"height":18,"x":1,"y":1,"identifier":{"key":"644c2e47-2903-475f-8a4a-6be1588ee02f","namespace":""},"hasFrame":true,"fontSize":"default","font":"default","type":"subobject-view","id":"5aeb5a71-3149-41ed-9d8a-d34b0a18b053"}],"layoutGrid":[10,10]},"modified":1652228997384,"location":"mine","persisted":1652228997384},"644c2e47-2903-475f-8a4a-6be1588ee02f":{"identifier":{"key":"644c2e47-2903-475f-8a4a-6be1588ee02f","namespace":""},"name":"Performance Example Imagery","type":"example.imagery","configuration":{"imageLocation":"","imageLoadDelayInMilliSeconds":20000,"imageSamples":[]},"telemetry":{"values":[{"name":"Name","key":"name"},{"name":"Time","key":"utc","format":"utc","hints":{"domain":2}},{"name":"Local Time","key":"local","format":"local-format","hints":{"domain":1}},{"name":"Image","key":"url","format":"image","hints":{"image":1}},{"name":"Image Download Name","key":"imageDownloadName","format":"imageDownloadName","hints":{"imageDownloadName":1}}]},"modified":1652228997375,"location":"21338566-d472-4377-aed1-21b79272c8de","persisted":1652228997375}},"rootId":"21338566-d472-4377-aed1-21b79272c8de"}

View File

@@ -1 +0,0 @@
{"openmct":{"6d2fa9fd-f2aa-461a-a1e1-164ac44bec9d":{"identifier":{"key":"6d2fa9fd-f2aa-461a-a1e1-164ac44bec9d","namespace":""},"name":"Performance Notebook","type":"notebook","configuration":{"defaultSort":"oldest","entries":{"3e31c412-33ba-4757-8ade-e9821f6ba321":{"8c8f6035-631c-45af-8c24-786c60295335":[{"id":"entry-1652815305457","createdOn":1652815305457,"createdBy":"","text":"Existing Entry 1","embeds":[]},{"id":"entry-1652815313465","createdOn":1652815313465,"createdBy":"","text":"Existing Entry 2","embeds":[]},{"id":"entry-1652815399955","createdOn":1652815399955,"createdBy":"","text":"Existing Entry 3","embeds":[]}]}},"imageMigrationVer":"v1","pageTitle":"Page","sections":[{"id":"3e31c412-33ba-4757-8ade-e9821f6ba321","isDefault":false,"isSelected":false,"name":"Section1","pages":[{"id":"8c8f6035-631c-45af-8c24-786c60295335","isDefault":false,"isSelected":false,"name":"Page1","pageTitle":"Page"},{"id":"36555942-c9aa-439c-bbdb-0aaf50db50f5","isDefault":false,"isSelected":false,"name":"Page2","pageTitle":"Page"}],"sectionTitle":"Section"},{"id":"dab0bd1d-2c5a-405c-987f-107123d6189a","isDefault":false,"isSelected":true,"name":"Section2","pages":[{"id":"f625a86a-cb99-4898-8082-80543c8de534","isDefault":false,"isSelected":false,"name":"Page1","pageTitle":"Page"},{"id":"e77ef810-f785-42a7-942e-07e999b79c59","isDefault":false,"isSelected":true,"name":"Page2","pageTitle":"Page"}],"sectionTitle":"Section"}],"sectionTitle":"Section","type":"General","showTime":"0"},"modified":1652815915219,"location":"mine","persisted":1652815915222}},"rootId":"6d2fa9fd-f2aa-461a-a1e1-164ac44bec9d"}

View File

@@ -1,77 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
This test suite is dedicated to tests which verify form functionality.
*/
const { test, expect } = require('@playwright/test');
const TEST_FOLDER = 'test folder';
test.describe('forms set', () => {
test('New folder form has title as required field', async ({ page }) => {
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
// Click button:has-text("Create")
await page.click('button:has-text("Create")');
// Click :nth-match(:text("Folder"), 2)
await page.click(':nth-match(:text("Folder"), 2)');
// Click text=Properties Title Notes >> input[type="text"]
await page.click('text=Properties Title Notes >> input[type="text"]');
// Fill text=Properties Title Notes >> input[type="text"]
await page.fill('text=Properties Title Notes >> input[type="text"]', '');
// Press Tab
await page.press('text=Properties Title Notes >> input[type="text"]', 'Tab');
const okButton = page.locator('text=OK');
await expect(okButton).toBeDisabled();
await expect(page.locator('.c-form-row__state-indicator').first()).toHaveClass(/invalid/);
// Click text=Properties Title Notes >> input[type="text"]
await page.click('text=Properties Title Notes >> input[type="text"]');
// Fill text=Properties Title Notes >> input[type="text"]
await page.fill('text=Properties Title Notes >> input[type="text"]', TEST_FOLDER);
// Press Tab
await page.press('text=Properties Title Notes >> input[type="text"]', 'Tab');
await expect(page.locator('.c-form-row__state-indicator').first()).not.toHaveClass(/invalid/);
// Click text=OK
await Promise.all([
page.waitForNavigation(),
page.click('text=OK')
]);
await expect(page.locator('.l-browse-bar__object-name')).toContainText(TEST_FOLDER);
});
test.fixme('Create all object types and verify correctness', async ({ page }) => {
//Create the following Domain Objects with their unique Object Types
// Sine Wave Generator (number object)
// Timer Object
// Plan View Object
// Clock Object
// Hyperlink
});
});

View File

@@ -24,8 +24,7 @@
This test suite is dedicated to tests which verify branding related components.
*/
const { test } = require('../fixtures.js');
const { expect } = require('@playwright/test');
const { test, expect } = require('@playwright/test');
test.describe('Branding tests', () => {
test('About Modal launches with basic branding properties', async ({ page }) => {
@@ -58,6 +57,6 @@ test.describe('Branding tests', () => {
page.waitForEvent('popup'),
page.locator('text=click here for third party licensing information').click()
]);
expect(page2.waitForURL('**/licenses**')).toBeTruthy();
expect(page2.waitForURL('**\/licenses**')).toBeTruthy();
});
});

View File

@@ -24,8 +24,7 @@
This test suite is dedicated to tests which verify the basic operations surrounding the example event generator.
*/
const { test } = require('../../fixtures.js');
const { expect } = require('@playwright/test');
const { test, expect } = require('@playwright/test');
test.describe('Example Event Generator Operations', () => {
test('Can create example event generator with a name', async ({ page }) => {

View File

@@ -24,8 +24,7 @@
This test suite is dedicated to tests which verify the basic operations surrounding conditionSets.
*/
const { test } = require('../../../fixtures.js');
const { expect } = require('@playwright/test');
const { test, expect } = require('@playwright/test');
test.describe('Sine Wave Generator', () => {
test('Create new Sine Wave Generator Object and validate create Form Logic', async ({ page }) => {

View File

@@ -24,123 +24,19 @@
This test suite is dedicated to tests which verify the basic operations surrounding moving objects.
*/
const { test } = require('../fixtures.js');
const { expect } = require('@playwright/test');
const { test, expect } = require('@playwright/test');
test.describe('Move item tests', () => {
test('Create a basic object and verify that it can be moved to another folder', async ({ page }) => {
// Go to Open MCT
await page.goto('/');
// Create a new folder in the root my items folder
let folder1 = "Folder1";
await page.locator('button:has-text("Create")').click();
await page.locator('li.icon-folder').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder1);
// Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184
await page.click('form[name="mctForm"] a:has-text("My Items")');
await Promise.all([
page.waitForNavigation(),
page.locator('text=OK').click(),
page.waitForSelector('.c-message-banner__message')
]);
//Wait until Save Banner is gone
await page.locator('.c-message-banner__close-button').click();
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
// Create another folder with a new name at default location, which is currently inside Folder 1
let folder2 = "Folder2";
await page.locator('button:has-text("Create")').click();
await page.locator('li.icon-folder').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder2);
// Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184
await page.click('form[name="mctForm"] a:has-text("My Items")');
await Promise.all([
page.waitForNavigation(),
page.locator('text=OK').click(),
page.waitForSelector('.c-message-banner__message')
]);
//Wait until Save Banner is gone
await page.locator('.c-message-banner__close-button').click();
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
// Move Folder 2 from Folder 1 to My Items
await page.locator('text=Open MCT My Items >> span').nth(3).click();
await page.locator('.c-tree__scrollable div div:nth-child(2) .c-tree__item .c-tree__item__view-control').click();
await page.locator(`a:has-text("${folder2}")`).click({
button: 'right'
});
await page.locator('li.icon-move').click();
await page.locator('form[name="mctForm"] >> text=My Items').click();
await page.locator('text=OK').click();
// Expect that Folder 2 is in My Items, the root folder
expect(page.locator(`text=My Items >> nth=0:has(text=${folder2})`)).toBeTruthy();
test.fixme('Create a basic object and verify that it can be moved to another Folder', async ({ page }) => {
//Create and save Folder
//Create and save Domain Object
//Verify that the newly created domain object can be moved to Folder from Step 1.
//Verify that newly moved object appears in the correct point in Tree
//Verify that newly moved object appears correctly in Inspector panel
});
test('Create a basic object and verify that it cannot be moved to telemetry object without Composition Provider', async ({ page }) => {
// Go to Open MCT
await page.goto('/');
// Create Telemetry Table
let telemetryTable = 'Test Telemetry Table';
await page.locator('button:has-text("Create")').click();
await page.locator('li:has-text("Telemetry Table")').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(telemetryTable);
// Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184
await page.click('form[name="mctForm"] a:has-text("My Items")');
await page.locator('text=OK').click();
// Finish editing and save Telemetry Table
await page.locator('.c-button--menu.c-button--major.icon-save').click();
await page.locator('text=Save and Finish Editing').click();
// Create New Folder Basic Domain Object
let folder = 'Test Folder';
await page.locator('button:has-text("Create")').click();
await page.locator('li:has-text("Folder")').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').click();
await page.locator('text=Properties Title Notes >> input[type="text"]').fill(folder);
// See if it's possible to put the folder in the Telemetry object during creation (Soft Assert)
await page.locator(`form[name="mctForm"] >> text=${telemetryTable}`).click();
let okButton = await page.locator('button.c-button.c-button--major:has-text("OK")');
let okButtonStateDisabled = await okButton.isDisabled();
expect.soft(okButtonStateDisabled).toBeTruthy();
// Continue test regardless of assertion and create it in My Items
await page.locator('form[name="mctForm"] >> text=My Items').click();
await page.locator('text=OK').click();
// Open My Items
await page.locator('text=Open MCT My Items >> span').nth(3).click();
// Select Folder Object and select Move from context menu
await Promise.all([
page.waitForNavigation(),
page.locator(`a:has-text("${folder}")`).click()
]);
await page.locator('.c-tree__item.is-navigated-object .c-tree__item__label .c-tree__item__type-icon').click({
button: 'right'
});
await page.locator('li.icon-move').click();
// See if it's possible to put the folder in the Telemetry object after creation
await page.locator('text=Location Open MCT My Items >> span').nth(3).click();
await page.locator(`form[name="mctForm"] >> text=${telemetryTable}`).click();
let okButton2 = await page.locator('button.c-button.c-button--major:has-text("OK")');
let okButtonStateDisabled2 = await okButton2.isDisabled();
expect(okButtonStateDisabled2).toBeTruthy();
test.fixme('Create a basic object and verify that it cannot be moved to object without Composition Provider', async ({ page }) => {
//Create and save Telemetry Object
//Create and save Domain Object
//Verify that the newly created domain object cannot be moved to Telemetry Object from step 1.
});
});

View File

@@ -1,177 +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 performance tests to ensure that testability of performance
is not broken upstream on Open MCT. Any assumptions made downstream will be tested here
TODO:
- Update resolution of performance config
- Add Performance Observer on init to push all performance marks
- Move client CDP connection to before or to a fixture
-
*/
const { test, expect } = require('@playwright/test');
const filePath = 'e2e/test-data/PerformanceDisplayLayout.json';
test.describe('Performance tests', () => {
test.beforeEach(async ({ page, browser }, testInfo) => {
// Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
// Click a:has-text("My Items")
await page.locator('a:has-text("My Items")').click({
button: 'right'
});
// Click text=Import from JSON
await page.locator('text=Import from JSON').click();
// Upload Performance Display Layout.json
await page.setInputFiles('#fileElem', filePath);
// Click text=OK
await page.locator('text=OK').click();
await expect(page.locator('a:has-text("Performance Display Layout Display Layout")')).toBeVisible();
//Create a Chrome Performance Timeline trace to store as a test artifact
console.log("\n==== Devtools: startTracing ====\n");
await browser.startTracing(page, {
path: `${testInfo.outputPath()}-trace.json`,
screenshots: true
});
});
test.afterEach(async ({ page, browser}) => {
console.log("\n==== Devtools: stopTracing ====\n");
await browser.stopTracing();
/* Measurement Section
/ The following section includes a block of performance measurements.
*/
//Get time difference between viewlarge actionability and evaluate time
await page.evaluate(() => (window.performance.measure("machine-time-difference", "viewlarge.start", "viewLarge.start.test")));
//Get StartTime
const startTime = await page.evaluate(() => window.performance.timing.navigationStart);
console.log('window.performance.timing.navigationStart', startTime);
//Get All Performance Marks
const getAllMarksJson = await page.evaluate(() => JSON.stringify(window.performance.getEntriesByType("mark")));
const getAllMarks = JSON.parse(getAllMarksJson);
console.log('window.performance.getEntriesByType("mark")', getAllMarks);
//Get All Performance Measures
const getAllMeasuresJson = await page.evaluate(() => JSON.stringify(window.performance.getEntriesByType("measure")));
const getAllMeasures = JSON.parse(getAllMeasuresJson);
console.log('window.performance.getEntriesByType("measure")', getAllMeasures);
});
/* The following test will navigate to a previously created Performance Display Layout and measure the
/ following metrics:
/ - ElementResourceTiming
/ - Interaction Timing
*/
test('Embedded View Large for Imagery is performant in Fixed Time', async ({ page, browser }) => {
const client = await page.context().newCDPSession(page);
// Tell the DevTools session to record performance metrics
// https://chromedevtools.github.io/devtools-protocol/tot/Performance/#method-getMetrics
await client.send('Performance.enable');
// Go to baseURL
await page.goto('/');
// Search Available after Launch
await page.locator('input[type="search"]').click();
await page.evaluate(() => window.performance.mark("search-available"));
// Fill Search input
await page.locator('input[type="search"]').fill('Performance Display Layout');
await page.evaluate(() => window.performance.mark("search-entered"));
//Search Result Appears and is clicked
await Promise.all([
page.waitForNavigation(),
page.locator('a:has-text("Performance Display Layout")').first().click(),
page.evaluate(() => window.performance.mark("click-search-result"))
]);
//Time to Example Imagery Frame loads within Display Layout
await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible'});
//Time to Example Imagery object loads
await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible'});
//Get background-image url from background-image css prop
const backgroundImage = await page.locator('.c-imagery__main-image__background-image');
let backgroundImageUrl = await backgroundImage.evaluate((el) => {
return window.getComputedStyle(el).getPropertyValue('background-image').match(/url\(([^)]+)\)/)[1];
});
backgroundImageUrl = backgroundImageUrl.slice(1, -1); //forgive me, padre
console.log('backgroundImageurl ' + backgroundImageUrl);
//Get ResourceTiming of background-image jpg
const resourceTimingJson = await page.evaluate((bgImageUrl) =>
JSON.stringify(window.performance.getEntriesByName(bgImageUrl).pop()),
backgroundImageUrl
);
console.log('resourceTimingJson ' + resourceTimingJson);
//Open Large view
await page.locator('button:has-text("Large View")').click(); //This action includes the performance.mark named 'viewLarge.start'
await page.evaluate(() => window.performance.mark("viewLarge.start.test")); //This is a mark only to compare evaluate timing
//Time to Imagery Rendered in Large Frame
await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible'});
await page.evaluate(() => window.performance.mark("background-image-frame"));
//Time to Example Imagery object loads
await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible'});
await page.evaluate(() => window.performance.mark("background-image-visible"));
// Get Current number of images in thumbstrip
await page.waitForSelector('.c-imagery__thumb');
const thumbCount = await page.locator('.c-imagery__thumb').count();
console.log('number of thumbs rendered ' + thumbCount);
await page.locator('.c-imagery__thumb').last().click();
//Get ResourceTiming of all jpg resources
const resourceTimingJson2 = await page.evaluate(() =>
JSON.stringify(window.performance.getEntriesByType('resource'))
);
const resourceTiming = JSON.parse(resourceTimingJson2);
const jpgResourceTiming = resourceTiming.find((element) =>
element.name.includes('.jpg')
);
console.log('jpgResourceTiming ' + JSON.stringify(jpgResourceTiming));
// Click Close Icon
await page.locator('.c-click-icon').click();
await page.evaluate(() => window.performance.mark("view-large-close-button"));
//await client.send('HeapProfiler.enable');
await client.send('HeapProfiler.collectGarbage');
let performanceMetrics = await client.send('Performance.getMetrics');
console.log(performanceMetrics.metrics);
});
});

View File

@@ -1,119 +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 an initial example for memory leak testing using performance. This configuration and execution must
be kept separate from the traditional performance measurements to avoid any "observer" effects associated with tracing
or profiling playwright and/or the browser.
Based on a pattern identified in https://github.com/trentmwillis/devtools-protocol-demos/blob/master/testing-demos/memory-leak-by-heap.js
and https://github.com/paulirish/automated-chrome-profiling/issues/3
Best path forward: https://github.com/cowchimp/headless-devtools/blob/master/src/Memory/example.js
*/
const { test, expect } = require('@playwright/test');
const filePath = 'e2e/test-data/PerformanceDisplayLayout.json';
// eslint-disable-next-line playwright/no-skipped-test
test.describe.skip('Memory Performance tests', () => {
test.beforeEach(async ({ page, browser }, testInfo) => {
// Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
// Click a:has-text("My Items")
await page.locator('a:has-text("My Items")').click({
button: 'right'
});
// Click text=Import from JSON
await page.locator('text=Import from JSON').click();
// Upload Performance Display Layout.json
await page.setInputFiles('#fileElem', filePath);
// Click text=OK
await page.locator('text=OK').click();
await expect(page.locator('a:has-text("Performance Display Layout Display Layout")')).toBeVisible();
});
test('Embedded View Large for Imagery is performant in Fixed Time', async ({ page, browser }) => {
await page.goto('/', {waitUntil: 'networkidle'});
// To to Search Available after Launch
await page.locator('input[type="search"]').click();
// Fill Search input
await page.locator('input[type="search"]').fill('Performance Display Layout');
//Search Result Appears and is clicked
await Promise.all([
page.waitForNavigation(),
page.locator('a:has-text("Performance Display Layout")').first().click()
]);
//Time to Example Imagery Frame loads within Display Layout
await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible'});
//Time to Example Imagery object loads
await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible'});
const client = await page.context().newCDPSession(page);
await client.send('HeapProfiler.enable');
await client.send('HeapProfiler.startSampling');
// await client.send('HeapProfiler.collectGarbage');
await client.send('Performance.enable');
let performanceMetricsBefore = await client.send('Performance.getMetrics');
console.log(performanceMetricsBefore.metrics);
//await client.send('Performance.disable');
//Open Large view
await page.locator('button:has-text("Large View")').click();
await client.send('HeapProfiler.takeHeapSnapshot');
//Time to Imagery Rendered in Large Frame
await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible'});
//Time to Example Imagery object loads
await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible'});
// Click Close Icon
await page.locator('.c-click-icon').click();
//Time to Example Imagery Frame loads within Display Layout
await page.waitForSelector('.c-imagery__main-image__bg', { state: 'visible'});
//Time to Example Imagery object loads
await page.waitForSelector('.c-imagery__main-image__background-image', { state: 'visible'});
await client.send('HeapProfiler.collectGarbage');
//await client.send('Performance.enable');
let performanceMetricsAfter = await client.send('Performance.getMetrics');
console.log(performanceMetricsAfter.metrics);
//await client.send('Performance.disable');
});
});

View File

@@ -1,158 +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 performance tests to ensure that testability of performance
is not broken upstream on Open MCT. Any assumptions made downstream will be tested here.
TODO:
- Update resolution of performance config
- Add Performance Observer on init to push all performance marks
- Move client CDP connection to before or to a fixture
*/
const { test, expect } = require('@playwright/test');
const notebookFilePath = 'e2e/test-data/PerformanceNotebook.json';
test.describe('Performance tests', () => {
test.beforeEach(async ({ page, browser }, testInfo) => {
// Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
// Click a:has-text("My Items")
await page.locator('a:has-text("My Items")').click({
button: 'right'
});
// Click text=Import from JSON
await page.locator('text=Import from JSON').click();
// Upload Performance Display Layout.json
await page.setInputFiles('#fileElem', notebookFilePath);
// TODO Fix this
await page.locator('text=OK >> nth=1').click();
await expect(page.locator('a:has-text("Performance Notebook")')).toBeVisible();
//Create a Chrome Performance Timeline trace to store as a test artifact
console.log("\n==== Devtools: startTracing ====\n");
await browser.startTracing(page, {
path: `${testInfo.outputPath()}-trace.json`,
screenshots: true
});
});
test.afterEach(async ({ page, browser}) => {
console.log("\n==== Devtools: stopTracing ====\n");
await browser.stopTracing();
/* Measurement Section
/ The following section includes a block of performance measurements.
*/
const startTime = await page.evaluate(() => window.performance.timing.navigationStart);
console.log('window.performance.timing.navigationStart', startTime);
//Get All Performance Marks
const getAllMarksJson = await page.evaluate(() => JSON.stringify(window.performance.getEntriesByType("mark")));
const getAllMarks = JSON.parse(getAllMarksJson);
console.log('window.performance.getEntriesByType("mark")', getAllMarks);
//Get All Performance Measures
const getAllMeasuresJson = await page.evaluate(() => JSON.stringify(window.performance.getEntriesByType("measure")));
const getAllMeasures = JSON.parse(getAllMeasuresJson);
console.log('window.performance.getEntriesByType("measure")', getAllMeasures);
});
/* The following test will navigate to a previously created Performance Display Layout and measure the
/ following metrics:
/ - ElementResourceTiming
/ - Interaction Timing
*/
test('Notebook Search, Add Entry, Update Entry are performant', async ({ page, browser }) => {
const client = await page.context().newCDPSession(page);
// Tell the DevTools session to record performance metrics
// https://chromedevtools.github.io/devtools-protocol/tot/Performance/#method-getMetrics
await client.send('Performance.enable');
// Go to baseURL
await page.goto('/');
// To to Search Available after Launch
await page.locator('input[type="search"]').click();
await page.evaluate(() => window.performance.mark("search-available"));
// Fill Search input
await page.locator('input[type="search"]').fill('Performance Notebook');
await page.evaluate(() => window.performance.mark("search-entered"));
//Search Result Appears and is clicked
await Promise.all([
page.waitForNavigation(),
page.locator('a:has-text("Performance Notebook")').first().click(),
page.evaluate(() => window.performance.mark("click-search-result"))
]);
await page.waitForSelector('.c-tree__item c-tree-and-search__loading loading', {state: 'hidden'});
await page.evaluate(() => window.performance.mark("search-spinner-gone"));
await page.waitForSelector('.l-browse-bar__object-name', { state: 'visible'});
await page.evaluate(() => window.performance.mark("object-title-appears"));
await page.waitForSelector('.c-notebook__entry >> nth=0', { state: 'visible'});
await page.evaluate(() => window.performance.mark("notebook-entry-appears"));
// Click Add new Notebook Entry
await page.locator('.c-notebook__drag-area').click();
await page.evaluate(() => window.performance.mark("new-notebook-entry-created"));
// Enter Notebook Entry text
await page.locator('div.c-ne__text').last().fill('New Entry');
await page.keyboard.press('Enter');
await page.evaluate(() => window.performance.mark("new-notebook-entry-filled"));
//Individual Notebook Entry Search
await page.evaluate(() => window.performance.mark("notebook-search-start"));
await page.locator('.c-notebook__search >> input').fill('Existing Entry');
await page.evaluate(() => window.performance.mark("notebook-search-filled"));
await page.waitForSelector('text=Search Results (3)', { state: 'visible'});
await page.evaluate(() => window.performance.mark("notebook-search-processed"));
await page.waitForSelector('.c-notebook__entry >> nth=2', { state: 'visible'});
await page.evaluate(() => window.performance.mark("notebook-search-processed"));
//Clear Search
await page.locator('.c-search.c-notebook__search .c-search__clear-input').click();
await page.evaluate(() => window.performance.mark("notebook-search-processed"));
// Hover on Last
await page.evaluate(() => window.performance.mark("new-notebook-entry-delete"));
await page.locator('div.c-ne__time-and-content').last().hover();
await page.locator('button[title="Delete this entry"]').last().click();
await page.locator('button:has-text("Ok")').click();
await page.waitForSelector('.c-notebook__entry >> nth=3', { state: 'detached'});
await page.evaluate(() => window.performance.mark("new-notebook-entry-deleted"));
//await client.send('HeapProfiler.enable');
await client.send('HeapProfiler.collectGarbage');
let performanceMetrics = await client.send('Performance.getMetrics');
console.log(performanceMetrics.metrics);
});
});

View File

@@ -24,8 +24,7 @@
This test suite is dedicated to tests which verify the basic operations surrounding conditionSets.
*/
const { test } = require('../../fixtures.js');
const { expect } = require('@playwright/test');
const { test, expect } = require('@playwright/test');
const path = require('path');
// https://github.com/nasa/openmct/issues/4323#issuecomment-1067282651

View File

@@ -24,10 +24,7 @@
This test suite is dedicated to tests which verify the basic operations surrounding exportAsJSON.
*/
const { test } = require('../../../fixtures.js');
// FIXME: Remove this eslint exception once tests are implemented
// eslint-disable-next-line no-unused-vars
const { expect } = require('@playwright/test');
const { test, expect } = require('@playwright/test');
test.describe('ExportAsJSON', () => {
test.fixme('Create a basic object and verify that it can be exported as JSON from Tree', async ({ page }) => {

View File

@@ -24,10 +24,7 @@
This test suite is dedicated to tests which verify the basic operations surrounding importAsJSON.
*/
const { test } = require('../../../fixtures.js');
// FIXME: Remove this eslint exception once tests are implemented
// eslint-disable-next-line no-unused-vars
const { expect } = require('@playwright/test');
const { test, expect } = require('@playwright/test');
test.describe('ExportAsJSON', () => {
test.fixme('Verify that domain object can be importAsJSON from Tree', async ({ page }) => {

View File

@@ -24,8 +24,7 @@
This test suite is dedicated to tests which verify the basic operations surrounding Clock.
*/
const { test } = require('../../../fixtures.js');
const { expect } = require('@playwright/test');
const { test, expect } = require('@playwright/test');
test.describe('Clock Generator', () => {

View File

@@ -21,167 +21,45 @@
*****************************************************************************/
/*
This test suite is dedicated to tests which verify the basic operations surrounding conditionSets. Note: this
suite is sharing state between tests which is considered an anti-pattern. Implimenting in this way to
demonstrate some playwright for test developers. This pattern should not be re-used in other CRUD suites.
This test suite is dedicated to tests which verify the basic operations surrounding conditionSets.
*/
const { test } = require('../../../fixtures.js');
const { expect } = require('@playwright/test');
const { test, expect } = require('@playwright/test');
let conditionSetUrl;
let getConditionSetIdentifierFromUrl;
test('Create new Condition Set object and store @localStorage', async ({ page, context }) => {
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
//Click the Create button
await page.click('button:has-text("Create")');
// Click text=Condition Set
await page.click('text=Condition Set');
// Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184
await page.click('form[name="mctForm"] a:has-text("My Items")');
// Click text=OK
await Promise.all([
page.waitForNavigation(),
page.click('text=OK')
]);
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
//Save localStorage for future test execution
await context.storageState({ path: './e2e/tests/recycled_storage.json' });
//Set object identifier from url
conditionSetUrl = await page.url();
console.log('conditionSetUrl ' + conditionSetUrl);
getConditionSetIdentifierFromUrl = await conditionSetUrl.split('/').pop().split('?')[0];
console.log('getConditionSetIdentifierFromUrl ' + getConditionSetIdentifierFromUrl);
});
test.describe.serial('Condition Set CRUD Operations on @localStorage', () => {
//Load localStorage for subsequent tests
test.use({ storageState: './e2e/tests/recycled_storage.json' });
//Begin suite of tests again localStorage
test('Condition set object properties persist in main view and inspector', async ({ page }) => {
//Navigate to baseURL with injected localStorage
await page.goto(conditionSetUrl, { waitUntil: 'networkidle' });
//Assertions on loaded Condition Set in main view
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
//Assertions on loaded Condition Set in Inspector
await expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy;
//Reload Page
await Promise.all([
page.reload(),
page.waitForLoadState('networkidle')
]);
//Re-verify after reload
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
//Assertions on loaded Condition Set in Inspector
await expect.soft(page.locator('_vue=item.name=Unnamed Condition Set')).toBeTruthy;
});
test('condition set object can be modified on @localStorage', async ({ page }) => {
await page.goto(conditionSetUrl, { waitUntil: 'networkidle' });
//Assertions on loaded Condition Set in main view
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
//Update the Condition Set properties
// Click Edit Button
await page.locator('text=Conditions View Snapshot >> button').nth(3).click();
//Edit Condition Set Name from main view
await page.locator('text=Unnamed Condition Set').first().fill('Renamed Condition Set');
await page.locator('text=Renamed Condition Set').first().press('Enter');
// Click Save Button
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
// Click Save and Finish Editing Option
await page.locator('text=Save and Finish Editing').click();
//Verify Main section reflects updated Name Property
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Renamed Condition Set');
// Verify Inspector properties
// Verify Inspector has updated Name property
await expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy();
// Verify Inspector Details has updated Name property
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
await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
// Verify Search Tree reflects renamed Name property
await page.locator('input[type="search"]').fill('Renamed');
await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
//Reload Page
await Promise.all([
page.reload(),
page.waitForLoadState('networkidle')
]);
//Verify Main section reflects updated Name Property
await expect.soft(page.locator('.l-browse-bar__object-name')).toContainText('Renamed Condition Set');
// Verify Inspector properties
// Verify Inspector has updated Name property
await expect.soft(page.locator('text=Renamed Condition Set').nth(1)).toBeTruthy();
// Verify Inspector Details has updated Name property
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
await expect(page.locator('a:has-text("Renamed Condition Set")')).toBeTruthy();
// Verify Search Tree reflects renamed Name property
await page.locator('input[type="search"]').fill('Renamed');
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
test.describe('Condition Set Operations', () => {
test('Create new button `condition set` creates new condition object', async ({ page }) => {
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
//Expect Unnamed Condition Set to be visible in Main View
await expect(page.locator('a:has-text("Unnamed Condition Set Condition Set")')).toBeVisible();
//Click the Create button
await page.click('button:has-text("Create")');
// Search for Unnamed Condition Set
await page.locator('input[type="search"]').fill('Unnamed Condition Set');
// Right Click to Open Actions Menu
await page.locator('a:has-text("Unnamed Condition Set")').click({
button: 'right'
});
// Click Remove Action
await page.locator('text=Remove').click();
// Click text=Condition Set
await page.click('text=Condition Set');
await page.locator('text=OK').click();
// Click text=OK
await Promise.all([
page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine/dab945d4-5a84-480e-8180-222b4aa730fa?tc.mode=fixed&tc.startBound=1639696164435&tc.endBound=1639697964435&tc.timeSystem=utc&view=conditionSet.view' }*/),
page.click('text=OK')
]);
//Expect Unnamed Condition Set to be removed in Main View
await expect(page.locator('a:has-text("Unnamed Condition Set Condition Set")')).not.toBeVisible();
await page.locator('.c-search__clear-input').click();
// Search for Unnamed Condition Set
await page.locator('input[type="search"]').fill('Unnamed Condition Set');
// Expect Unnamed Condition Set to be removed
await expect(page.locator('a:has-text("Unnamed Condition Set")')).not.toBeVisible();
//Feature?
//Domain Object is still available by direct URL after delete
await page.goto(conditionSetUrl, { waitUntil: 'networkidle' });
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Condition Set');
});
test.fixme('condition set object properties exist', async ({ page }) => {
//Go to object created in step one
//Verify the Condition Set properties persist on Save
//Verify the Condition Set properties persist on page.reload()
});
test.fixme('condition set object can be modified', async ({ page }) => {
//Go to object created in step one
//Update the Condition Set properties
//Verify the Condition Set properties persist on Save
//Verify the Condition Set properties persist on page.reload()
});
test.fixme('condition set object can be deleted', async ({ page }) => {
//Go to object created in step one
//Verify that Condition Set object can be deleted
//Verify the Condition Set object does not exist in Tree
//Verify the Condition Set object does not exist with direct navigation to object's URL
});
});

View File

@@ -24,15 +24,13 @@
This test suite is dedicated to tests which verify the basic operations surrounding imagery,
but only assume that example imagery is present.
*/
/* globals process */
const { test } = require('../../../fixtures.js');
const { expect } = require('@playwright/test');
const { test, expect } = require('@playwright/test');
test.describe('Example Imagery', () => {
test.beforeEach(async ({ page }) => {
page.on('console', msg => console.log(msg.text()));
page.on('console', msg => console.log(msg.text()))
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
@@ -42,24 +40,18 @@ test.describe('Example Imagery', () => {
// Click text=Example Imagery
await page.click('text=Example Imagery');
// Click on My Items in Tree. Workaround for https://github.com/nasa/openmct/issues/5184
await page.click('form[name="mctForm"] a:has-text("My Items")');
// Click text=OK
await Promise.all([
page.waitForNavigation({waitUntil: 'networkidle'}),
page.click('text=OK'),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine/dab945d4-5a84-480e-8180-222b4aa730fa?tc.mode=fixed&tc.startBound=1639696164435&tc.endBound=1639697964435&tc.timeSystem=utc&view=conditionSet.view' }*/),
page.click('text=OK')
]);
//Wait until Save Banner is gone
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');
});
const backgroundImageSelector = '.c-imagery__main-image__background-image';
test('Can use Mouse Wheel to zoom in and out of latest image', async ({ page }) => {
const bgImageLocator = page.locator(backgroundImageSelector);
const bgImageLocator = await page.locator(backgroundImageSelector);
const deltaYStep = 100; //equivalent to 1x zoom
await bgImageLocator.hover();
const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox();
@@ -85,11 +77,9 @@ test.describe('Example Imagery', () => {
test('Can use alt+drag to move around image once zoomed in', async ({ page }) => {
const deltaYStep = 100; //equivalent to 1x zoom
const panHotkey = process.platform === 'linux' ? ['Control', 'Alt'] : ['Alt'];
const bgImageLocator = page.locator(backgroundImageSelector);
const bgImageLocator = await page.locator(backgroundImageSelector);
await bgImageLocator.hover();
// zoom in
await page.mouse.wheel(0, deltaYStep * 2);
await bgImageLocator.hover();
@@ -101,59 +91,50 @@ test.describe('Example Imagery', () => {
// center the mouse pointer
await page.mouse.move(imageCenterX, imageCenterY);
//Get Diagnostic info about process environment
console.log('process.platform is ' + process.platform);
const getUA = await page.evaluate(() => navigator.userAgent);
console.log('navigator.userAgent ' + getUA);
// Pan Imagery Hints
const expectedAltText = process.platform === 'linux' ? 'Ctrl+Alt drag to pan' : 'Alt drag to pan';
const imageryHintsText = await page.locator('.c-imagery__hints').innerText();
expect(expectedAltText).toEqual(imageryHintsText);
// pan right
await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
await page.keyboard.down('Alt');
await page.mouse.down();
await page.mouse.move(imageCenterX - 200, imageCenterY, 10);
await page.mouse.up();
await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
await page.keyboard.up('Alt');
const afterRightPanBoundingBox = await bgImageLocator.boundingBox();
expect(zoomedBoundingBox.x).toBeGreaterThan(afterRightPanBoundingBox.x);
// pan left
await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
await page.keyboard.down('Alt');
await page.mouse.down();
await page.mouse.move(imageCenterX, imageCenterY, 10);
await page.mouse.up();
await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
await page.keyboard.up('Alt');
const afterLeftPanBoundingBox = await bgImageLocator.boundingBox();
expect(afterRightPanBoundingBox.x).toBeLessThan(afterLeftPanBoundingBox.x);
// pan up
await page.mouse.move(imageCenterX, imageCenterY);
await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
await page.keyboard.down('Alt');
await page.mouse.down();
await page.mouse.move(imageCenterX, imageCenterY + 200, 10);
await page.mouse.up();
await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
await page.keyboard.up('Alt');
const afterUpPanBoundingBox = await bgImageLocator.boundingBox();
expect(afterUpPanBoundingBox.y).toBeGreaterThan(afterLeftPanBoundingBox.y);
// pan down
await Promise.all(panHotkey.map(x => page.keyboard.down(x)));
await page.keyboard.down('Alt');
await page.mouse.down();
await page.mouse.move(imageCenterX, imageCenterY - 200, 10);
await page.mouse.up();
await Promise.all(panHotkey.map(x => page.keyboard.up(x)));
await page.keyboard.up('Alt');
const afterDownPanBoundingBox = await bgImageLocator.boundingBox();
expect(afterDownPanBoundingBox.y).toBeLessThan(afterUpPanBoundingBox.y);
});
test('Can use + - buttons to zoom on the image', async ({ page }) => {
const bgImageLocator = page.locator(backgroundImageSelector);
const bgImageLocator = await page.locator(backgroundImageSelector);
await bgImageLocator.hover();
const zoomInBtn = page.locator('.t-btn-zoom-in');
const zoomOutBtn = page.locator('.t-btn-zoom-out');
const zoomInBtn = await page.locator('.t-btn-zoom-in');
const zoomOutBtn = await page.locator('.t-btn-zoom-out');
const initialBoundingBox = await bgImageLocator.boundingBox();
await zoomInBtn.click();
@@ -174,27 +155,21 @@ test.describe('Example Imagery', () => {
});
test('Can use the reset button to reset the image', async ({ page }) => {
const bgImageLocator = page.locator(backgroundImageSelector);
// wait for zoom animation to finish
const bgImageLocator = await page.locator(backgroundImageSelector);
await bgImageLocator.hover();
const zoomInBtn = page.locator('.t-btn-zoom-in');
const zoomResetBtn = page.locator('.t-btn-zoom-reset');
const zoomInBtn = await page.locator('.t-btn-zoom-in');
const zoomResetBtn = await page.locator('.t-btn-zoom-reset');
const initialBoundingBox = await bgImageLocator.boundingBox();
await zoomInBtn.click();
// wait for zoom animation to finish
await bgImageLocator.hover();
await zoomInBtn.click();
// wait for zoom animation to finish
await bgImageLocator.hover();
const zoomedInBoundingBox = await bgImageLocator.boundingBox();
expect.soft(zoomedInBoundingBox.height).toBeGreaterThan(initialBoundingBox.height);
expect.soft(zoomedInBoundingBox.width).toBeGreaterThan(initialBoundingBox.width);
await zoomResetBtn.click();
// wait for zoom animation to finish
await bgImageLocator.hover();
const resetBoundingBox = await bgImageLocator.boundingBox();
@@ -205,271 +180,38 @@ test.describe('Example Imagery', () => {
expect(resetBoundingBox.width).toEqual(initialBoundingBox.width);
});
test('Using the zoom features does not pause telemetry', async ({ page }) => {
const bgImageLocator = page.locator(backgroundImageSelector);
const pausePlayButton = page.locator('.c-button.pause-play');
// wait for zoom animation to finish
await bgImageLocator.hover();
// open the time conductor drop down
await page.locator('.c-conductor__controls button.c-mode-button').click();
// Click local clock
await page.locator('.icon-clock >> text=Local Clock').click();
await expect.soft(pausePlayButton).not.toHaveClass(/is-paused/);
const zoomInBtn = page.locator('.t-btn-zoom-in');
await zoomInBtn.click();
// wait for zoom animation to finish
await bgImageLocator.hover();
return expect(pausePlayButton).not.toHaveClass(/is-paused/);
});
//test('Can use Mouse Wheel to zoom in and out of previous image');
//test('Can zoom into the latest image and the real-time/fixed-time imagery will pause');
//test.skip('Can zoom into a previous image from thumbstrip in real-time or fixed-time');
//test.skip('Clicking on the left arrow should pause the imagery and go to previous image');
//test.skip('If the imagery view is in pause mode, it should not be updated when new images come in');
//test.skip('If the imagery view is not in pause mode, it should be updated when new images come in');
});
// The following test case will cover these scenarios
// ('Can use Mouse Wheel to zoom in and out of previous image');
// ('Can use alt+drag to move around image once zoomed in');
// ('Clicking on the left arrow should pause the imagery and go to previous image');
// ('If the imagery view is in pause mode, it should not be updated when new images come in');
// ('If the imagery view is not in pause mode, it should be updated when new images come in');
const backgroundImageSelector = '.c-imagery__main-image__background-image';
test('Example Imagery in Display layout', async ({ page }) => {
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/5265'
});
// Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
// Click the Create button
await page.click('button:has-text("Create")');
// Click text=Example Imagery
await page.click('text=Example Imagery');
// Clear and set Image load delay to minimum value
// FIXME: Update the value to 5000 ms when this bug is fixed.
// See: https://github.com/nasa/openmct/issues/5265
await page.locator('input[type="number"]').fill('');
await page.locator('input[type="number"]').fill('0');
// Click text=OK
await Promise.all([
page.waitForNavigation({waitUntil: 'networkidle'}),
page.click('text=OK'),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
// Wait until Save Banner is gone
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
await expect(page.locator('.l-browse-bar__object-name')).toContainText('Unnamed Example Imagery');
const bgImageLocator = page.locator(backgroundImageSelector);
await bgImageLocator.hover();
// Click previous image button
const previousImageButton = page.locator('.c-nav--prev');
await previousImageButton.click();
// Verify previous image
const selectedImage = page.locator('.selected');
await expect(selectedImage).toBeVisible();
// Zoom in
const originalImageDimensions = await page.locator(backgroundImageSelector).boundingBox();
await bgImageLocator.hover();
const deltaYStep = 100; // equivalent to 1x zoom
await page.mouse.wheel(0, deltaYStep * 2);
const zoomedBoundingBox = await bgImageLocator.boundingBox();
const imageCenterX = zoomedBoundingBox.x + zoomedBoundingBox.width / 2;
const imageCenterY = zoomedBoundingBox.y + zoomedBoundingBox.height / 2;
// Wait for zoom animation to finish
await bgImageLocator.hover();
const imageMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox();
expect(imageMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height);
expect(imageMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width);
// Center the mouse pointer
await page.mouse.move(imageCenterX, imageCenterY);
// Pan Imagery Hints
const expectedAltText = process.platform === 'linux' ? 'Ctrl+Alt drag to pan' : 'Alt drag to pan';
const imageryHintsText = await page.locator('.c-imagery__hints').innerText();
expect(expectedAltText).toEqual(imageryHintsText);
// Click next image button
const nextImageButton = page.locator('.c-nav--next');
await nextImageButton.click();
// Click time conductor mode button
await page.locator('.c-mode-button').click();
// Select local clock mode
await page.locator('[data-testid=conductor-modeOption-realtime]').click();
// Zoom in on next image
await bgImageLocator.hover();
await page.mouse.wheel(0, deltaYStep * 2);
// Wait for zoom animation to finish
await bgImageLocator.hover();
const imageNextMouseZoomedIn = await page.locator(backgroundImageSelector).boundingBox();
expect(imageNextMouseZoomedIn.height).toBeGreaterThan(originalImageDimensions.height);
expect(imageNextMouseZoomedIn.width).toBeGreaterThan(originalImageDimensions.width);
// Click previous image button
await previousImageButton.click();
// Verify previous image
await expect(selectedImage).toBeVisible();
const imageCount = await page.locator('.c-imagery__thumb').count();
await expect.poll(async () => {
const newImageCount = await page.locator('.c-imagery__thumb').count();
return newImageCount;
}, {
message: "verify that new images still stream in",
timeout: 6 * 1000
}).toBeGreaterThan(imageCount);
// Verify selected image is still displayed
await expect(selectedImage).toBeVisible();
// Unpause imagery
await page.locator('.pause-play').click();
//Get background-image url from background-image css prop
const backgroundImage = page.locator('.c-imagery__main-image__background-image');
let backgroundImageUrl = await backgroundImage.evaluate((el) => {
return window.getComputedStyle(el).getPropertyValue('background-image').match(/url\(([^)]+)\)/)[1];
});
let backgroundImageUrl1 = backgroundImageUrl.slice(1, -1); //forgive me, padre
console.log('backgroundImageUrl1 ' + backgroundImageUrl1);
let backgroundImageUrl2;
await expect.poll(async () => {
// Verify next image has updated
let backgroundImageUrlNext = await backgroundImage.evaluate((el) => {
return window.getComputedStyle(el).getPropertyValue('background-image').match(/url\(([^)]+)\)/)[1];
});
backgroundImageUrl2 = backgroundImageUrlNext.slice(1, -1); //forgive me, padre
return backgroundImageUrl2;
}, {
message: "verify next image has updated",
timeout: 6 * 1000
}).not.toBe(backgroundImageUrl1);
console.log('backgroundImageUrl2 ' + backgroundImageUrl2);
});
test.describe('Example imagery thumbnails resize in display layouts', () => {
test('Resizing the layout changes thumbnail visibility and size', async ({ page }) => {
await page.goto('/', { waitUntil: 'networkidle' });
const thumbsWrapperLocator = page.locator('.c-imagery__thumbs-wrapper');
// Click button:has-text("Create")
await page.locator('button:has-text("Create")').click();
// Click li:has-text("Display Layout")
await page.locator('li:has-text("Display Layout")').click();
const displayLayoutTitleField = page.locator('text=Properties Title Notes Horizontal grid (px) Vertical grid (px) Horizontal size ( >> input[type="text"]');
await displayLayoutTitleField.click();
await displayLayoutTitleField.fill('Thumbnail Display Layout');
// Click text=OK
await Promise.all([
page.waitForNavigation(),
page.locator('text=OK').click()
]);
// Click text=Snapshot Save and Finish Editing Save and Continue Editing >> button >> nth=1
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
// Click text=Save and Finish Editing
await page.locator('text=Save and Finish Editing').click();
// Click button:has-text("Create")
await page.locator('button:has-text("Create")').click();
// Click li:has-text("Example Imagery")
await page.locator('li:has-text("Example Imagery")').click();
const imageryTitleField = page.locator('text=Properties Title Notes Images url list (comma separated) Image load delay (milli >> input[type="text"]');
// Click text=Properties Title Notes Images url list (comma separated) Image load delay (milli >> input[type="text"]
await imageryTitleField.click();
// Fill text=Properties Title Notes Images url list (comma separated) Image load delay (milli >> input[type="text"]
await imageryTitleField.fill('Thumbnail Example Imagery');
// Click text=OK
await Promise.all([
page.waitForNavigation(),
page.locator('text=OK').click()
]);
// Click text=Thumbnail Example Imagery Imagery Layout Snapshot >> button >> nth=0
await Promise.all([
page.waitForNavigation(),
page.locator('text=Thumbnail Example Imagery Imagery Layout Snapshot >> button').first().click()
]);
// Edit mode
await page.locator('text=Thumbnail Display Layout Snapshot >> button').nth(3).click();
// Click on example imagery to expose toolbar
await page.locator('text=Thumbnail Example Imagery Snapshot Large View').click();
// expect thumbnails not be visible when first added
expect.soft(thumbsWrapperLocator.isHidden()).toBeTruthy();
// Resize the example imagery vertically to change the thumbnail visibility
/*
The following arbitrary values are added to observe the separate visual
conditions of the thumbnails (hidden, small thumbnails, regular thumbnails).
Specifically, height is set to 50px for small thumbs and 100px for regular
*/
// Click #mct-input-id-103
await page.locator('#mct-input-id-103').click();
// Fill #mct-input-id-103
await page.locator('#mct-input-id-103').fill('50');
expect(thumbsWrapperLocator.isVisible()).toBeTruthy();
await expect(thumbsWrapperLocator).toHaveClass(/is-small-thumbs/);
// Resize the example imagery vertically to change the thumbnail visibility
// Click #mct-input-id-103
await page.locator('#mct-input-id-103').click();
// Fill #mct-input-id-103
await page.locator('#mct-input-id-103').fill('100');
expect(thumbsWrapperLocator.isVisible()).toBeTruthy();
await expect(thumbsWrapperLocator).not.toHaveClass(/is-small-thumbs/);
});
test.describe('Example Imagery in Display layout', () => {
test.skip('Can use Mouse Wheel to zoom in and out of previous image');
test.skip('Can use alt+drag to move around image once zoomed in');
test.skip('Can zoom into the latest image and the real-time/fixed-time imagery will pause');
test.skip('Clicking on the left arrow should pause the imagery and go to previous image');
test.skip('If the imagery view is in pause mode, it should not be updated when new images come in');
test.skip('If the imagery view is not in pause mode, it should be updated when new images come in');
});
test.describe('Example Imagery in Flexible layout', () => {
test.fixme('Can use Mouse Wheel to zoom in and out of previous image');
test.fixme('Can use alt+drag to move around image once zoomed in');
test.fixme('Can zoom into the latest image and the real-time/fixed-time imagery will pause');
test.fixme('Clicking on the left arrow should pause the imagery and go to previous image');
test.fixme('If the imagery view is in pause mode, it should not be updated when new images come in');
test.fixme('If the imagery view is not in pause mode, it should be updated when new images come in');
test.skip('Can use Mouse Wheel to zoom in and out of previous image');
test.skip('Can use alt+drag to move around image once zoomed in');
test.skip('Can zoom into the latest image and the real-time/fixed-time imagery will pause');
test.skip('Clicking on the left arrow should pause the imagery and go to previous image');
test.skip('If the imagery view is in pause mode, it should not be updated when new images come in');
test.skip('If the imagery view is not in pause mode, it should be updated when new images come in');
});
test.describe('Example Imagery in Tabs view', () => {
test.fixme('Can use Mouse Wheel to zoom in and out of previous image');
test.fixme('Can use alt+drag to move around image once zoomed in');
test.fixme('Can zoom into the latest image and the real-time/fixed-time imagery will pause');
test.fixme('Can zoom into a previous image from thumbstrip in real-time or fixed-time');
test.fixme('Clicking on the left arrow should pause the imagery and go to previous image');
test.fixme('If the imagery view is in pause mode, it should not be updated when new images come in');
test.fixme('If the imagery view is not in pause mode, it should be updated when new images come in');
test.skip('Can use Mouse Wheel to zoom in and out of previous image');
test.skip('Can use alt+drag to move around image once zoomed in');
test.skip('Can zoom into the latest image and the real-time/fixed-time imagery will pause');
test.skip('Can zoom into a previous image from thumbstrip in real-time or fixed-time');
test.skip('Clicking on the left arrow should pause the imagery and go to previous image');
test.skip('If the imagery view is in pause mode, it should not be updated when new images come in');
test.skip('If the imagery view is not in pause mode, it should be updated when new images come in');
});

View File

@@ -21,11 +21,10 @@
*****************************************************************************/
/*
Testsuite for plot autoscale.
Test for plot autoscale.
*/
const { test: _test } = require('../../../fixtures.js');
const { expect } = require('@playwright/test');
const { test: _test, expect } = require('@playwright/test');
// create a new `test` API that will not append platform details to snapshot
// file names, only for the tests in this file, so that the same snapshots will
@@ -48,10 +47,7 @@ test.use({
});
test.describe('ExportAsJSON', () => {
test('User can set autoscale with a valid range @snapshot', async ({ page }) => {
//This is necessary due to the size of the test suite.
await test.setTimeout(120 * 1000);
test.only('autoscale off causes no error from undefined user range', async ({ page }) => {
await page.goto('/', { waitUntil: 'networkidle' });
await setTimeRange(page);
@@ -72,6 +68,14 @@ test.describe('ExportAsJSON', () => {
.then(shot => expect(shot).toMatchSnapshot('autoscale-canvas-prepan.png', { maxDiffPixels: 40 }))
]);
let errorCount = 0;
function onError() {
errorCount++;
}
page.on('pageerror', onError);
await page.keyboard.down('Alt');
await canvas.dragTo(canvas, {
@@ -87,12 +91,18 @@ test.describe('ExportAsJSON', () => {
await page.keyboard.up('Alt');
page.off('pageerror', onError);
// There would have been an error at this point. So if there isn't, then
// we fixed it.
expect(errorCount).toBe(0);
// Ensure the drag worked.
await Promise.all([
testYTicks(page, ['0.00', '0.50', '1.00', '1.50', '2.00']),
new Promise(r => setTimeout(r, 100))
.then(() => canvas.screenshot())
.then(shot => expect(shot).toMatchSnapshot('autoscale-canvas-panned.png', { maxDiffPixels: 40 }))
.then(shot => expect(shot).toMatchSnapshot('autoscale-canvas-panned.png', { maxDiffPixels: 20 }))
]);
});
});
@@ -124,14 +134,9 @@ async function createSinewaveOverlayPlot(page) {
// add overlay plot with defaults
await page.locator('li:has-text("Overlay Plot")').click();
await Promise.all([
page.waitForNavigation(),
page.locator('text=OK').click(),
//Wait for Save Banner to appear1
page.waitForSelector('.c-message-banner__message')
page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine/a9268c6f-45cc-4bcd-a6a0-50ac4036e396?tc.mode=fixed&tc.startBound=1649305424163&tc.endBound=1649307224163&tc.timeSystem=utc&view=plot-overlay' }*/),
page.locator('text=OK').click()
]);
//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();
@@ -143,19 +148,14 @@ async function createSinewaveOverlayPlot(page) {
// add sine wave generator with defaults
await page.locator('li:has-text("Sine Wave Generator")').click();
await Promise.all([
page.waitForNavigation(),
page.locator('text=OK').click(),
//Wait for Save Banner to appear1
page.waitForSelector('.c-message-banner__message')
page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine/a9268c6f-45cc-4bcd-a6a0-50ac4036e396/5cfa5c69-17bc-4a99-9545-4da8125380c5?tc.mode=fixed&tc.startBound=1649305424163&tc.endBound=1649307224163&tc.timeSystem=utc&view=plot-single' }*/),
page.locator('text=OK').click()
]);
//Wait until Save Banner is gone
await page.locator('.c-message-banner__close-button').click();
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
// focus the overlay plot
await page.locator('text=Open MCT My Items >> span').nth(3).click();
await Promise.all([
page.waitForNavigation(),
page.waitForNavigation(/*{ url: 'http://localhost:8080/#/browse/mine/a9268c6f-45cc-4bcd-a6a0-50ac4036e396?tc.mode=fixed&tc.startBound=1649305424163&tc.endBound=1649307224163&tc.timeSystem=utc&view=plot-overlay' }*/),
page.locator('text=Unnamed Overlay Plot').first().click()
]);
}
@@ -168,18 +168,11 @@ async function turnOffAutoscale(page) {
await page.locator('text=Unnamed Overlay Plot Snapshot >> button').nth(3).click();
// uncheck autoscale
await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"] >> nth=1').uncheck();
await page.locator('text=Y Axis Scaling Auto scale Padding >> input[type="checkbox"]').uncheck();
// save
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
await Promise.all([
page.locator('text=Save and Finish Editing').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
//Wait until Save Banner is gone
await page.locator('.c-message-banner__close-button').click();
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
await page.locator('text=Save and Finish Editing').click();
}
/**
@@ -187,7 +180,6 @@ async function turnOffAutoscale(page) {
*/
async function testYTicks(page, values) {
const yTicks = page.locator('.gl-plot-y-tick-label');
await page.locator('canvas >> nth=1').hover();
let promises = [yTicks.count().then(c => expect(c).toBe(values.length))];
for (let i = 0, l = values.length; i < l; i += 1) {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -1,312 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
/*
Tests to verify log plot functionality. Note this test suite if very much under active development and should not
necessarily be used for reference when writing new tests in this area.
*/
const { test } = require('../../../fixtures.js');
const { expect } = require('@playwright/test');
test.describe('Log plot tests', () => {
test('Log Plot ticks are functionally correct in regular and log mode and after refresh', async ({ page }) => {
//This is necessary due to the size of the test suite.
await test.setTimeout(120 * 1000);
await makeOverlayPlot(page);
await testRegularTicks(page);
await enableEditMode(page);
await enableLogMode(page);
await testLogTicks(page);
await disableLogMode(page);
await testRegularTicks(page);
await enableLogMode(page);
await testLogTicks(page);
await saveOverlayPlot(page);
await testLogTicks(page);
//await testLogPlotPixels(page);
// FIXME: Get rid of the waitForTimeout() and lint warning exception.
// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(1 * 1000);
// refresh page and wait for charts and ticks to load
await page.reload({ waitUntil: 'networkidle'});
await page.waitForSelector('.gl-plot-chart-area');
await page.waitForSelector('.gl-plot-y-tick-label');
// test log ticks hold up after refresh
await testLogTicks(page);
//await testLogPlotPixels(page);
});
// Leaving test as 'TODO' for now.
// NOTE: Not eligible for community contributions.
test.fixme('Verify that log mode option is reflected in import/export JSON', async ({ page }) => {
await makeOverlayPlot(page);
await enableEditMode(page);
await enableLogMode(page);
await saveOverlayPlot(page);
// TODO ...export, delete the overlay, then import it...
//await testLogTicks(page);
// TODO, the plot is slightly at different position that in the other test, so this fails.
// ...We can fix it by copying all steps from the first test...
// await testLogPlotPixels(page);
});
});
/**
* Makes an overlay plot with a sine wave generator and clicks on the overlay plot in the sidebar so it is the active thing displayed.
* @param {import('@playwright/test').Page} page
*/
async function makeOverlayPlot(page) {
// fresh page with time range from 2022-03-29 22:00:00.000Z to 2022-03-29 22:00:30.000Z
await page.goto('/', { waitUntil: 'networkidle' });
// Set a specific time range for consistency, otherwise it will change
// on every test to a range based on the current time.
const timeInputs = page.locator('input.c-input--datetime');
await timeInputs.first().click();
await timeInputs.first().fill('2022-03-29 22:00:00.000Z');
await timeInputs.nth(1).click();
await timeInputs.nth(1).fill('2022-03-29 22:00:30.000Z');
// create overlay plot
await page.locator('button.c-create-button').click();
await page.locator('li:has-text("Overlay Plot")').click();
await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle'}),
page.locator('text=OK').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
//Wait until Save Banner is gone
await page.locator('.c-message-banner__close-button').click();
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
// save the overlay plot
await saveOverlayPlot(page);
// create a sinewave generator
await page.locator('button.c-create-button').click();
await page.locator('li:has-text("Sine Wave Generator")').click();
// set amplitude to 6, offset 4, period 2
await page.locator('div:nth-child(5) .form-row .c-form-row__controls .form-control .field input').click();
await page.locator('div:nth-child(5) .form-row .c-form-row__controls .form-control .field input').fill('6');
await page.locator('div:nth-child(6) .form-row .c-form-row__controls .form-control .field input').click();
await page.locator('div:nth-child(6) .form-row .c-form-row__controls .form-control .field input').fill('4');
await page.locator('div:nth-child(7) .form-row .c-form-row__controls .form-control .field input').click();
await page.locator('div:nth-child(7) .form-row .c-form-row__controls .form-control .field input').fill('2');
// Click OK to make generator
await Promise.all([
page.waitForNavigation({ waitUntil: 'networkidle'}),
page.locator('text=OK').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
//Wait until Save Banner is gone
await page.locator('.c-message-banner__close-button').click();
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
// click on overlay plot
await page.locator('text=Open MCT My Items >> span').nth(3).click();
await Promise.all([
page.waitForNavigation(),
page.locator('text=Unnamed Overlay Plot').first().click()
]);
}
/**
* @param {import('@playwright/test').Page} page
*/
async function testRegularTicks(page) {
const yTicks = await page.locator('.gl-plot-y-tick-label');
expect(await yTicks.count()).toBe(7);
await expect(yTicks.nth(0)).toHaveText('-2');
await expect(yTicks.nth(1)).toHaveText('0');
await expect(yTicks.nth(2)).toHaveText('2');
await expect(yTicks.nth(3)).toHaveText('4');
await expect(yTicks.nth(4)).toHaveText('6');
await expect(yTicks.nth(5)).toHaveText('8');
await expect(yTicks.nth(6)).toHaveText('10');
}
/**
* @param {import('@playwright/test').Page} page
*/
async function testLogTicks(page) {
const yTicks = await page.locator('.gl-plot-y-tick-label');
expect(await yTicks.count()).toBe(28);
await expect(yTicks.nth(0)).toHaveText('-2.98');
await expect(yTicks.nth(1)).toHaveText('-2.50');
await expect(yTicks.nth(2)).toHaveText('-2.00');
await expect(yTicks.nth(3)).toHaveText('-1.51');
await expect(yTicks.nth(4)).toHaveText('-1.20');
await expect(yTicks.nth(5)).toHaveText('-1.00');
await expect(yTicks.nth(6)).toHaveText('-0.80');
await expect(yTicks.nth(7)).toHaveText('-0.58');
await expect(yTicks.nth(8)).toHaveText('-0.40');
await expect(yTicks.nth(9)).toHaveText('-0.20');
await expect(yTicks.nth(10)).toHaveText('-0.00');
await expect(yTicks.nth(11)).toHaveText('0.20');
await expect(yTicks.nth(12)).toHaveText('0.40');
await expect(yTicks.nth(13)).toHaveText('0.58');
await expect(yTicks.nth(14)).toHaveText('0.80');
await expect(yTicks.nth(15)).toHaveText('1.00');
await expect(yTicks.nth(16)).toHaveText('1.20');
await expect(yTicks.nth(17)).toHaveText('1.51');
await expect(yTicks.nth(18)).toHaveText('2.00');
await expect(yTicks.nth(19)).toHaveText('2.50');
await expect(yTicks.nth(20)).toHaveText('2.98');
await expect(yTicks.nth(21)).toHaveText('3.50');
await expect(yTicks.nth(22)).toHaveText('4.00');
await expect(yTicks.nth(23)).toHaveText('4.50');
await expect(yTicks.nth(24)).toHaveText('5.31');
await expect(yTicks.nth(25)).toHaveText('7.00');
await expect(yTicks.nth(26)).toHaveText('8.00');
await expect(yTicks.nth(27)).toHaveText('9.00');
}
/**
* @param {import('@playwright/test').Page} page
*/
async function enableEditMode(page) {
// turn on edit mode
await page.locator('text=Unnamed Overlay Plot Snapshot >> button').nth(3).click();
await expect(await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1)).toBeVisible();
}
/**
* @param {import('@playwright/test').Page} page
*/
async function enableLogMode(page) {
// turn on log mode
await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"]').first().check();
}
/**
* @param {import('@playwright/test').Page} page
*/
async function disableLogMode(page) {
// turn off log mode
await page.locator('text=Y Axis Label Log mode Auto scale Padding >> input[type="checkbox"]').first().uncheck();
}
/**
* @param {import('@playwright/test').Page} page
*/
async function saveOverlayPlot(page) {
// save overlay plot
await page.locator('text=Snapshot Save and Finish Editing Save and Continue Editing >> button').nth(1).click();
await Promise.all([
page.locator('text=Save and Finish Editing').click(),
//Wait for Save Banner to appear
page.waitForSelector('.c-message-banner__message')
]);
//Wait until Save Banner is gone
await page.locator('.c-message-banner__close-button').click();
await page.waitForSelector('.c-message-banner__message', { state: 'detached' });
}
/**
* @param {import('@playwright/test').Page} page
*/
// FIXME: Remove this eslint exception once implemented
// eslint-disable-next-line no-unused-vars
async function testLogPlotPixels(page) {
const pixelsMatch = await page.evaluate(async () => {
// TODO get canvas pixels at a few locations to make sure they're the correct color, to test that the plot comes out as expected.
await new Promise((r) => setTimeout(r, 5 * 1000));
// These are some pixels that should be blue points in the log plot.
// If the plot changes shape to an unexpected shape, this will
// likely fail, which is what we want.
//
// I found these pixels by pausing playwright in debug mode at this
// point, and using similar code as below to output the pixel data, then
// I logged those pixels here.
const expectedBluePixels = [
// TODO these pixel sets only work with the first test, but not the second test.
// [60, 35],
// [121, 125],
// [156, 377],
// [264, 73],
// [372, 186],
// [576, 73],
// [659, 439],
// [675, 423]
[60, 35],
[120, 125],
[156, 375],
[264, 73],
[372, 185],
[575, 72],
[659, 437],
[675, 421]
];
// The first canvas in the DOM is the one that has the plot point
// icons (canvas 2d), which is the one we are testing. The second
// one in the DOM is the WebGL canvas with the line. (Why aren't
// they both WebGL?)
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
for (const pixel of expectedBluePixels) {
// XXX Possible optimization: call getImageData only once with
// area including all pixels to be tested.
const data = ctx.getImageData(pixel[0], pixel[1], 1, 1).data;
// #43b0ffff <-- openmct cyanish-blue with 100% opacity
// if (data[0] !== 0x43 || data[1] !== 0xb0 || data[2] !== 0xff || data[3] !== 0xff) {
if (data[0] === 0 && data[1] === 0 && data[2] === 0 && data[3] === 0) {
// If any pixel is empty, it means we didn't hit a plot point.
return false;
}
}
return true;
});
expect(pixelsMatch).toBe(true);
}

View File

@@ -20,12 +20,11 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
const { test } = require('../../../fixtures.js');
const { expect } = require('@playwright/test');
const { test, expect } = require('@playwright/test');
test.describe('Time conductor operations', () => {
test.describe('Time counductor operations', () => {
test('validate start time does not exceeds end time', async ({ page }) => {
// Go to baseURL
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
const year = new Date().getFullYear();
@@ -68,168 +67,3 @@ test.describe('Time conductor operations', () => {
expect(endDateValidityStatus).not.toBeTruthy();
});
});
// Testing instructions:
// Try to change the realtime offsets when in realtime (local clock) mode.
test.describe('Time conductor input fields real-time mode', () => {
test('validate input fields in real-time mode', async ({ page }) => {
const startOffset = {
secs: '23'
};
const endOffset = {
secs: '31'
};
// Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
// Switch to real-time mode
await setRealTimeMode(page);
// Set start time offset
await setStartOffset(page, startOffset);
// Verify time was updated on time offset button
await expect(page.locator('data-testid=conductor-start-offset-button')).toContainText('00:30:23');
// Set end time offset
await setEndOffset(page, endOffset);
// Verify time was updated on preceding time offset button
await expect(page.locator('data-testid=conductor-end-offset-button')).toContainText('00:00:31');
});
/**
* Verify that offsets and url params are preserved when switching
* between fixed timespan and real-time mode.
*/
test('preserve offsets and url params when switching between fixed and real-time mode', async ({ page }) => {
const startOffset = {
mins: '30',
secs: '23'
};
const endOffset = {
secs: '01'
};
// Convert offsets to milliseconds
const startDelta = (30 * 60 * 1000) + (23 * 1000);
const endDelta = (1 * 1000);
// Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
// Switch to real-time mode
await setRealTimeMode(page);
// Set start time offset
await setStartOffset(page, startOffset);
// Set end time offset
await setEndOffset(page, endOffset);
// Switch to fixed timespan mode
await setFixedTimeMode(page);
// Switch back to real-time mode
await setRealTimeMode(page);
// Verify updated start time offset persists after mode switch
await expect(page.locator('data-testid=conductor-start-offset-button')).toContainText('00:30:23');
// Verify updated end time offset persists after mode switch
await expect(page.locator('data-testid=conductor-end-offset-button')).toContainText('00:00:01');
// Verify url parameters persist after mode switch
await page.waitForNavigation();
expect(page.url()).toContain(`startDelta=${startDelta}`);
expect(page.url()).toContain(`endDelta=${endDelta}`);
});
});
/**
* @typedef {Object} OffsetValues
* @property {string | undefined} hours
* @property {string | undefined} mins
* @property {string | undefined} secs
*/
/**
* Set the values (hours, mins, secs) for the start time offset when in realtime mode
* @param {import('@playwright/test').Page} page
* @param {OffsetValues} offset
*/
async function setStartOffset(page, offset) {
const startOffsetButton = page.locator('data-testid=conductor-start-offset-button');
await setTimeConductorOffset(page, offset, startOffsetButton);
}
/**
* Set the values (hours, mins, secs) for the end time offset when in realtime mode
* @param {import('@playwright/test').Page} page
* @param {OffsetValues} offset
*/
async function setEndOffset(page, offset) {
const endOffsetButton = page.locator('data-testid=conductor-end-offset-button');
await setTimeConductorOffset(page, offset, endOffsetButton);
}
/**
* Set the time conductor to fixed timespan mode
* @param {import('@playwright/test').Page} page
*/
async function setFixedTimeMode(page) {
await setTimeConductorMode(page, true);
}
/**
* Set the time conductor to realtime mode
* @param {import('@playwright/test').Page} page
*/
async function setRealTimeMode(page) {
await setTimeConductorMode(page, false);
}
/**
* Set the values (hours, mins, secs) for the TimeConductor offsets when in realtime mode
* @param {import('@playwright/test').Page} page
* @param {OffsetValues} offset
* @param {import('@playwright/test').Locator} offsetButton
*/
async function setTimeConductorOffset(page, {hours, mins, secs}, offsetButton) {
await offsetButton.click();
if (hours) {
await page.fill('.pr-time-controls__hrs', hours);
}
if (mins) {
await page.fill('.pr-time-controls__mins', mins);
}
if (secs) {
await page.fill('.pr-time-controls__secs', secs);
}
// Click the check button
await page.locator('.icon-check').click();
}
/**
* Set the time conductor mode to either fixed timespan or realtime mode.
* @param {import('@playwright/test').Page} page
* @param {boolean} [isFixedTimespan=true] true for fixed timespan mode, false for realtime mode; default is true
*/
async function setTimeConductorMode(page, isFixedTimespan = true) {
// Click 'mode' button
await page.locator('.c-mode-button').click();
// Switch time conductor mode
if (isFixedTimespan) {
await page.locator('data-testid=conductor-modeOption-fixed').click();
} else {
await page.locator('data-testid=conductor-modeOption-realtime').click();
}
}

View File

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

View File

@@ -33,8 +33,7 @@ comfortable running this test during a live mission?" Avoid creating or deleting
Make no assumptions about the order that elements appear in the DOM.
*/
const { test } = require('../fixtures.js');
const { expect } = require('@playwright/test');
const { test, expect } = require('@playwright/test');
test('Verify that the create button appears and that the Folder Domain Object is available for selection', async ({ page }) => {

View File

@@ -47,10 +47,7 @@ test.beforeEach(async ({ context }) => {
path: path.join(__dirname, '../../..', './node_modules/sinon/pkg/sinon.js')
});
await context.addInitScript(() => {
window.__clock = sinon.useFakeTimers({
now: 0,
shouldAdvanceTime: true
}); //Set browser clock to UNIX Epoch
window.__clock = sinon.useFakeTimers(); //Set browser clock to UNIX Epoch
});
});
@@ -59,7 +56,8 @@ test('Visual - Root and About', async ({ page }) => {
await page.goto('/', { waitUntil: 'networkidle' });
// Verify that Create button is actionable
await expect(page.locator('button:has-text("Create")')).toBeEnabled();
const createButtonLocator = page.locator('button:has-text("Create")');
await expect(createButtonLocator).toBeEnabled();
// Take a snapshot of the Dashboard
await page.waitForTimeout(VISUAL_GRACE_PERIOD);
@@ -173,24 +171,3 @@ test('Visual - Sine Wave Generator Form', async ({ page }) => {
await page.waitForTimeout(VISUAL_GRACE_PERIOD);
await percySnapshot(page, 'removed amplitude property value');
});
test('Visual - Save Successful Banner', async ({ page }) => {
//Go to baseURL
await page.goto('/', { waitUntil: 'networkidle' });
//Click the Create button
await page.click('button:has-text("Create")');
//NOTE Something other than example imagery
await page.click('text=Timer');
// Click text=OK
await page.click('text=OK');
await page.locator('.c-message-banner__message').hover({ trial: true });
await percySnapshot(page, 'Banner message shown');
//Wait until Save Banner is gone
await page.waitForSelector('.c-message-banner__message', { state: 'detached'});
await percySnapshot(page, 'Banner message gone');
});

View File

@@ -21,7 +21,7 @@
*****************************************************************************/
import EventEmitter from 'EventEmitter';
import { v4 as uuid } from 'uuid';
import uuid from 'uuid';
import createExampleUser from './exampleUserCreator';
export default class ExampleUserProvider extends EventEmitter {

View File

@@ -77,7 +77,7 @@
openmct.install(openmct.plugins.LocalStorage());
openmct.install(openmct.plugins.example.Generator());
openmct.install(openmct.plugins.example.EventGeneratorPlugin());
openmct.install(openmct.plugins.example.ExampleImagery());
@@ -196,8 +196,6 @@
openmct.install(openmct.plugins.Clock({ enableClockIndicator: true }));
openmct.install(openmct.plugins.Timer());
openmct.install(openmct.plugins.Timelist());
openmct.install(openmct.plugins.BarChart());
openmct.install(openmct.plugins.ScatterPlot());
openmct.start();
</script>
</html>

View File

@@ -1,30 +1,31 @@
{
"name": "openmct",
"version": "2.0.4-SNAPSHOT",
"version": "2.0.3-SNAPSHOT",
"description": "The Open MCT core platform",
"devDependencies": {
"@babel/eslint-parser": "7.16.3",
"@braintree/sanitize-url": "6.0.0",
"@percy/cli": "1.2.1",
"@percy/playwright": "1.0.4",
"@playwright/test": "1.21.1",
"@percy/cli": "1.0.4",
"@percy/playwright": "1.0.2",
"@playwright/test": "1.19.2",
"@types/eventemitter3": "^1.0.0",
"@types/jasmine": "^4.0.1",
"@types/karma": "^6.3.2",
"@types/lodash": "^4.14.178",
"@types/mocha": "^9.1.0",
"allure-playwright": "2.0.0-beta.15",
"babel-loader": "8.2.3",
"babel-plugin-istanbul": "6.1.1",
"comma-separated-values": "3.6.4",
"copy-webpack-plugin": "11.0.0",
"copy-webpack-plugin": "10.2.0",
"cross-env": "7.0.3",
"css-loader": "4.0.0",
"d3-axis": "3.0.0",
"d3-scale": "3.3.0",
"d3-selection": "3.0.0",
"d3-axis": "1.0.x",
"d3-scale": "1.0.x",
"d3-selection": "1.3.x",
"eslint": "8.13.0",
"eslint-plugin-compat": "4.0.2",
"eslint-plugin-playwright": "0.9.0",
"eslint-plugin-playwright": "0.8.0",
"eslint-plugin-vue": "8.5.0",
"eslint-plugin-you-dont-need-lodash-underscore": "6.12.0",
"eventemitter3": "1.2.0",
@@ -34,12 +35,12 @@
"git-rev-sync": "3.0.2",
"html2canvas": "1.4.1",
"imports-loader": "0.8.0",
"jasmine-core": "4.1.1",
"jasmine-core": "4.0.1",
"jsdoc": "3.5.5",
"karma": "6.3.20",
"karma": "6.3.18",
"karma-chrome-launcher": "3.1.1",
"karma-cli": "2.0.0",
"karma-coverage": "2.2.0",
"karma-coverage": "2.1.1",
"karma-coverage-istanbul-reporter": "3.0.3",
"karma-firefox-launcher": "2.1.2",
"karma-jasmine": "4.0.1",
@@ -47,32 +48,32 @@
"karma-sourcemap-loader": "0.3.8",
"karma-spec-reporter": "0.0.34",
"karma-webpack": "5.0.0",
"lighthouse": "9.6.1",
"lighthouse": "9.5.0",
"location-bar": "3.0.1",
"lodash": "4.17.21",
"mini-css-extract-plugin": "2.6.0",
"moment": "2.29.3",
"moment": "2.29.1",
"moment-duration-format": "2.3.2",
"moment-timezone": "0.5.34",
"node-bourbon": "4.2.3",
"painterro": "1.2.56",
"plotly.js-basic-dist": "2.12.0",
"plotly.js-gl2d-dist": "2.12.0",
"plotly.js-basic-dist": "2.5.0",
"plotly.js-gl2d-dist": "2.5.0",
"printj": "1.3.1",
"request": "2.88.2",
"resolve-url-loader": "5.0.0",
"sass": "1.49.9",
"sass-loader": "12.6.0",
"sinon": "14.0.0",
"sinon": "13.0.1",
"style-loader": "^1.0.1",
"uuid": "8.3.2",
"uuid": "3.3.3",
"vue": "2.6.14",
"vue-eslint-parser": "8.3.0",
"vue-loader": "15.9.8",
"vue-template-compiler": "2.6.14",
"webpack": "5.68.0",
"webpack-cli": "4.9.2",
"webpack-dev-middleware": "5.3.3",
"webpack-dev-middleware": "5.3.1",
"webpack-hot-middleware": "2.25.1",
"webpack-merge": "5.8.0",
"zepto": "1.2.0"
@@ -81,8 +82,8 @@
"clean": "rm -rf ./dist ./node_modules ./package-lock.json",
"clean-test-lint": "npm run clean; npm install; npm run test; npm run lint",
"start": "node app.js",
"lint": "eslint example src e2e --ext .js,.vue openmct.js --max-warnings=0",
"lint:fix": "eslint example src e2e --ext .js,.vue openmct.js --fix",
"lint": "eslint example src --ext .js,.vue openmct.js",
"lint:fix": "eslint example src --ext .js,.vue openmct.js --fix",
"build:prod": "cross-env webpack --config webpack.prod.js",
"build:dev": "webpack --config webpack.dev.js",
"build:coverage": "webpack --config webpack.coverage.js",
@@ -91,12 +92,10 @@
"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:ci": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome smoke branding default condition timeConductor clock exampleImagery notebook persistence performance",
"test:e2e:ci": "npx playwright test --config=e2e/playwright-ci.config.js --project=chrome smoke default condition timeConductor",
"test:e2e:local": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome",
"test:e2e:updatesnapshots": "npx playwright test --config=e2e/playwright-local.config.js --project=chrome --grep @snapshot --update-snapshots",
"test:e2e:visual": "percy exec --config ./e2e/.percy.yml -- npx playwright test --config=e2e/playwright-visual.config.js",
"test:e2e:visual": "percy exec --config ./e2e/.percy.yml -- npx playwright test --config=e2e/playwright-visual.config.js default",
"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_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",

View File

@@ -241,7 +241,9 @@ define([
this.branding = BrandingAPI.default;
// Plugins that are installed by default
this.install(this.plugins.Plot());
this.install(this.plugins.Chart());
this.install(this.plugins.TelemetryTable.default());
this.install(PreviewPlugin.default());
this.install(LicensesPlugin.default());

View File

@@ -1,6 +1,5 @@
import AutoCompleteField from './components/controls/AutoCompleteField.vue';
import ClockDisplayFormatField from './components/controls/ClockDisplayFormatField.vue';
import CheckBoxField from './components/controls/CheckBoxField.vue';
import Datetime from './components/controls/Datetime.vue';
import FileInput from './components/controls/FileInput.vue';
import Locator from './components/controls/Locator.vue';
@@ -8,13 +7,11 @@ import NumberField from './components/controls/NumberField.vue';
import SelectField from './components/controls/SelectField.vue';
import TextAreaField from './components/controls/TextAreaField.vue';
import TextField from './components/controls/TextField.vue';
import ToggleSwitchField from './components/controls/ToggleSwitchField.vue';
import Vue from 'vue';
export const DEFAULT_CONTROLS_MAP = {
'autocomplete': AutoCompleteField,
'checkbox': CheckBoxField,
'composite': ClockDisplayFormatField,
'datetime': Datetime,
'file-input': FileInput,
@@ -22,8 +19,7 @@ export const DEFAULT_CONTROLS_MAP = {
'numberfield': NumberField,
'select': SelectField,
'textarea': TextAreaField,
'textfield': TextField,
'toggleSwitch': ToggleSwitchField
'textfield': TextField
};
export default class FormControl {
@@ -98,3 +94,4 @@ export default class FormControl {
};
}
}

View File

@@ -23,13 +23,10 @@
import FormController from './FormController';
import FormProperties from './components/FormProperties.vue';
import EventEmitter from 'EventEmitter';
import Vue from 'vue';
export default class FormsAPI extends EventEmitter {
export default class FormsAPI {
constructor(openmct) {
super();
this.openmct = openmct;
this.formController = new FormController(openmct);
}
@@ -110,8 +107,6 @@ export default class FormsAPI extends EventEmitter {
let onDismiss;
let onSave;
const self = this;
const promise = new Promise((resolve, reject) => {
onSave = onFormSave(resolve);
onDismiss = onFormDismiss(reject);
@@ -120,7 +115,7 @@ export default class FormsAPI extends EventEmitter {
const vm = new Vue({
components: { FormProperties },
provide: {
openmct: self.openmct
openmct: this.openmct
},
data() {
return {
@@ -137,7 +132,7 @@ export default class FormsAPI extends EventEmitter {
if (element) {
element.append(formElement);
} else {
overlay = self.openmct.overlays.overlay({
overlay = this.openmct.overlays.overlay({
element: vm.$el,
size: 'small',
onDestroy: () => vm.$destroy()
@@ -145,7 +140,6 @@ export default class FormsAPI extends EventEmitter {
}
function onFormPropertyChange(data) {
self.emit('onFormPropertyChange', data);
if (onChange) {
onChange(data);
}

View File

@@ -1,157 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { createOpenMct, resetApplicationState } from '../../utils/testing';
describe('The Forms API', () => {
let openmct;
let element;
beforeEach((done) => {
element = document.createElement('div');
element.style.display = 'block';
element.style.width = '1920px';
element.style.height = '1080px';
openmct = createOpenMct();
openmct.on('start', done);
openmct.startHeadless(element);
});
afterEach(() => {
return resetApplicationState(openmct);
});
it('openmct supports form API', () => {
expect(openmct.forms).not.toBe(null);
});
describe('check default form controls exists', () => {
it('autocomplete', () => {
const control = openmct.forms.getFormControl('autocomplete');
expect(control).not.toBe(null);
});
it('clock', () => {
const control = openmct.forms.getFormControl('composite');
expect(control).not.toBe(null);
});
it('datetime', () => {
const control = openmct.forms.getFormControl('datetime');
expect(control).not.toBe(null);
});
it('file-input', () => {
const control = openmct.forms.getFormControl('file-input');
expect(control).not.toBe(null);
});
it('locator', () => {
const control = openmct.forms.getFormControl('locator');
expect(control).not.toBe(null);
});
it('numberfield', () => {
const control = openmct.forms.getFormControl('numberfield');
expect(control).not.toBe(null);
});
it('select', () => {
const control = openmct.forms.getFormControl('select');
expect(control).not.toBe(null);
});
it('textarea', () => {
const control = openmct.forms.getFormControl('textarea');
expect(control).not.toBe(null);
});
it('textfield', () => {
const control = openmct.forms.getFormControl('textfield');
expect(control).not.toBe(null);
});
});
it('supports user defined form controls', () => {
const newFormControl = {
show: () => {
console.log('show new control');
},
destroy: () => {
console.log('destroy');
}
};
openmct.forms.addNewFormControl('newFormControl', newFormControl);
const control = openmct.forms.getFormControl('newFormControl');
expect(control).not.toBe(null);
expect(control.show).not.toBe(null);
expect(control.destroy).not.toBe(null);
});
describe('show form on UI', () => {
let formStructure;
beforeEach(() => {
formStructure = {
title: 'Test Show Form',
sections: [
{
rows: [
{
key: 'name',
control: 'textfield',
name: 'Title',
pattern: '\\S+',
required: false,
cssClass: 'l-input-lg',
value: 'Test Name'
}
]
}
]
};
});
it('when container element is provided', (done) => {
openmct.forms.showForm(formStructure, { element }).catch(() => {
done();
});
const titleElement = element.querySelector('.c-overlay__dialog-title');
expect(titleElement.textContent).toBe(formStructure.title);
element.querySelector('.js-cancel-button').click();
});
it('when container element is not provided', (done) => {
openmct.forms.showForm(formStructure).catch(() => {
done();
});
const titleElement = document.querySelector('.c-overlay__dialog-title');
const title = titleElement.textContent;
expect(title).toBe(formStructure.title);
document.querySelector('.js-cancel-button').click();
});
});
});

View File

@@ -21,9 +21,9 @@
*****************************************************************************/
<template>
<div class="c-form js-form">
<div class="c-form">
<div class="c-overlay__top-bar c-form__top-bar">
<div class="c-overlay__dialog-title js-form-title">{{ model.title }}</div>
<div class="c-overlay__dialog-title">{{ model.title }}</div>
<div class="c-overlay__dialog-hint hint">All fields marked <span class="req icon-asterisk"></span> are required.</div>
</div>
<form
@@ -70,7 +70,7 @@
</button>
<button
tabindex="0"
class="c-button js-cancel-button"
class="c-button"
@click="onDismiss"
>
{{ cancelLabel }}
@@ -81,7 +81,7 @@
<script>
import FormRow from "@/api/forms/components/FormRow.vue";
import { v4 as uuid } from 'uuid';
import uuid from 'uuid';
export default {
components: {

View File

@@ -79,12 +79,10 @@ export default {
rowClass() {
let cssClass = this.cssClass;
if (!this.row.required) {
return;
if (this.row.required) {
cssClass = `${cssClass} req`;
}
cssClass = `${cssClass} req`;
if (this.visited && this.valid !== undefined) {
if (this.valid === true) {
cssClass = `${cssClass} valid`;

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.
*****************************************************************************/
<template>
<span class="form-control shell">
<span
class="field control"
:class="model.cssClass"
>
<input
type="checkbox"
:checked="isChecked"
@input="toggleCheckBox"
>
</span>
</span>
</template>
<script>
import toggleMixin from '../../toggle-check-box-mixin';
export default {
mixins: [toggleMixin],
props: {
model: {
type: Object,
required: true
}
},
data() {
return {
isChecked: this.model.value
};
}
};
</script>

View File

@@ -40,12 +40,6 @@
>
{{ name }}
</button>
<button
v-if="removable"
class="c-button icon-trash"
title="Remove file"
@click="removeFile"
></button>
</span>
</span>
</template>
@@ -69,9 +63,6 @@ export default {
const fileInfo = this.fileInfo || this.model.value;
return fileInfo && fileInfo.name || this.model.text;
},
removable() {
return (this.fileInfo || this.model.value) && this.model.removable;
}
},
mounted() {
@@ -106,15 +97,6 @@ export default {
},
selectFile() {
this.$refs.fileInput.click();
},
removeFile() {
this.model.value = undefined;
this.fileInfo = undefined;
const data = {
model: this.model,
value: undefined
};
this.$emit('onChange', data);
}
}
};

View File

@@ -58,6 +58,7 @@ export default {
},
methods: {
updateText() {
console.log('updateText', this.field);
const data = {
model: this.model,
value: this.field

View File

@@ -1,62 +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.
*****************************************************************************/
<template>
<span class="form-control shell">
<span
class="field control"
:class="model.cssClass"
>
<ToggleSwitch
id="switchId"
:checked="isChecked"
@change="toggleCheckBox"
/>
</span>
</span>
</template>
<script>
import toggleMixin from '../../toggle-check-box-mixin';
import ToggleSwitch from '@/ui/components/ToggleSwitch.vue';
import { v4 as uuid } from 'uuid';
export default {
components: {
ToggleSwitch
},
mixins: [toggleMixin],
props: {
model: {
type: Object,
required: true
}
},
data() {
return {
switchId: `toggleSwitch-${uuid}`,
isChecked: this.model.value
};
}
};
</script>

View File

@@ -1,19 +0,0 @@
export default {
data() {
return {
isChecked: false
};
},
methods: {
toggleCheckBox(event) {
this.isChecked = !this.isChecked;
const data = {
model: this.model,
value: this.isChecked
};
this.$emit('onChange', data);
}
}
};

View File

@@ -26,31 +26,29 @@ import { createOpenMct, createMouseEvent, resetApplicationState } from '../../ut
describe ('The Menu API', () => {
let openmct;
let appHolder;
let element;
let menuAPI;
let actionsArray;
let x;
let y;
let result;
let menuElement;
const x = 8;
const y = 16;
const menuOptions = {
onDestroy: () => {
console.log('default onDestroy');
}
};
let onDestroy;
beforeEach((done) => {
appHolder = document.createElement('div');
const appHolder = document.createElement('div');
appHolder.style.display = 'block';
appHolder.style.width = '1920px';
appHolder.style.height = '1080px';
openmct = createOpenMct();
element = document.createElement('div');
element.style.display = 'block';
element.style.width = '1920px';
element.style.height = '1080px';
openmct.on('start', done);
openmct.startHeadless();
openmct.startHeadless(appHolder);
menuAPI = new MenuAPI(openmct);
actionsArray = [
@@ -58,7 +56,7 @@ describe ('The Menu API', () => {
key: 'test-css-class-1',
name: 'Test Action 1',
cssClass: 'icon-clock',
description: 'This is a test action 1',
description: 'This is a test action',
onItemClicked: () => {
result = 'Test Action 1 Invoked';
}
@@ -67,165 +65,149 @@ describe ('The Menu API', () => {
key: 'test-css-class-2',
name: 'Test Action 2',
cssClass: 'icon-clock',
description: 'This is a test action 2',
description: 'This is a test action',
onItemClicked: () => {
result = 'Test Action 2 Invoked';
}
}
];
x = 8;
y = 16;
});
afterEach(() => {
return resetApplicationState(openmct);
});
describe('showMenu method', () => {
beforeAll(() => {
spyOn(menuOptions, 'onDestroy').and.callThrough();
});
it('creates an instance of Menu when invoked', (done) => {
menuOptions.onDestroy = done;
menuAPI.showMenu(x, y, actionsArray, menuOptions);
describe("showMenu method", () => {
it("creates an instance of Menu when invoked", () => {
menuAPI.showMenu(x, y, actionsArray);
expect(menuAPI.menuComponent).toBeInstanceOf(Menu);
document.body.click();
});
describe('creates a menu component', () => {
it('with all the actions passed in', (done) => {
menuOptions.onDestroy = done;
describe("creates a menu component", () => {
let menuComponent;
let vueComponent;
beforeEach(() => {
onDestroy = jasmine.createSpy('onDestroy');
const menuOptions = {
onDestroy
};
menuAPI.showMenu(x, y, actionsArray, menuOptions);
menuElement = document.querySelector('.c-menu');
expect(menuElement).toBeDefined();
vueComponent = menuAPI.menuComponent.component;
menuComponent = document.querySelector(".c-menu");
const listItems = menuElement.children[0].children;
spyOn(vueComponent, '$destroy');
});
it("renders a menu component in the expected x and y coordinates", () => {
let boundingClientRect = menuComponent.getBoundingClientRect();
let left = boundingClientRect.left;
let top = boundingClientRect.top;
expect(left).toEqual(x);
expect(top).toEqual(y);
});
it("with all the actions passed in", () => {
expect(menuComponent).toBeDefined();
let listItems = menuComponent.children[0].children;
expect(listItems.length).toEqual(actionsArray.length);
document.body.click();
});
it('with click-able menu items, that will invoke the correct callBack', (done) => {
menuOptions.onDestroy = done;
menuAPI.showMenu(x, y, actionsArray, menuOptions);
menuElement = document.querySelector('.c-menu');
const listItem1 = menuElement.children[0].children[0];
it("with click-able menu items, that will invoke the correct callBacks", () => {
let listItem1 = menuComponent.children[0].children[0];
listItem1.click();
expect(result).toEqual('Test Action 1 Invoked');
expect(result).toEqual("Test Action 1 Invoked");
});
it('dismisses the menu when action is clicked on', (done) => {
menuOptions.onDestroy = done;
it("dismisses the menu when action is clicked on", () => {
let listItem1 = menuComponent.children[0].children[0];
menuAPI.showMenu(x, y, actionsArray, menuOptions);
menuElement = document.querySelector('.c-menu');
const listItem1 = menuElement.children[0].children[0];
listItem1.click();
menuElement = document.querySelector('.c-menu');
let menu = document.querySelector('.c-menu');
expect(menuElement).toBeNull();
expect(menu).toBeNull();
});
it('invokes the destroy method when menu is dismissed', (done) => {
menuOptions.onDestroy = done;
menuAPI.showMenu(x, y, actionsArray, menuOptions);
const vueComponent = menuAPI.menuComponent.component;
spyOn(vueComponent, '$destroy');
it("invokes the destroy method when menu is dismissed", () => {
document.body.click();
expect(vueComponent.$destroy).toHaveBeenCalled();
});
it('invokes the onDestroy callback if passed in', (done) => {
let count = 0;
menuOptions.onDestroy = () => {
count++;
expect(count).toEqual(1);
done();
};
menuAPI.showMenu(x, y, actionsArray, menuOptions);
it("invokes the onDestroy callback if passed in", () => {
document.body.click();
expect(onDestroy).toHaveBeenCalled();
});
});
});
describe('superMenu method', () => {
it('creates a superMenu', (done) => {
menuOptions.onDestroy = done;
describe("superMenu method", () => {
it("creates a superMenu", () => {
menuAPI.showSuperMenu(x, y, actionsArray);
menuAPI.showSuperMenu(x, y, actionsArray, menuOptions);
menuElement = document.querySelector('.c-super-menu__menu');
const superMenu = document.querySelector('.c-super-menu__menu');
expect(menuElement).not.toBeNull();
document.body.click();
expect(superMenu).not.toBeNull();
});
it('Mouse over a superMenu shows correct description', (done) => {
menuOptions.onDestroy = done;
it("Mouse over a superMenu shows correct description", (done) => {
menuAPI.showSuperMenu(x, y, actionsArray);
menuAPI.showSuperMenu(x, y, actionsArray, menuOptions);
menuElement = document.querySelector('.c-super-menu__menu');
const superMenuItem = menuElement.querySelector('li');
const superMenu = document.querySelector('.c-super-menu__menu');
const superMenuItem = superMenu.querySelector('li');
const mouseOverEvent = createMouseEvent('mouseover');
superMenuItem.dispatchEvent(mouseOverEvent);
const itemDescription = document.querySelector('.l-item-description__description');
menuAPI.menuComponent.component.$nextTick(() => {
expect(menuElement).not.toBeNull();
setTimeout(() => {
expect(itemDescription.innerText).toEqual(actionsArray[0].description);
document.body.click();
});
expect(superMenu).not.toBeNull();
done();
}, 300);
});
});
describe('Menu Placements', () => {
it('default menu position BOTTOM_RIGHT', (done) => {
menuOptions.onDestroy = done;
describe("Menu Placements", () => {
it("default menu position BOTTOM_RIGHT", () => {
menuAPI.showMenu(x, y, actionsArray);
menuAPI.showMenu(x, y, actionsArray, menuOptions);
menuElement = document.querySelector('.c-menu');
const menu = document.querySelector('.c-menu');
const boundingClientRect = menuElement.getBoundingClientRect();
const boundingClientRect = menu.getBoundingClientRect();
const left = boundingClientRect.left;
const top = boundingClientRect.top;
expect(left).toEqual(x);
expect(top).toEqual(y);
document.body.click();
});
it('menu position BOTTOM_RIGHT', (done) => {
menuOptions.onDestroy = done;
menuOptions.placement = openmct.menus.menuPlacement.BOTTOM_RIGHT;
it("menu position BOTTOM_RIGHT", () => {
const menuOptions = {
placement: openmct.menus.menuPlacement.BOTTOM_RIGHT
};
menuAPI.showMenu(x, y, actionsArray, menuOptions);
menuElement = document.querySelector('.c-menu');
const boundingClientRect = menuElement.getBoundingClientRect();
const menu = document.querySelector('.c-menu');
const boundingClientRect = menu.getBoundingClientRect();
const left = boundingClientRect.left;
const top = boundingClientRect.top;
expect(left).toEqual(x);
expect(top).toEqual(y);
document.body.click();
});
});
});

View File

@@ -12,7 +12,6 @@
:key="action.name"
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
:title="action.description"
:data-testid="action.testId || false"
@click="action.onItemClicked"
>
{{ action.name }}
@@ -38,7 +37,6 @@
:key="action.name"
:class="action.cssClass"
:title="action.description"
:data-testid="action.testId || false"
@click="action.onItemClicked"
>
{{ action.name }}

View File

@@ -15,7 +15,6 @@
:key="action.name"
:class="[action.cssClass, action.isDisabled ? 'disabled' : '']"
:title="action.description"
:data-testid="action.testId || false"
@click="action.onItemClicked"
@mouseover="toggleItemDescription(action)"
@mouseleave="toggleItemDescription()"
@@ -46,7 +45,6 @@
:key="action.name"
:class="action.cssClass"
:title="action.description"
:data-testid="action.testId || false"
@click="action.onItemClicked"
@mouseover="toggleItemDescription(action)"
@mouseleave="toggleItemDescription()"

View File

@@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { v4 as uuid } from 'uuid';
import uuid from 'uuid';
class InMemorySearchProvider {
/**
@@ -36,14 +36,13 @@ class InMemorySearchProvider {
*/
this.MAX_CONCURRENT_REQUESTS = 100;
/**
* If max results is not specified in query, use this as default.
*/
* If max results is not specified in query, use this as default.
*/
this.DEFAULT_MAX_RESULTS = 100;
this.openmct = openmct;
this.indexedIds = {};
this.indexedCompositions = {};
this.idsToIndex = [];
this.pendingIndex = {};
this.pendingRequests = 0;
@@ -59,6 +58,7 @@ class InMemorySearchProvider {
this.onWorkerMessageError = this.onWorkerMessageError.bind(this);
this.onerror = this.onWorkerError.bind(this);
this.startIndexing = this.startIndexing.bind(this);
this.onMutationOfIndexedObject = this.onMutationOfIndexedObject.bind(this);
this.openmct.on('start', this.startIndexing);
this.openmct.on('destroy', () => {
@@ -68,9 +68,6 @@ class InMemorySearchProvider {
this.worker.port.onmessageerror = null;
this.worker.port.close();
}
this.destroyObservers(this.indexedIds);
this.destroyObservers(this.indexedCompositions);
});
}
@@ -140,7 +137,7 @@ class InMemorySearchProvider {
};
modelResults.hits = await Promise.all(event.data.results.map(async (hit) => {
const identifier = this.openmct.objects.parseKeyString(hit.keyString);
const domainObject = await this.openmct.objects.get(identifier);
const domainObject = await this.openmct.objects.get(identifier.key);
return domainObject;
}));
@@ -216,52 +213,29 @@ class InMemorySearchProvider {
}
}
onNameMutation(domainObject, name) {
onMutationOfIndexedObject(domainObject) {
const provider = this;
domainObject.name = name;
provider.index(domainObject);
}
onCompositionMutation(domainObject, composition) {
const provider = this;
const indexedComposition = domainObject.composition;
const identifiersToIndex = composition
.filter(identifier => !indexedComposition
.some(indexedIdentifier => this.openmct.objects
.areIdsEqual([identifier, indexedIdentifier])));
identifiersToIndex.forEach(identifier => {
this.openmct.objects.get(identifier).then(objectToIndex => provider.index(objectToIndex));
});
provider.index(domainObject.identifier, domainObject);
}
/**
* Pass a domainObject to the worker to be indexed.
* If the object has composition, schedule those ids for later indexing.
* Watch for object changes and re-index object and children if so
* Pass an id and model to the worker to be indexed. If the model has
* composition, schedule those ids for later indexing.
*
* @private
* @param domainObject a domainObject
* @param id a model id
* @param model a model
*/
async index(domainObject) {
async index(id, domainObject) {
const provider = this;
const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
const keyString = this.openmct.objects.makeKeyString(id);
if (!this.indexedIds[keyString]) {
this.indexedIds[keyString] = this.openmct.objects.observe(
domainObject,
'name',
this.onNameMutation.bind(this, domainObject)
);
this.indexedCompositions[keyString] = this.openmct.objects.observe(
domainObject,
'composition',
this.onCompositionMutation.bind(this, domainObject)
);
this.openmct.objects.observe(domainObject, `*`, this.onMutationOfIndexedObject);
}
if ((keyString !== 'ROOT')) {
this.indexedIds[keyString] = true;
if ((id.key !== 'ROOT')) {
if (this.worker) {
this.worker.port.postMessage({
request: 'index',
@@ -273,12 +247,15 @@ class InMemorySearchProvider {
}
}
const composition = this.openmct.composition.get(domainObject);
const composition = this.openmct.composition.registry.find(foundComposition => {
return foundComposition.appliesTo(domainObject);
});
if (composition !== undefined) {
const children = await composition.load();
children.forEach(child => provider.scheduleForIndexing(child.identifier));
if (composition) {
const childIdentifiers = await composition.load(domainObject);
childIdentifiers.forEach(function (childIdentifier) {
provider.scheduleForIndexing(childIdentifier);
});
}
}
@@ -294,12 +271,12 @@ class InMemorySearchProvider {
const provider = this;
this.pendingRequests += 1;
const domainObject = await this.openmct.objects.get(keyString);
const identifier = await this.openmct.objects.parseKeyString(keyString);
const domainObject = await this.openmct.objects.get(identifier.key);
delete provider.pendingIndex[keyString];
try {
if (domainObject) {
await provider.index(domainObject);
await provider.index(identifier, domainObject);
}
} catch (error) {
console.warn('Failed to index domain object ' + keyString, error);
@@ -328,9 +305,9 @@ class InMemorySearchProvider {
}
/**
* A local version of the same SharedWorker function
* if we don't have SharedWorkers available (e.g., iOS)
*/
* A local version of the same SharedWorker function
* if we don't have SharedWorkers available (e.g., iOS)
*/
localIndexItem(keyString, model) {
this.localIndexedItems[keyString] = {
type: model.type,
@@ -370,16 +347,6 @@ class InMemorySearchProvider {
};
this.onWorkerMessage(eventToReturn);
}
destroyObservers(observers) {
Object.entries(observers).forEach(([keyString, unobserve]) => {
if (typeof unobserve === 'function') {
unobserve();
}
delete observers[keyString];
});
}
}
export default InMemorySearchProvider;

View File

@@ -105,18 +105,13 @@ describe("The Object API Search Function", () => {
beforeEach((done) => {
openmct = createOpenMct();
const defaultObjectProvider = openmct.objects.getProvider({
key: '',
namespace: ''
});
openmct.objects.addProvider('foo', defaultObjectProvider);
spyOn(openmct.objects.inMemorySearchProvider, "query").and.callThrough();
spyOn(openmct.objects.inMemorySearchProvider, "localSearch").and.callThrough();
openmct.on('start', async () => {
mockIdentifier1 = {
key: 'some-object',
namespace: 'foo'
namespace: 'some-namespace'
};
mockDomainObject1 = {
type: 'clock',
@@ -125,7 +120,7 @@ describe("The Object API Search Function", () => {
};
mockIdentifier2 = {
key: 'some-other-object',
namespace: 'foo'
namespace: 'some-namespace'
};
mockDomainObject2 = {
type: 'clock',
@@ -134,16 +129,16 @@ describe("The Object API Search Function", () => {
};
mockIdentifier3 = {
key: 'yet-another-object',
namespace: 'foo'
namespace: 'some-namespace'
};
mockDomainObject3 = {
type: 'clock',
name: 'redBear',
identifier: mockIdentifier3
};
await openmct.objects.inMemorySearchProvider.index(mockDomainObject1);
await openmct.objects.inMemorySearchProvider.index(mockDomainObject2);
await openmct.objects.inMemorySearchProvider.index(mockDomainObject3);
await openmct.objects.inMemorySearchProvider.index(mockIdentifier1, mockDomainObject1);
await openmct.objects.inMemorySearchProvider.index(mockIdentifier2, mockDomainObject2);
await openmct.objects.inMemorySearchProvider.index(mockIdentifier3, mockDomainObject3);
done();
});
openmct.startHeadless();
@@ -180,9 +175,9 @@ describe("The Object API Search Function", () => {
beforeEach(async () => {
openmct.objects.inMemorySearchProvider.worker = null;
// reindex locally
await openmct.objects.inMemorySearchProvider.index(mockDomainObject1);
await openmct.objects.inMemorySearchProvider.index(mockDomainObject2);
await openmct.objects.inMemorySearchProvider.index(mockDomainObject3);
await openmct.objects.inMemorySearchProvider.index(mockIdentifier1, mockDomainObject1);
await openmct.objects.inMemorySearchProvider.index(mockIdentifier2, mockDomainObject2);
await openmct.objects.inMemorySearchProvider.index(mockIdentifier3, mockDomainObject3);
});
it("calls local search", () => {
openmct.objects.search('foo');

View File

@@ -22,14 +22,12 @@
export default class Transaction {
constructor(objectAPI) {
this.dirtyObjects = {};
this.dirtyObjects = new Set();
this.objectAPI = objectAPI;
}
add(object) {
const key = this.objectAPI.makeKeyString(object.identifier);
this.dirtyObjects[key] = object;
this.dirtyObjects.add(object);
}
cancel() {
@@ -39,8 +37,7 @@ export default class Transaction {
commit() {
const promiseArray = [];
const save = this.objectAPI.save.bind(this.objectAPI);
Object.values(this.dirtyObjects).forEach(object => {
this.dirtyObjects.forEach(object => {
promiseArray.push(this.createDirtyObjectPromise(object, save));
});
@@ -51,9 +48,7 @@ export default class Transaction {
return new Promise((resolve, reject) => {
action(object)
.then((success) => {
const key = this.objectAPI.makeKeyString(object.identifier);
delete this.dirtyObjects[key];
this.dirtyObjects.delete(object);
resolve(success);
})
.catch(reject);
@@ -62,8 +57,7 @@ export default class Transaction {
getDirtyObject(identifier) {
let dirtyObject;
Object.values(this.dirtyObjects).forEach(object => {
this.dirtyObjects.forEach(object => {
const areIdsEqual = this.objectAPI.areIdsEqual(object.identifier, identifier);
if (areIdsEqual) {
dirtyObject = object;
@@ -73,11 +67,14 @@ export default class Transaction {
return dirtyObject;
}
start() {
this.dirtyObjects = new Set();
}
_clear() {
const promiseArray = [];
const refresh = this.objectAPI.refresh.bind(this.objectAPI);
Object.values(this.dirtyObjects).forEach(object => {
this.dirtyObjects.forEach(object => {
promiseArray.push(this.createDirtyObjectPromise(object, refresh));
});

View File

@@ -34,24 +34,24 @@ describe("Transaction Class", () => {
});
it('has no dirty objects', () => {
expect(Object.keys(transaction.dirtyObjects).length).toEqual(0);
expect(transaction.dirtyObjects.size).toEqual(0);
});
it('add(), adds object to dirtyObjects', () => {
const mockDomainObjects = createMockDomainObjects();
transaction.add(mockDomainObjects[0]);
expect(Object.keys(transaction.dirtyObjects).length).toEqual(1);
expect(transaction.dirtyObjects.size).toEqual(1);
});
it('cancel(), clears all dirtyObjects', (done) => {
const mockDomainObjects = createMockDomainObjects(3);
mockDomainObjects.forEach(transaction.add.bind(transaction));
expect(Object.keys(transaction.dirtyObjects).length).toEqual(3);
expect(transaction.dirtyObjects.size).toEqual(3);
transaction.cancel()
.then(success => {
expect(Object.keys(transaction.dirtyObjects).length).toEqual(0);
expect(transaction.dirtyObjects.size).toEqual(0);
}).finally(done);
});
@@ -59,12 +59,12 @@ describe("Transaction Class", () => {
const mockDomainObjects = createMockDomainObjects(3);
mockDomainObjects.forEach(transaction.add.bind(transaction));
expect(Object.keys(transaction.dirtyObjects).length).toEqual(3);
expect(transaction.dirtyObjects.size).toEqual(3);
spyOn(objectAPI, 'save').and.callThrough();
transaction.commit()
.then(success => {
expect(Object.keys(transaction.dirtyObjects).length).toEqual(0);
expect(transaction.dirtyObjects.size).toEqual(0);
expect(objectAPI.save.calls.count()).toEqual(3);
}).finally(done);
});
@@ -73,7 +73,7 @@ describe("Transaction Class", () => {
const mockDomainObjects = createMockDomainObjects();
transaction.add(mockDomainObjects[0]);
expect(Object.keys(transaction.dirtyObjects).length).toEqual(1);
expect(transaction.dirtyObjects.size).toEqual(1);
const dirtyObject = transaction.getDirtyObject(mockDomainObjects[0].identifier);
expect(dirtyObject).toEqual(mockDomainObjects[0]);
@@ -82,7 +82,7 @@ describe("Transaction Class", () => {
it('getDirtyObject(), returns empty dirtyObject for no active transaction', () => {
const mockDomainObjects = createMockDomainObjects();
expect(Object.keys(transaction.dirtyObjects).length).toEqual(0);
expect(transaction.dirtyObjects.size).toEqual(0);
const dirtyObject = transaction.getDirtyObject(mockDomainObjects[0].identifier);
expect(dirtyObject).toEqual(undefined);

View File

@@ -512,7 +512,7 @@ define([
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';
const hasRequestProvider = Object.hasOwn(requestProvider, 'request');
return supportsRequest && hasRequestProvider;
});

View File

@@ -22,7 +22,11 @@
import _ from 'lodash';
import EventEmitter from 'EventEmitter';
import { LOADED_ERROR, TIMESYSTEM_KEY_NOTIFICATION, TIMESYSTEM_KEY_WARNING } from './constants';
const ERRORS = {
TIMESYSTEM_KEY: 'All telemetry metadata must have a telemetry value with a key that matches the key of the active time system.',
LOADED: 'Telemetry Collection has already been loaded.'
};
/** Class representing a Telemetry Collection. */
@@ -57,7 +61,7 @@ export class TelemetryCollection extends EventEmitter {
*/
load() {
if (this.loaded) {
this._error(LOADED_ERROR);
this._error(ERRORS.LOADED);
}
this._setTimeSystem(this.openmct.time.timeSystem());
@@ -168,7 +172,6 @@ export class TelemetryCollection extends EventEmitter {
* @private
*/
_processNewTelemetry(telemetryData) {
performance.mark('tlm:process:start');
if (telemetryData === undefined) {
return;
}
@@ -263,10 +266,6 @@ export class TelemetryCollection extends EventEmitter {
this.lastBounds = bounds;
if (isTick) {
if (this.timeKey === undefined) {
return;
}
// need to check futureBuffer and need to check
// if anything has fallen out of bounds
let startIndex = 0;
@@ -306,6 +305,7 @@ export class TelemetryCollection extends EventEmitter {
if (added.length > 0) {
this.emit('add', added);
}
} else {
// user bounds change, reset
this._reset();
@@ -325,16 +325,12 @@ export class TelemetryCollection extends EventEmitter {
let domains = this.metadata.valuesForHints(['domain']);
let domain = domains.find((d) => d.key === timeSystem.key);
if (domain !== undefined) {
// timeKey is used to create a dummy datum used for sorting
this.timeKey = domain.source;
} else {
this.timeKey = undefined;
this._warn(TIMESYSTEM_KEY_WARNING);
this.openmct.notifications.alert(TIMESYSTEM_KEY_NOTIFICATION);
if (domain === undefined) {
this._error(ERRORS.TIMESYSTEM_KEY);
}
// timeKey is used to create a dummy datum used for sorting
this.timeKey = domain.source; // this defaults to key if no source is set
let metadataValue = this.metadata.value(timeSystem.key) || { format: timeSystem.key };
let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
@@ -356,7 +352,6 @@ export class TelemetryCollection extends EventEmitter {
* @todo handle subscriptions more granually
*/
_reset() {
performance.mark('tlm:reset');
this.boundedTelemetry = [];
this.futureBuffer = [];
@@ -405,8 +400,4 @@ export class TelemetryCollection extends EventEmitter {
_error(message) {
throw new Error(message);
}
_warn(message) {
console.warn(message);
}
}

View File

@@ -1,101 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import {
createOpenMct,
resetApplicationState
} from 'utils/testing';
import { TIMESYSTEM_KEY_WARNING } from './constants';
describe('Telemetry Collection', () => {
let openmct;
let mockMetadataProvider;
let mockMetadata = {};
let domainObject;
beforeEach(done => {
openmct = createOpenMct();
openmct.on('start', done);
domainObject = {
identifier: {
key: 'a',
namespace: 'b'
},
type: 'sample-type'
};
mockMetadataProvider = {
key: 'mockMetadataProvider',
supportsMetadata() {
return true;
},
getMetadata() {
return mockMetadata;
}
};
openmct.telemetry.addProvider(mockMetadataProvider);
openmct.startHeadless();
});
afterEach(() => {
return resetApplicationState();
});
it('Warns if telemetry metadata does not match the active timesystem', () => {
mockMetadata.values = [
{
key: 'foo',
name: 'Bar',
hints: {
domain: 1
}
}
];
const telemetryCollection = openmct.telemetry.requestCollection(domainObject);
spyOn(telemetryCollection, '_warn');
telemetryCollection.load();
expect(telemetryCollection._warn).toHaveBeenCalledOnceWith(TIMESYSTEM_KEY_WARNING);
});
it('Does not warn if telemetry metadata matches the active timesystem', () => {
mockMetadata.values = [
{
key: 'utc',
name: 'Timestamp',
format: 'utc',
hints: {
domain: 1
}
}
];
const telemetryCollection = openmct.telemetry.requestCollection(domainObject);
spyOn(telemetryCollection, '_warn');
telemetryCollection.load();
expect(telemetryCollection._warn).not.toHaveBeenCalled();
});
});

View File

@@ -1,25 +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 const TIMESYSTEM_KEY_WARNING = 'All telemetry metadata must have a telemetry value with a key that matches the key of the active time system.';
export const TIMESYSTEM_KEY_NOTIFICATION = 'Telemetry metadata does not match the active time system.';
export const LOADED_ERROR = 'Telemetry Collection has already been loaded.';

View File

@@ -40,13 +40,11 @@ describe("The User API", () => {
});
afterEach(() => {
const activeOverlays = openmct.overlays.activeOverlays;
activeOverlays.forEach(overlay => overlay.dismiss());
return resetApplicationState(openmct);
});
describe('with regard to user providers', () => {
it('allows you to specify a user provider', () => {
openmct.user.on('providerAdded', (provider) => {
expect(provider).toBeInstanceOf(ExampleUserProvider);

View File

@@ -33,7 +33,7 @@ function replaceDotsWithUnderscores(filename) {
import {saveAs} from 'saveAs';
import html2canvas from 'html2canvas';
import { v4 as uuid } from 'uuid';
import uuid from 'uuid';
class ImageExporter {
constructor(openmct) {
@@ -51,7 +51,7 @@ class ImageExporter {
const overlays = this.openmct.overlays;
const dialog = overlays.dialog({
iconClass: 'info',
message: 'Capturing image, please wait...',
message: 'Caputuring an image',
buttons: [
{
label: 'Cancel',

View File

@@ -52,6 +52,7 @@ export default (agent, document) => {
if (agent.isMobile()) {
const mediaQuery = window.matchMedia("(orientation: landscape)");
function eventHandler(event) {
console.log("changed");
if (event.matches) {
body.classList.remove("portrait");
body.classList.add("landscape");

View File

@@ -1,3 +1,4 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
@@ -113,12 +114,14 @@ export default {
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
this.formats = this.openmct.telemetry.getFormatMap(this.metadata);
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
this.bounds = this.openmct.time.bounds();
this.limitEvaluator = this.openmct
.telemetry
.limitEvaluator(this.domainObject);
this.openmct.time.on('timeSystem', this.updateTimeSystem);
this.openmct.time.on('bounds', this.updateBounds);
this.timestampKey = this.openmct.time.timeSystem().key;
@@ -132,41 +135,72 @@ export default {
this.valueKey = this.valueMetadata ? this.valueMetadata.key : undefined;
this.telemetryCollection = this.openmct.telemetry.requestCollection(this.domainObject, {
size: 1,
strategy: 'latest'
});
this.telemetryCollection.on('add', this.setLatestValues);
this.telemetryCollection.on('clear', this.resetValues);
this.telemetryCollection.load();
this.unsubscribe = this.openmct
.telemetry
.subscribe(this.domainObject, this.setLatestValues);
this.requestHistory();
if (this.hasUnits) {
this.setUnit();
}
},
destroyed() {
this.unsubscribe();
this.openmct.time.off('timeSystem', this.updateTimeSystem);
this.telemetryCollection.off('add', this.setLatestValues);
this.telemetryCollection.off('clear', this.resetValues);
this.telemetryCollection.destroy();
this.openmct.time.off('bounds', this.updateBounds);
},
methods: {
updateView() {
if (!this.updatingView) {
this.updatingView = true;
requestAnimationFrame(() => {
this.timestamp = this.getParsedTimestamp(this.latestDatum);
this.datum = this.latestDatum;
let newTimestamp = this.getParsedTimestamp(this.latestDatum);
if (this.shouldUpdate(newTimestamp)) {
this.timestamp = newTimestamp;
this.datum = this.latestDatum;
}
this.updatingView = false;
});
}
},
setLatestValues(data) {
this.latestDatum = data[data.length - 1];
setLatestValues(datum) {
this.latestDatum = datum;
this.updateView();
},
shouldUpdate(newTimestamp) {
return this.inBounds(newTimestamp)
&& (this.timestamp === undefined || newTimestamp > this.timestamp);
},
requestHistory() {
this.openmct
.telemetry
.request(this.domainObject, {
start: this.bounds.start,
end: this.bounds.end,
size: 1,
strategy: 'latest'
})
.then((array) => this.setLatestValues(array[array.length - 1]))
.catch((error) => {
console.warn('Error fetching data', error);
});
},
updateBounds(bounds, isTick) {
this.bounds = bounds;
if (!isTick) {
this.resetValues();
this.requestHistory();
}
},
inBounds(timestamp) {
return timestamp >= this.bounds.start && timestamp <= this.bounds.end;
},
updateTimeSystem(timeSystem) {
this.resetValues();
this.timestampKey = timeSystem.key;
},
updateViewContext() {
@@ -207,3 +241,4 @@ export default {
}
};
</script>

View File

@@ -46,7 +46,6 @@ describe("The LAD Table", () => {
let openmct;
let ladPlugin;
let historicalProvider;
let parent;
let child;
let telemetryCount = 3;
@@ -82,13 +81,6 @@ describe("The LAD Table", () => {
spyOn(openmct.objects, 'get').and.returnValue(Promise.resolve({}));
historicalProvider = {
request: () => {
return Promise.resolve([]);
}
};
spyOn(openmct.telemetry, 'findRequestProvider').and.returnValue(historicalProvider);
openmct.time.bounds({
start: bounds.start,
end: bounds.end
@@ -155,7 +147,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 (done) => {
beforeEach(async () => {
let telemetryRequestResolve;
let telemetryObjectResolve;
let anotherTelemetryObjectResolve;
@@ -174,12 +166,11 @@ describe("The LAD Table", () => {
callBack();
});
historicalProvider.request = () => {
openmct.telemetry.request.and.callFake(() => {
telemetryRequestResolve(mockTelemetry);
return telemetryRequestPromise;
};
});
openmct.objects.get.and.callFake((obj) => {
if (obj.key === 'telemetry-object') {
telemetryObjectResolve(mockObj.telemetry);
@@ -204,8 +195,6 @@ describe("The LAD Table", () => {
await Promise.all([telemetryRequestPromise, telemetryObjectPromise, anotherTelemetryObjectPromise]);
await Vue.nextTick();
done();
});
it("should show one row per object in the composition", () => {

View File

@@ -40,14 +40,6 @@ export default {
BarGraph
},
inject: ['openmct', 'domainObject', 'path'],
props: {
options: {
type: Object,
default() {
return {};
}
}
},
data() {
this.telemetryObjects = {};
this.telemetryObjectFormats = {};
@@ -255,7 +247,7 @@ export default {
}
});
let trace = {
const trace = {
key,
name: telemetryObject.name,
x: xValues,
@@ -263,18 +255,13 @@ export default {
text: yValues.map(String),
xAxisMetadata: axisMetadata.xAxisMetadata,
yAxisMetadata: axisMetadata.yAxisMetadata,
type: this.options.type ? this.options.type : 'bar',
type: 'bar',
marker: {
color: this.domainObject.configuration.barStyles.series[key].color
},
hoverinfo: 'skip'
};
if (this.options.type) {
trace.mode = 'markers';
trace.hoverinfo = 'x+y';
}
this.addTrace(trace, key);
},
isDataInTimeRange(datum, key) {

View File

@@ -1,57 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { SCATTER_PLOT_KEY } from './scatterPlotConstants';
export default function ScatterPlotCompositionPolicy(openmct) {
function hasRange(metadata) {
const rangeValues = metadata.valuesForHints(['range']).map((value) => {
return value.source;
});
const uniqueRangeValues = new Set(rangeValues);
return uniqueRangeValues && uniqueRangeValues.size > 1;
}
function hasScatterPlotTelemetry(domainObject) {
if (!openmct.telemetry.isTelemetryObject(domainObject)) {
return false;
}
let metadata = openmct.telemetry.getMetadata(domainObject);
return metadata.values().length > 0 && hasRange(metadata);
}
return {
allow: function (parent, child) {
if (parent.type === SCATTER_PLOT_KEY) {
if ((child.type === 'conditionSet') || (!hasScatterPlotTelemetry(child))) {
return false;
}
}
return true;
}
};
}

View File

@@ -1,146 +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.
*****************************************************************************/
<template>
<span class="form-control">
<span
class="field control"
:class="model.cssClass"
>
<div
class="c-form--sub-grid"
>
<div class="c-form__row">
<span
class="req-indicator"
:class="{'req': isRequired}"
>
</span>
<label>Minimum X axis value</label>
<input
ref="domainMin"
v-model.number="domainMin"
data-field-name="domainMin"
type="number"
@input="onChange('domainMin')"
>
</div>
<div class="c-form__row">
<span
class="req-indicator"
:class="{'req': isRequired}"
>
</span>
<label>Maximum X axis value</label>
<input
ref="domainMax"
v-model.number="domainMax"
data-field-name="domainMax"
type="number"
@input="onChange('domainMax')"
>
</div>
<div class="c-form__row">
<span
class="req-indicator"
:class="{'req': isRequired}"
>
</span>
<label>Minimum Y axis value</label>
<input
ref="rangeMin"
v-model.number="rangeMin"
data-field-name="rangeMin"
type="number"
@input="onChange('rangeMin')"
>
</div>
<div class="c-form__row">
<span
class="req-indicator"
:class="{'req': isRequired}"
>
</span>
<label>Maximum Y axis value</label>
<input
ref="rangeMax"
v-model.number="rangeMax"
data-field-name="rangeMax"
type="number"
@input="onChange('rangeMax')"
>
</div>
</div>
</span>
</span>
</template>
<script>
export default {
props: {
model: {
type: Object,
required: true
}
},
data() {
return {
rangeMax: this.model.value.rangeMax,
rangeMin: this.model.value.rangeMin,
domainMax: this.model.value.domainMax,
domainMin: this.model.value.domainMin
};
},
computed: {
isRequired() {
return [this.rangeMax, this.rangeMin, this.domainMin, this.domainMax].some(value => value !== undefined && value !== '');
}
},
methods: {
onChange(property) {
if (this[property] === '') {
this[property] = undefined;
}
const data = {
model: this.model,
value: {
rangeMax: this.rangeMax,
rangeMin: this.rangeMin,
domainMax: this.domainMax,
domainMin: this.domainMin
}
};
if (property) {
this.model.validate(data);
}
this.$emit('onChange', data);
}
}
};
</script>

View File

@@ -1,346 +0,0 @@
<!--
Open MCT, Copyright (c) 2014-2021, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT is licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Open MCT includes source code licensed under additional open source
licenses. See the Open Source Licenses file (LICENSES.md) included with
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<template>
<ScatterPlotWithUnderlay
class="c-plot c-scatter-chart-view"
:data="trace"
:plot-axis-title="plotAxisTitle"
@subscribe="subscribeToAll"
@unsubscribe="removeAllSubscriptions"
/>
</template>
<script>
import ScatterPlotWithUnderlay from './ScatterPlotWithUnderlay.vue';
import _ from 'lodash';
export default {
components: {
ScatterPlotWithUnderlay
},
inject: ['openmct', 'domainObject', 'path'],
data() {
this.telemetryObjects = {};
this.telemetryObjectFormats = {};
this.valuesByTimestamp = {};
this.subscriptions = [];
return {
trace: []
};
},
computed: {
plotAxisTitle() {
const { xAxisMetadata = {}, yAxisMetadata = {} } = this.trace[0] || {};
const xAxisUnit = xAxisMetadata.units ? `(${xAxisMetadata.units})` : '';
const yAxisUnit = yAxisMetadata.units ? `(${yAxisMetadata.units})` : '';
return {
xAxisTitle: `${xAxisMetadata.name || ''} ${xAxisUnit}`,
yAxisTitle: `${yAxisMetadata.name || ''} ${yAxisUnit}`
};
}
},
mounted() {
this.setTimeContext();
this.loadComposition();
this.reloadTelemetry = this.reloadTelemetry.bind(this);
this.reloadTelemetry = _.debounce(this.reloadTelemetry, 500);
this.unobserve = this.openmct.objects.observe(this.domainObject, 'configuration.axes', this.reloadTelemetry);
this.unobserveUnderlayRanges = this.openmct.objects.observe(this.domainObject, 'configuration.ranges', this.reloadTelemetry);
},
beforeDestroy() {
this.stopFollowingTimeContext();
if (!this.composition) {
return;
}
this.removeAllSubscriptions();
this.composition.off('add', this.addToComposition);
this.composition.off('remove', this.removeTelemetryObject);
if (this.unobserve) {
this.unobserve();
}
if (this.unobserveUnderlayRanges) {
this.unobserveUnderlayRanges();
}
},
methods: {
setTimeContext() {
this.stopFollowingTimeContext();
this.timeContext = this.openmct.time.getContextForView(this.path);
this.followTimeContext();
},
followTimeContext() {
this.timeContext.on('bounds', this.reloadTelemetry);
},
stopFollowingTimeContext() {
if (this.timeContext) {
this.timeContext.off('bounds', this.reloadTelemetry);
}
},
addToComposition(telemetryObject) {
if (Object.values(this.telemetryObjects).length > 0) {
this.confirmRemoval(telemetryObject);
} else {
this.addTelemetryObject(telemetryObject);
}
},
removeFromComposition(telemetryObject) {
let composition = this.domainObject.composition.filter(id =>
!this.openmct.objects.areIdsEqual(id, telemetryObject.identifier)
);
this.openmct.objects.mutate(this.domainObject, 'composition', composition);
},
addTelemetryObject(telemetryObject) {
// grab information we need from the added telmetry object
const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);
this.telemetryObjects[key] = telemetryObject;
const metadata = this.openmct.telemetry.getMetadata(telemetryObject);
this.telemetryObjectFormats[key] = this.openmct.telemetry.getFormatMap(metadata);
this.getDataForTelemetry(key);
},
confirmRemoval(telemetryObject) {
const dialog = this.openmct.overlays.dialog({
iconClass: 'alert',
message: 'This action will replace the current telemetry source. Do you want to continue?',
buttons: [
{
label: 'Ok',
emphasis: true,
callback: () => {
const oldTelemetryObject = Object.values(this.telemetryObjects)[0];
this.removeFromComposition(oldTelemetryObject);
this.removeTelemetryObject(oldTelemetryObject.identifier);
this.valuesByTimestamp = {};
this.addTelemetryObject(telemetryObject);
dialog.dismiss();
}
},
{
label: 'Cancel',
callback: () => {
this.removeFromComposition(telemetryObject);
dialog.dismiss();
}
}
]
});
},
getTelemetryProcessor(keyString) {
return (telemetry) => {
//Check that telemetry object has not been removed since telemetry was requested.
const telemetryObject = this.telemetryObjects[keyString];
if (!telemetryObject) {
return;
}
telemetry.forEach(datum => {
this.addDataToGraph(telemetryObject, datum);
});
this.updateTrace(telemetryObject);
};
},
getAxisMetadata(telemetryObject) {
const metadata = this.openmct.telemetry.getMetadata(telemetryObject);
if (!metadata) {
return {};
}
return metadata.valuesForHints(['range']);
},
loadComposition() {
this.composition = this.openmct.composition.get(this.domainObject);
this.composition.on('add', this.addToComposition);
this.composition.on('remove', this.removeTelemetryObject);
this.composition.load();
},
reloadTelemetry() {
this.valuesByTimestamp = {};
Object.keys(this.telemetryObjects).forEach(key => {
this.getDataForTelemetry(key);
});
},
getDataForTelemetry(key) {
const telemetryObject = this.telemetryObjects[key];
if (!telemetryObject) {
return;
}
const telemetryProcessor = this.getTelemetryProcessor(key);
const options = this.getOptions();
this.openmct.telemetry.request(telemetryObject, options).then(telemetryProcessor);
this.subscribeToObject(telemetryObject);
},
removeTelemetryObject(identifier) {
const key = this.openmct.objects.makeKeyString(identifier);
if (this.telemetryObjects[key]) {
delete this.telemetryObjects[key];
}
if (this.telemetryObjectFormats && this.telemetryObjectFormats[key]) {
delete this.telemetryObjectFormats[key];
}
this.removeSubscription(key);
},
addDataToGraph(telemetryObject, data) {
const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);
if (data.message) {
this.openmct.notifications.alert(data.message);
}
if (!this.domainObject.configuration.axes.xKey || !this.domainObject.configuration.axes.yKey) {
return;
}
const timestamp = this.getTimestampForDatum(data, key, telemetryObject);
let valueForTimestamp = this.valuesByTimestamp[timestamp] || {};
//populate x values
let metadataKey = this.domainObject.configuration.axes.xKey;
if (data[metadataKey] !== undefined) {
valueForTimestamp.x = this.format(key, metadataKey, data);
}
metadataKey = this.domainObject.configuration.axes.yKey;
if (data[metadataKey] !== undefined) {
valueForTimestamp.y = this.format(key, metadataKey, data);
}
this.valuesByTimestamp[timestamp] = valueForTimestamp;
},
updateTrace(telemetryObject) {
const xAndyValues = Object.values(this.valuesByTimestamp);
const xValues = xAndyValues.map(value => value.x);
const yValues = xAndyValues.map(value => value.y);
const axisMetadata = this.getAxisMetadata(telemetryObject);
const xAxisMetadata = axisMetadata.find(metadata => metadata.source === this.domainObject.configuration.axes.xKey);
let yAxisMetadata = {};
if (this.domainObject.configuration.axes.yKey) {
yAxisMetadata = axisMetadata.find(metadata => metadata.source === this.domainObject.configuration.axes.yKey);
}
let trace = {
key: this.openmct.objects.makeKeyString(this.domainObject.identifier),
name: this.domainObject.name,
x: xValues,
y: yValues,
text: yValues.map(String),
xAxisMetadata: xAxisMetadata,
yAxisMetadata: yAxisMetadata,
type: 'scatter',
mode: 'markers',
marker: {
color: this.domainObject.configuration.styles.color
},
hoverinfo: 'x+y'
};
if (this.domainObject.configuration.ranges !== undefined && this.domainObject.configuration.ranges.domainMin !== undefined && this.domainObject.configuration.ranges.domainMax !== undefined) {
trace.xaxis = {
min: this.domainObject.configuration.ranges.domainMin,
max: this.domainObject.configuration.ranges.domainMax
};
}
if (this.domainObject.configuration.ranges !== undefined && this.domainObject.configuration.ranges.rangeMin !== undefined && this.domainObject.configuration.ranges.rangeMax !== undefined) {
trace.yaxis = {
min: this.domainObject.configuration.ranges.rangeMin,
max: this.domainObject.configuration.ranges.rangeMax
};
}
this.trace = [trace];
},
getTimestampForDatum(datum, key, telemetryObject) {
const timeSystemKey = this.timeContext.timeSystem().key;
const metadata = this.openmct.telemetry.getMetadata(telemetryObject);
let metadataValue = metadata.value(timeSystemKey) || { format: timeSystemKey };
return this.parse(key, metadataValue.source, datum);
},
format(telemetryObjectKey, metadataKey, data) {
const formats = this.telemetryObjectFormats[telemetryObjectKey];
return formats[metadataKey].format(data);
},
parse(telemetryObjectKey, metadataKey, datum) {
if (!datum) {
return;
}
const formats = this.telemetryObjectFormats[telemetryObjectKey];
return formats[metadataKey].parse(datum);
},
getOptions() {
const { start, end } = this.timeContext.bounds();
return {
end,
start
};
},
subscribeToObject(telemetryObject) {
const key = this.openmct.objects.makeKeyString(telemetryObject.identifier);
this.removeSubscription(key);
const options = this.getOptions();
const unsubscribe = this.openmct.telemetry.subscribe(telemetryObject,
data => this.addDataToGraph(telemetryObject, data)
, options);
this.subscriptions.push({
key,
unsubscribe
});
},
subscribeToAll() {
const telemetryObjects = Object.values(this.telemetryObjects);
telemetryObjects.forEach(this.subscribeToObject);
},
removeAllSubscriptions() {
this.subscriptions.forEach(subscription => subscription.unsubscribe());
this.subscriptions = [];
},
removeSubscription(key) {
const found = this.subscriptions.findIndex(subscription => subscription.key === key);
if (found > -1) {
this.subscriptions[found].unsubscribe();
this.subscriptions.splice(found, 1);
}
}
}
};
</script>

View File

@@ -1,79 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2021, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import ScatterPlotView from './ScatterPlotView.vue';
import { SCATTER_PLOT_KEY, SCATTER_PLOT_VIEW, TIME_STRIP_KEY } from './scatterPlotConstants.js';
import Vue from 'vue';
export default function ScatterPlotViewProvider(openmct) {
function isCompactView(objectPath) {
let isChildOfTimeStrip = objectPath.find(object => object.type === TIME_STRIP_KEY);
return isChildOfTimeStrip && !openmct.router.isNavigatedObject(objectPath);
}
return {
key: SCATTER_PLOT_VIEW,
name: 'Scatter Plot',
cssClass: 'icon-telemetry',
canView(domainObject, objectPath) {
return domainObject && domainObject.type === SCATTER_PLOT_KEY;
},
canEdit(domainObject, objectPath) {
return domainObject && domainObject.type === SCATTER_PLOT_KEY;
},
view: function (domainObject, objectPath) {
let component;
return {
show: function (element) {
let isCompact = isCompactView(objectPath);
component = new Vue({
el: element,
components: {
ScatterPlotView
},
provide: {
openmct,
domainObject,
path: objectPath
},
data() {
return {
options: {
compact: isCompact
}
};
},
template: '<scatter-plot-view :options="options"></scatter-plot-view>'
});
},
destroy: function () {
component.$destroy();
component = undefined;
}
};
}
};
}

View File

@@ -1,393 +0,0 @@
<template>
<div
ref="plotWrapper"
class="has-local-controls"
:class="{ 's-unsynced' : isZoomed }"
>
<div
v-if="isZoomed"
class="l-state-indicators"
>
<span
class="l-state-indicators__alert-no-lad t-object-alert t-alert-unsynced icon-alert-triangle"
title="This plot is not currently displaying the latest data. Reset pan/zoom to view latest data."
></span>
</div>
<div
ref="plot"
class="c-scatter-chart"
></div>
<div
ref="localControl"
class="gl-plot__local-controls h-local-controls h-local-controls--overlay-content c-local-controls--show-on-hover"
>
<button
v-if="data.length"
class="c-button icon-reset"
:disabled="!isZoomed"
title="Reset pan/zoom"
@click="reset()"
>
</button>
</div>
</div>
</template>
<script>
import Plotly from 'plotly-basic';
const MULTI_AXES_X_PADDING_PERCENT = {
LEFT: 8,
RIGHT: 94
};
import { getValidatedData } from "@/plugins/plan/util";
const PATH_COLORS = ['blue', 'red', 'green'];
const MARKER_COLOR = 'white';
export default {
inject: ['openmct', 'domainObject'],
props: {
data: {
type: Array,
default() {
return [];
}
},
plotAxisTitle: {
type: Object,
default() {
return {};
}
}
},
data() {
return {
isZoomed: false,
yAxisRange: {
min: '',
max: ''
},
xAxisRange: {
min: '',
max: ''
}
};
},
watch: {
data: {
immediate: false,
handler: 'updateData'
}
},
mounted() {
this.getUnderlayPlotData();
Plotly.newPlot(this.$refs.plot, Array.from(this.data.concat(this.getShapes(this.shapesData))), this.getLayout(), {
responsive: true,
displayModeBar: false
});
this.registerListeners();
this.$refs.plot.on('plotly_relayout', this.zoom);
},
beforeDestroy() {
if (this.$refs.plot && this.$refs.plot.off) {
this.$refs.plot.off('plotly_relayout', this.zoom);
}
if (this.plotResizeObserver) {
this.plotResizeObserver.unobserve(this.$refs.plotWrapper);
clearTimeout(this.resizeTimer);
}
if (this.unlistenUnderlay) {
this.unlistenUnderlay();
}
if (this.unlistenUnderlayRanges) {
this.unlistenUnderlayRanges();
}
if (this.unobserveColorChanges) {
this.unobserveColorChanges();
}
},
methods: {
getUnderlayPlotData() {
if (this.domainObject.selectFile) {
this.shapesData = getValidatedData(this.domainObject);
} else {
this.shapesData = [];
}
},
observeForUnderlayPlotChanges() {
this.getUnderlayPlotData();
this.updateData();
},
getAxisMinMax() {
if (!this.data.length) {
return;
}
// For now, use x and y axes min, max values only if an underlay is available
if (this.shapesData.length && this.data[0].xaxis) {
this.xAxisRange = this.data[0].xaxis;
}
if (this.shapesData.length && this.data[0].yaxis) {
this.yAxisRange = this.data[0].yaxis;
}
},
getLayout() {
this.getAxisMinMax();
const yAxesMeta = this.getYAxisMeta();
const primaryYaxis = this.getYaxisLayout(yAxesMeta['1']);
const xAxisDomain = this.getXAxisDomain(yAxesMeta);
const shapes = this.shapesData.map((shapeData, index) => {
if (!shapeData.x || !shapeData.y
|| !shapeData.x.length || !shapeData.y.length
|| shapeData.x.length !== shapeData.y.length) {
return "";
}
let path = `M ${shapeData.x[0]},${shapeData.y[0]}`;
shapeData.x.forEach((point, shapeIndex) => {
if (shapeIndex > 0) {
path = `${path} L${point},${shapeData.y[shapeIndex]}`;
}
});
return {
path,
type: 'path',
line: {
color: PATH_COLORS[index]
},
opacity: 0.5
};
});
return {
autosize: true,
showlegend: false,
textposition: 'auto',
font: {
family: 'Helvetica Neue, Helvetica, Arial, sans-serif',
size: '12px',
color: '#666'
},
xaxis: {
domain: xAxisDomain,
range: [this.xAxisRange.min, this.xAxisRange.max],
title: this.plotAxisTitle.xAxisTitle,
automargin: true
},
yaxis: primaryYaxis,
margin: {
l: 5,
r: 5,
t: 5,
b: 0
},
paper_bgcolor: 'transparent',
plot_bgcolor: 'transparent',
shapes,
layer: 'below'
};
},
getYAxisMeta() {
const yAxisMeta = {};
this.data.forEach(datum => {
const yAxisMetadata = datum.yAxisMetadata;
const range = '1';
const side = 'left';
const name = yAxisMetadata.name;
const unit = yAxisMetadata.units;
yAxisMeta[range] = {
range,
side,
name,
unit
};
});
return yAxisMeta;
},
getXAxisDomain(yAxisMeta) {
let leftPaddingPerc = 0;
let rightPaddingPerc = 100;
let rightSide = yAxisMeta && Object.values(yAxisMeta).filter((axisMeta => axisMeta.side === 'right'));
let leftSide = yAxisMeta && Object.values(yAxisMeta).filter((axisMeta => axisMeta.side === 'left'));
if (yAxisMeta && rightSide.length > 1) {
rightPaddingPerc = MULTI_AXES_X_PADDING_PERCENT.RIGHT;
}
if (yAxisMeta && leftSide.length > 1) {
leftPaddingPerc = MULTI_AXES_X_PADDING_PERCENT.LEFT;
}
return [leftPaddingPerc / 100, rightPaddingPerc / 100];
},
getYaxisLayout(yAxisMeta) {
if (!yAxisMeta) {
return {};
}
const { name, range, side = 'left', unit } = yAxisMeta;
const title = `${name} ${unit ? '(' + unit + ')' : ''}`;
const yaxis = {
automargin: true,
title
};
let yRange = this.yAxisRange;
if (range === '1') {
yaxis.range = [yRange.min, yRange.max];
return yaxis;
}
yaxis.range = [yRange.min, yRange.max];
yaxis.anchor = side.toLowerCase() === 'left'
? 'free'
: 'x';
yaxis.showline = side.toLowerCase() === 'left';
yaxis.side = side.toLowerCase();
yaxis.overlaying = 'y';
yaxis.position = 0.01;
return yaxis;
},
registerListeners() {
this.unobserveColorChanges = this.openmct.objects.observe(this.domainObject, 'configuration.styles.color', this.updateColors);
this.unlistenUnderlay = this.openmct.objects.observe(this.domainObject, 'selectFile', this.observeForUnderlayPlotChanges);
this.unlistenUnderlayRanges = this.openmct.objects.observe(this.domainObject, 'configuration.ranges', this.updateData);
this.resizeTimer = false;
if (window.ResizeObserver) {
this.plotResizeObserver = new ResizeObserver(() => {
// debounce and trigger window resize so that plotly can resize the plot
clearTimeout(this.resizeTimer);
this.resizeTimer = setTimeout(() => {
window.dispatchEvent(new Event('resize'));
}, 250);
});
this.plotResizeObserver.observe(this.$refs.plotWrapper);
}
},
updateColors() {
const colors = [];
const indices = [];
this.data.forEach((item, index) => {
const colorExists = this.domainObject.configuration.styles.color;
indices.push(index);
if (colorExists) {
colors.push(this.domainObject.configuration.styles.color);
} else {
colors.push(item.marker.color);
}
});
const plotUpdate = {
'marker.color': colors
};
Plotly.restyle(this.$refs.plot, plotUpdate, indices);
},
reset() {
this.isZoomed = false;
this.updatePlot();
this.$emit('subscribe');
},
updateData() {
this.updatePlot();
},
updateLocalControlPosition() {
const localControl = this.$refs.localControl;
localControl.style.display = 'none';
const plot = this.$refs.plot;
const bgLayer = this.$el.querySelector('.bglayer');
const plotBoundingRect = plot.getBoundingClientRect();
const bgLayerBoundingRect = bgLayer.getBoundingClientRect();
const top = bgLayerBoundingRect.top - plotBoundingRect.top + 5;
const left = bgLayerBoundingRect.left - plotBoundingRect.left + 5;
localControl.style.top = `${top}px`;
localControl.style.left = `${left}px`;
localControl.style.display = 'block';
},
updatePlot() {
if (!this.$refs || !this.$refs.plot || this.isZoomed) {
return;
}
Plotly.react(this.$refs.plot, Array.from(this.data.concat(this.getShapes(this.shapesData))), this.getLayout());
},
zoom(eventData) {
const autorange = eventData['xaxis.autorange'];
const { autosize } = eventData;
if (autosize || autorange) {
return;
}
this.isZoomed = true;
this.$emit('unsubscribe');
},
getShapes() {
let markerData = {
x: [],
y: []
};
const shapes = this.shapesData.map((shapeData, index) => {
if (!shapeData.x || !shapeData.y
|| !shapeData.x.length || !shapeData.y.length
|| shapeData.x.length !== shapeData.y.length) {
return "";
}
let text = [];
shapeData.x.forEach((point) => {
text.push(`${parseFloat(point).toPrecision(2)}`);
});
markerData.x = markerData.x.concat(shapeData.x);
markerData.y = markerData.y.concat(shapeData.y);
return {
x: shapeData.x,
y: shapeData.y,
mode: 'text',
text,
textfont: {
family: 'Helvetica Neue, Helvetica, Arial, sans-serif',
size: '12px',
color: PATH_COLORS[index]
},
opacity: 0.5
};
});
shapes.push({
x: markerData.x,
y: markerData.y,
mode: "markers",
marker: {
size: 6,
color: MARKER_COLOR
}
});
return shapes;
}
}
};
</script>

View File

@@ -1,64 +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.
-->
<template>
<div>
<div v-if="canEdit">
<plot-options-edit />
</div>
<div v-else>
<plot-options-browse />
</div>
</div>
</template>
<script>
import PlotOptionsBrowse from "./PlotOptionsBrowse.vue";
import PlotOptionsEdit from "./PlotOptionsEdit.vue";
export default {
components: {
PlotOptionsBrowse,
PlotOptionsEdit
},
inject: ['openmct', 'domainObject'],
data() {
return {
isEditing: this.openmct.editor.isEditing()
};
},
computed: {
canEdit() {
return this.isEditing && !this.domainObject.locked;
}
},
mounted() {
this.openmct.editor.on('isEditing', this.setEditState);
},
beforeDestroy() {
this.openmct.editor.off('isEditing', this.setEditState);
},
methods: {
setEditState(isEditing) {
this.isEditing = isEditing;
}
}
};
</script>

View File

@@ -1,153 +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.
-->
<template>
<div class="js-plot-options-browse grid-properties">
<ul class="l-inspector-part">
<h2 title="Object view settings">Settings</h2>
<li class="grid-row">
<div
class="grid-cell label"
title="X axis selection"
>X Axis</div>
<div class="grid-cell value">{{ xKeyLabel }}</div>
</li>
<li class="grid-row">
<div
class="grid-cell label"
title="Y axis selection"
>Y Axis</div>
<div class="grid-cell value">{{ yKeyLabel }}</div>
</li>
<ColorSwatch
:current-color="currentColor"
edit-title="Manually set the color for this plot"
view-title="The marker color for this plot"
short-label="Color"
/>
</ul>
</div>
</template>
<script>
import ColorSwatch from "../../../../ui/color/ColorSwatch.vue";
import Color from "../../../../ui/color/Color";
import ColorPalette from "../../../../ui/color/ColorPalette";
export default {
components: { ColorSwatch },
inject: ['openmct', 'domainObject'],
data() {
return {
xKeyLabel: '',
yKeyLabel: '',
currentColor: undefined
};
},
mounted() {
this.plotSeries = [];
this.colorPalette = new ColorPalette();
this.initColor();
this.composition = this.openmct.composition.get(this.domainObject);
this.registerListeners();
this.composition.load();
},
beforeDestroy() {
this.stopListening();
},
methods: {
initColor() {
// this is called before the plot is initialized
if (!this.domainObject.configuration.styles || !this.domainObject.configuration.styles.color) {
const color = this.colorPalette.getNextColor().asHexString();
this.domainObject.configuration.styles = {
color
};
}
this.currentColor = this.domainObject.configuration.styles.color;
const colorObject = Color.fromHexString(this.currentColor);
this.colorPalette.remove(colorObject);
},
registerListeners() {
this.composition.on('add', this.addSeries);
this.composition.on('remove', this.removeSeries);
this.unobserve = this.openmct.objects.observe(this.domainObject, 'configuration.axes', this.setAxesLabels);
},
stopListening() {
this.composition.off('add', this.addSeries);
this.composition.off('remove', this.removeSeries);
if (this.unobserve) {
this.unobserve();
}
},
addSeries(series, index) {
this.$set(this.plotSeries, this.plotSeries.length, series);
this.setAxesLabels();
},
removeSeries(series) {
const index = this.plotSeries.findIndex(plotSeries => this.openmct.objects.areIdsEqual(series.identifier, plotSeries.identifier));
if (index !== undefined) {
this.$delete(this.plotSeries, index);
this.setAxesLabels();
}
},
setAxesLabels() {
let xKeyOptions = [];
let yKeyOptions = [];
if (this.plotSeries.length <= 0) {
return;
}
const series = this.plotSeries[0];
const metadataValues = this.openmct.telemetry.getMetadata(series).valuesForHints(['range']);
metadataValues.forEach((metadataValue) => {
xKeyOptions.push({
name: metadataValue.name || metadataValue.key,
value: metadataValue.source || metadataValue.key
});
yKeyOptions.push({
name: metadataValue.name || metadataValue.key,
value: metadataValue.source || metadataValue.key
});
});
let xKeyOptionIndex;
let yKeyOptionIndex;
if (this.domainObject.configuration.axes.xKey) {
xKeyOptionIndex = xKeyOptions.findIndex(option => option.value === this.domainObject.configuration.axes.xKey);
if (xKeyOptionIndex > -1) {
this.xKeyLabel = xKeyOptions[xKeyOptionIndex].name;
}
}
if (metadataValues.length > 1 && this.domainObject.configuration.axes.yKey) {
yKeyOptionIndex = yKeyOptions.findIndex(option => option.value === this.domainObject.configuration.axes.yKey);
if (yKeyOptionIndex > -1) {
this.yKeyLabel = yKeyOptions[yKeyOptionIndex].name;
}
}
}
}
};
</script>

View File

@@ -1,262 +0,0 @@
<!--
Open MCT, Copyright (c) 2014-2022, United States Government
as represented by the Administrator of the National Aeronautics and Space
Administration. All rights reserved.
Open MCT is licensed under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations
under the License.
Open MCT includes source code licensed under additional open source
licenses. See the Open Source Licenses file (LICENSES.md) included with
this source code distribution or the Licensing information page available
at runtime from the About dialog for additional information.
-->
<template>
<div class="js-plot-options-edit grid-properties">
<ul class="l-inspector-part">
<h2 title="Object view settings">Settings</h2>
<li class="grid-row">
<div
class="grid-cell label"
title="X axis selection."
>X Axis</div>
<div class="grid-cell value">
<select
v-model="xKey"
@change="updateForm('xKey')"
>
<option
v-for="option in xKeyOptions"
:key="`xKey-${option.value}`"
:value="option.value"
:selected="option.value == xKey"
>
{{ option.name }}
</option>
</select>
</div>
</li>
<li class="grid-row">
<div
class="grid-cell label"
title="Y axis selection."
>Y Axis</div>
<div class="grid-cell value">
<select
v-model="yKey"
@change="updateForm('yKey')"
>
<option
v-for="option in yKeyOptions"
:key="`yKey-${option.value}`"
:value="option.value"
:selected="option.value == yKey"
>
{{ option.name }}
</option>
</select>
</div>
</li>
<ColorSwatch
:current-color="currentColor"
title="Manually set the line and marker color for this plot."
edit-title="Manually set the line and marker color for this plot."
view-title="The line and marker color for this plot."
short-label="Color"
@colorSet="setColor"
/>
</ul>
</div>
</template>
<script>
import Color from "../../../../ui/color/Color";
import ColorPalette from "../../../../ui/color/ColorPalette";
import ColorSwatch from "../../../../ui/color/ColorSwatch.vue";
export default {
components: { ColorSwatch },
inject: ['openmct', 'domainObject'],
data() {
return {
xKey: undefined,
yKey: undefined,
xKeyOptions: [],
yKeyOptions: [],
currentColor: undefined
};
},
mounted() {
this.plotSeries = [];
this.colorPalette = new ColorPalette();
this.initColor();
this.composition = this.openmct.composition.get(this.domainObject);
this.registerListeners();
this.composition.load();
},
beforeDestroy() {
this.stopListening();
},
methods: {
initColor() {
// this is called before the plot is initialized
if (!this.domainObject.configuration.styles || !this.domainObject.configuration.styles.color) {
const color = this.colorPalette.getNextColor().asHexString();
this.domainObject.configuration.styles = {
color
};
}
this.currentColor = this.domainObject.configuration.styles.color;
const colorObject = Color.fromHexString(this.currentColor);
this.colorPalette.remove(colorObject);
},
setColor(chosenColor) {
this.currentColor = chosenColor.asHexString();
this.openmct.objects.mutate(
this.domainObject,
`configuration.styles.color`,
this.currentColor
);
},
registerListeners() {
this.composition.on('add', this.addSeries);
this.composition.on('remove', this.removeSeries);
this.unobserve = this.openmct.objects.observe(this.domainObject, 'configuration.axes', this.setupOptions);
},
stopListening() {
this.composition.off('add', this.addSeries);
this.composition.off('remove', this.removeSeries);
if (this.unobserve) {
this.unobserve();
}
},
addSeries(series, index) {
this.$set(this.plotSeries, this.plotSeries.length, series);
this.setupOptions();
},
removeSeries(seriesIdentifier) {
const index = this.plotSeries.findIndex(plotSeries => this.openmct.objects.areIdsEqual(seriesIdentifier, plotSeries.identifier));
if (index >= 0) {
this.$delete(this.plotSeries, index);
this.setupOptions();
}
},
setupOptions() {
this.xKeyOptions = [];
this.yKeyOptions = [];
if (this.plotSeries.length <= 0) {
return;
}
let update = false;
const series = this.plotSeries[0];
const metadataValues = this.openmct.telemetry.getMetadata(series).valuesForHints(['range']);
metadataValues.forEach((metadataValue) => {
this.xKeyOptions.push({
name: metadataValue.name || metadataValue.key,
value: metadataValue.source || metadataValue.key
});
this.yKeyOptions.push({
name: metadataValue.name || metadataValue.key,
value: metadataValue.source || metadataValue.key
});
});
let xKeyOptionIndex;
let yKeyOptionIndex;
if (this.domainObject.configuration.axes.xKey) {
xKeyOptionIndex = this.xKeyOptions.findIndex(option => option.value === this.domainObject.configuration.axes.xKey);
if (xKeyOptionIndex > -1) {
this.xKey = this.xKeyOptions[xKeyOptionIndex].value;
} else {
this.xKey = undefined;
}
}
if (this.xKey === undefined) {
update = true;
xKeyOptionIndex = 0;
this.xKey = this.xKeyOptions[xKeyOptionIndex].value;
}
if (metadataValues.length > 1) {
if (this.domainObject.configuration.axes.yKey) {
yKeyOptionIndex = this.yKeyOptions.findIndex(option => option.value === this.domainObject.configuration.axes.yKey);
if (yKeyOptionIndex > -1 && yKeyOptionIndex !== xKeyOptionIndex) {
this.yKey = this.yKeyOptions[yKeyOptionIndex].value;
} else {
this.yKey = undefined;
}
}
if (this.yKey === undefined) {
update = true;
yKeyOptionIndex = this.yKeyOptions.findIndex((option, index) => index !== xKeyOptionIndex);
this.yKey = this.yKeyOptions[yKeyOptionIndex].value;
}
this.yKeyOptions = this.yKeyOptions.map((option, index) => {
if (index === xKeyOptionIndex) {
option.name = `${option.name} (swap)`;
option.swap = yKeyOptionIndex;
} else {
option.name = option.name.replace(' (swap)', '');
option.swap = undefined;
}
return option;
});
}
this.xKeyOptions = this.xKeyOptions.map((option, index) => {
if (index === yKeyOptionIndex) {
option.name = `${option.name} (swap)`;
option.swap = xKeyOptionIndex;
} else {
option.name = option.name.replace(' (swap)', '');
option.swap = undefined;
}
return option;
});
if (update === true) {
this.saveConfiguration();
}
},
updateForm(property) {
if (property === 'xKey') {
const xKeyOption = this.xKeyOptions.find(option => option.value === this.xKey);
if (xKeyOption.swap !== undefined) {
//swap
this.yKey = this.xKeyOptions[xKeyOption.swap].value;
}
} else if (property === 'yKey') {
const yKeyOption = this.yKeyOptions.find(option => option.value === this.yKey);
if (yKeyOption.swap !== undefined) {
//swap
this.xKey = this.yKeyOptions[yKeyOption.swap].value;
}
}
this.saveConfiguration();
},
saveConfiguration() {
this.openmct.objects.mutate(this.domainObject, `configuration.axes`, {
xKey: this.xKey,
yKey: this.yKey
});
}
}
};
</script>

View File

@@ -1,48 +0,0 @@
import { SCATTER_PLOT_INSPECTOR_KEY, SCATTER_PLOT_KEY } from '../scatterPlotConstants';
import Vue from 'vue';
import PlotOptions from "./PlotOptions.vue";
export default function ScatterPlotInspectorViewProvider(openmct) {
return {
key: SCATTER_PLOT_INSPECTOR_KEY,
name: 'Bar Graph Inspector View',
canView: function (selection) {
if (selection.length === 0 || selection[0].length === 0) {
return false;
}
let object = selection[0][0].context.item;
return object
&& object.type === SCATTER_PLOT_KEY;
},
view: function (selection) {
let component;
return {
show: function (element) {
component = new Vue({
el: element,
components: {
PlotOptions
},
provide: {
openmct,
domainObject: selection[0][0].context.item
},
template: '<plot-options></plot-options>'
});
},
destroy: function () {
if (component) {
component.$destroy();
component = undefined;
}
}
};
},
priority: function () {
return 1;
}
};
}

View File

@@ -1,127 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { SCATTER_PLOT_KEY } from './scatterPlotConstants.js';
import ScatterPlotViewProvider from './ScatterPlotViewProvider';
import ScatterPlotInspectorViewProvider from './inspector/ScatterPlotInspectorViewProvider';
import ScatterPlotCompositionPolicy from './ScatterPlotCompositionPolicy';
import Vue from "vue";
import ScatterPlotForm from "./ScatterPlotForm.vue";
export default function () {
return function install(openmct) {
openmct.forms.addNewFormControl('scatter-plot-form-control', getScatterPlotFormControl(openmct));
openmct.types.addType(SCATTER_PLOT_KEY, {
key: SCATTER_PLOT_KEY,
name: "Scatter Plot",
cssClass: "icon-plot-scatter",
description: "View data as a scatter plot.",
creatable: true,
initialize: function (domainObject) {
domainObject.composition = [];
domainObject.configuration = {
styles: {},
axes: {},
ranges: {}
};
},
form: [
{
name: 'Underlay data (JSON file)',
key: 'selectFile',
control: 'file-input',
text: 'Select File...',
type: 'application/json',
removable: true,
hideFromInspector: true,
property: [
"selectFile"
]
},
{
name: "Underlay ranges",
control: "scatter-plot-form-control",
cssClass: "l-input",
key: "scatterPlotForm",
required: false,
hideFromInspector: false,
property: [
"configuration",
"ranges"
],
validate: ({ value }, callback) => {
const { rangeMin, rangeMax, domainMin, domainMax } = value;
const valid = {
rangeMin,
rangeMax,
domainMin,
domainMax
};
if (callback) {
callback(valid);
}
const values = Object.values(valid);
const hasAllValues = values.every(rangeValue => rangeValue !== undefined);
const hasNoValues = values.every(rangeValue => rangeValue === undefined);
return hasAllValues || hasNoValues;
}
}
],
priority: 891
});
openmct.objectViews.addProvider(new ScatterPlotViewProvider(openmct));
openmct.inspectorViews.addProvider(new ScatterPlotInspectorViewProvider(openmct));
openmct.composition.addPolicy(new ScatterPlotCompositionPolicy(openmct).allow);
};
function getScatterPlotFormControl(openmct) {
return {
show(element, model, onChange) {
const rowComponent = new Vue({
el: element,
components: {
ScatterPlotForm
},
provide: {
openmct
},
data() {
return {
model,
onChange
};
},
template: `<scatter-plot-form :model="model" @onChange="onChange"></scatter-plot-form>`
});
return rowComponent;
}
};
}
}

View File

@@ -1,421 +0,0 @@
/*****************************************************************************
* Open MCT, Copyright (c) 2014-2022, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* Open MCT is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* Open MCT includes source code licensed under additional open source
* licenses. See the Open Source Licenses file (LICENSES.md) included with
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
import {createOpenMct, resetApplicationState} from "utils/testing";
import Vue from "vue";
import ScatterPlotPlugin from "./plugin";
import ScatterPlot from './ScatterPlotView.vue';
import EventEmitter from "EventEmitter";
import { SCATTER_PLOT_VIEW, SCATTER_PLOT_KEY } from './scatterPlotConstants';
describe("the plugin", function () {
let element;
let child;
let openmct;
let telemetryPromise;
let telemetryPromiseResolve;
let mockObjectPath;
beforeEach((done) => {
mockObjectPath = [
{
name: 'mock folder',
type: 'fake-folder',
identifier: {
key: 'mock-folder',
namespace: ''
}
}
];
const testTelemetry = [
{
'utc': 1,
'some-key': 'some-value 1',
'some-other-key': 'some-other-value 1'
},
{
'utc': 2,
'some-key': 'some-value 2',
'some-other-key': 'some-other-value 2'
},
{
'utc': 3,
'some-key': 'some-value 3',
'some-other-key': 'some-other-value 3'
}
];
openmct = createOpenMct();
telemetryPromise = new Promise((resolve) => {
telemetryPromiseResolve = resolve;
});
spyOn(openmct.telemetry, 'request').and.callFake(() => {
telemetryPromiseResolve(testTelemetry);
return telemetryPromise;
});
openmct.install(new ScatterPlotPlugin());
element = document.createElement("div");
element.style.width = "640px";
element.style.height = "480px";
child = document.createElement("div");
child.style.width = "640px";
child.style.height = "480px";
element.appendChild(child);
document.body.appendChild(element);
spyOn(window, 'ResizeObserver').and.returnValue({
observe() {},
unobserve() {},
disconnect() {}
});
openmct.time.timeSystem("utc", {
start: 0,
end: 4
});
openmct.types.addType("test-object", {
creatable: true
});
openmct.on("start", done);
openmct.startHeadless();
});
afterEach((done) => {
openmct.time.timeSystem('utc', {
start: 0,
end: 1
});
resetApplicationState(openmct).then(done).catch(done);
});
describe("The scatter plot view", () => {
let testDomainObject;
let scatterPlotObject;
// eslint-disable-next-line no-unused-vars
let component;
let mockComposition;
beforeEach(async () => {
scatterPlotObject = {
identifier: {
namespace: "",
key: "test-plot"
},
type: "telemetry.plot.scatter-plot",
name: "Test Scatter Plot",
configuration: {
axes: {},
styles: {}
}
};
testDomainObject = {
identifier: {
namespace: "",
key: "test-object"
},
type: "test-object",
name: "Test Object",
telemetry: {
values: [{
key: "utc",
format: "utc",
name: "Time",
hints: {
domain: 1
}
}, {
key: "some-key",
name: "Some attribute",
hints: {
range: 1
}
}, {
key: "some-other-key",
name: "Another attribute",
hints: {
range: 2
}
}]
}
};
mockComposition = new EventEmitter();
mockComposition.load = () => {
mockComposition.emit('add', testDomainObject);
return [testDomainObject];
};
spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
let viewContainer = document.createElement("div");
child.append(viewContainer);
component = new Vue({
el: viewContainer,
components: {
ScatterPlot
},
provide: {
openmct: openmct,
domainObject: scatterPlotObject,
composition: openmct.composition.get(scatterPlotObject)
},
template: "<ScatterPlot></ScatterPlot>"
});
await Vue.nextTick();
});
it("provides a scatter plot view", () => {
const applicableViews = openmct.objectViews.get(scatterPlotObject, mockObjectPath);
const plotViewProvider = applicableViews.find((viewProvider) => viewProvider.key === SCATTER_PLOT_VIEW);
expect(plotViewProvider).toBeDefined();
});
it("Renders plotly scatter plot", () => {
let scatterPlotElement = element.querySelectorAll(".plotly");
expect(scatterPlotElement.length).toBe(1);
});
});
describe("the scatter plot objects", () => {
const mockObject = {
name: 'A very nice scatter plot',
key: SCATTER_PLOT_KEY,
creatable: true
};
it('defines a scatter plot object type with the correct key', () => {
const objectDef = openmct.types.get(SCATTER_PLOT_KEY).definition;
expect(objectDef.key).toEqual(mockObject.key);
});
it('is creatable', () => {
const objectDef = openmct.types.get(SCATTER_PLOT_KEY).definition;
expect(objectDef.creatable).toEqual(mockObject.creatable);
});
});
describe("The scatter plot composition policy", () => {
it("allows composition for telemetry that contain at least 2 ranges", () => {
const parent = {
"composition": [],
"configuration": {
axes: {},
styles: {}
},
"name": "Some Scatter Plot",
"type": "telemetry.plot.scatter-plot",
"location": "mine",
"modified": 1631005183584,
"persisted": 1631005183502,
"identifier": {
"namespace": "",
"key": "b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9"
}
};
const testTelemetryObject = {
identifier: {
namespace: "",
key: "test-object"
},
type: "test-object",
name: "Test Object",
telemetry: {
values: [{
key: "some-key",
name: "Some attribute",
hints: {
domain: 1
}
}, {
key: "some-other-key",
name: "Another attribute",
hints: {
range: 1
}
}, {
key: "some-other-key2",
name: "Another attribute2",
hints: {
range: 2
}
}]
}
};
const composition = openmct.composition.get(parent);
expect(() => {
composition.add(testTelemetryObject);
}).not.toThrow();
expect(parent.composition.length).toBe(1);
});
it("disallows composition for telemetry that don't contain at least 2 range hints", () => {
const parent = {
"composition": [],
"configuration": {
axes: {},
styles: {}
},
"name": "Some Scatter Plot",
"type": "telemetry.plot.scatter-plot",
"location": "mine",
"modified": 1631005183584,
"persisted": 1631005183502,
"identifier": {
"namespace": "",
"key": "b78e7e23-f2b8-4776-b1f0-3ff778f5c8a9"
}
};
const testTelemetryObject = {
identifier: {
namespace: "",
key: "test-object"
},
type: "test-object",
name: "Test Object",
telemetry: {
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);
expect(() => {
composition.add(testTelemetryObject);
}).toThrow();
expect(parent.composition.length).toBe(0);
});
});
describe('the inspector view', () => {
let mockComposition;
let testDomainObject;
let selection;
let plotInspectorView;
let viewContainer;
let optionsElement;
beforeEach(async () => {
testDomainObject = {
identifier: {
namespace: "",
key: "test-object"
},
type: "test-object",
name: "Test Object",
telemetry: {
values: [{
key: "utc",
format: "utc",
name: "Time",
hints: {
domain: 1
}
}, {
key: "some-key",
name: "Some attribute",
hints: {
range: 1
}
}, {
key: "some-other-key",
name: "Another attribute",
hints: {
range: 2
}
}]
}
};
selection = [
[
{
context: {
item: {
id: "test-object",
identifier: {
key: "test-object",
namespace: ''
},
type: "telemetry.plot.scatter-plot",
configuration: {
axes: {},
styles: {
}
},
composition: [
{
key: '~Some~foo.scatter'
}
]
}
}
}
]
];
mockComposition = new EventEmitter();
mockComposition.load = () => {
mockComposition.emit('add', testDomainObject);
return [testDomainObject];
};
spyOn(openmct.composition, 'get').and.returnValue(mockComposition);
viewContainer = document.createElement('div');
child.append(viewContainer);
const applicableViews = openmct.inspectorViews.get(selection);
plotInspectorView = applicableViews[0];
plotInspectorView.show(viewContainer);
await Vue.nextTick();
optionsElement = element.querySelector('.c-scatter-plot-options');
});
afterEach(() => {
plotInspectorView.destroy();
});
it('it renders the options', () => {
expect(optionsElement).toBeDefined();
});
});
});

View File

@@ -1,4 +0,0 @@
export const SCATTER_PLOT_VIEW = 'scatter-plot.view';
export const SCATTER_PLOT_KEY = 'telemetry.plot.scatter-plot';
export const SCATTER_PLOT_INSPECTOR_KEY = 'telemetry.plot.scatter-plot.inspector';
export const TIME_STRIP_KEY = 'time-strip';

View File

@@ -21,7 +21,7 @@
*****************************************************************************/
import EventEmitter from 'EventEmitter';
import { v4 as uuid } from 'uuid';
import uuid from 'uuid';
import TelemetryCriterion from "./criterion/TelemetryCriterion";
import { evaluateResults } from './utils/evaluator';
import { getLatestTimestamp } from './utils/time';

View File

@@ -22,7 +22,7 @@
import Condition from "./Condition";
import { getLatestTimestamp } from './utils/time';
import { v4 as uuid } from 'uuid';
import uuid from "uuid";
import EventEmitter from 'EventEmitter';
export default class ConditionManager extends EventEmitter {

View File

@@ -214,7 +214,7 @@
import Criterion from './Criterion.vue';
import ConditionDescription from "./ConditionDescription.vue";
import { TRIGGER, TRIGGER_LABEL } from "@/plugins/condition/utils/constants";
import { v4 as uuid } from 'uuid';
import uuid from 'uuid';
export default {
components: {

View File

@@ -23,7 +23,7 @@ import ConditionSetViewProvider from './ConditionSetViewProvider.js';
import ConditionSetCompositionPolicy from "./ConditionSetCompositionPolicy";
import ConditionSetMetadataProvider from './ConditionSetMetadataProvider';
import ConditionSetTelemetryProvider from './ConditionSetTelemetryProvider';
import { v4 as uuid } from 'uuid';
import uuid from "uuid";
export default function ConditionPlugin() {

View File

@@ -27,7 +27,7 @@
:href="url"
>
<div class="c-condition-widget__label">
{{ label }}
{{ internalDomainObject.conditionalLabel || internalDomainObject.label }}
</div>
</component>
</template>
@@ -39,112 +39,28 @@ export default {
inject: ['openmct', 'domainObject'],
data: function () {
return {
conditionalLabel: '',
conditionSetIdentifier: null,
domainObjectLabel: '',
url: null,
urlDefined: false,
useConditionSetOutputAsLabel: false
internalDomainObject: this.domainObject
};
},
computed: {
label() {
return this.useConditionSetOutputAsLabel
? this.conditionalLabel
: this.domainObjectLabel
;
}
},
watch: {
conditionSetIdentifier: {
handler(newValue, oldValue) {
if (!oldValue || !newValue || !this.openmct.objects.areIdsEqual(newValue, oldValue)) {
return;
}
this.listenToConditionSetChanges();
},
deep: true
urlDefined() {
return this.internalDomainObject.url && this.internalDomainObject.url.length > 0;
},
url() {
return this.urlDefined ? sanitizeUrl(this.internalDomainObject.url) : null;
}
},
mounted() {
this.unlisten = this.openmct.objects.observe(this.domainObject, '*', this.updateDomainObject);
if (this.domainObject) {
this.updateDomainObject(this.domainObject);
this.listenToConditionSetChanges();
}
this.unlisten = this.openmct.objects.observe(this.internalDomainObject, '*', this.updateInternalDomainObject);
},
beforeDestroy() {
this.conditionSetIdentifier = null;
if (this.unlisten) {
this.unlisten();
}
this.stopListeningToConditionSetChanges();
},
methods: {
async listenToConditionSetChanges() {
if (!this.conditionSetIdentifier) {
return;
}
const conditionSetDomainObject = await this.openmct.objects.get(this.conditionSetIdentifier);
this.stopListeningToConditionSetChanges();
if (!conditionSetDomainObject) {
this.openmct.notifications.alert('Unable to find condition set');
}
this.telemetryCollection = this.openmct.telemetry.requestCollection(conditionSetDomainObject, {
size: 1,
strategy: 'latest'
});
this.telemetryCollection.on('add', this.updateConditionLabel, this);
this.telemetryCollection.load();
},
stopListeningToConditionSetChanges() {
if (this.telemetryCollection) {
this.telemetryCollection.off('add', this.updateConditionLabel, this);
this.telemetryCollection.destroy();
this.telemetryCollection = null;
}
},
updateConditionLabel([latestDatum]) {
if (!this.conditionSetIdentifier) {
this.stopListeningToConditionSetChanges();
return;
}
this.conditionalLabel = latestDatum.output || '';
},
updateDomainObject(domainObject) {
if (this.domainObjectLabel !== domainObject.label) {
this.domainObjectLabel = domainObject.label;
}
const urlDefined = domainObject.url && domainObject.url.length > 0;
if (this.urlDefined !== urlDefined) {
this.urlDefined = urlDefined;
}
const url = this.urlDefined ? sanitizeUrl(domainObject.url) : null;
if (this.url !== url) {
this.url = url;
}
const conditionSetIdentifier = domainObject.configuration.objectStyles.conditionSetIdentifier;
if (this.conditionSetIdentifier !== conditionSetIdentifier) {
this.conditionSetIdentifier = conditionSetIdentifier;
}
const useConditionSetOutputAsLabel = this.conditionSetIdentifier && domainObject.configuration.useConditionSetOutputAsLabel;
if (this.useConditionSetOutputAsLabel !== useConditionSetOutputAsLabel) {
this.useConditionSetOutputAsLabel = useConditionSetOutputAsLabel;
}
updateInternalDomainObject(domainObject) {
this.internalDomainObject = domainObject;
}
}
};

View File

@@ -73,7 +73,7 @@
</template>
<script>
import { v4 as uuid } from 'uuid';
import uuid from 'uuid';
import SubobjectView from './SubobjectView.vue';
import TelemetryView from './TelemetryView.vue';
import BoxView from './BoxView.vue';

View File

@@ -222,20 +222,20 @@ export default {
.then(this.setObject);
}
this.openmct.time.on("bounds", this.refreshData);
this.status = this.openmct.status.get(this.item.identifier);
this.removeStatusListener = this.openmct.status.observe(this.item.identifier, this.setStatus);
},
beforeDestroy() {
this.removeSubscription();
this.removeStatusListener();
if (this.removeSelectable) {
this.removeSelectable();
}
this.telemetryCollection.off('add', this.setLatestValues);
this.telemetryCollection.off('clear', this.refreshData);
this.telemetryCollection.destroy();
this.openmct.time.off("bounds", this.refreshData);
if (this.mutablePromise) {
this.mutablePromise.then(() => {
@@ -253,9 +253,34 @@ export default {
return `At ${timeFormatter.format(this.datum)} ${this.domainObject.name} had a value of ${this.telemetryValue}${unit}`;
},
setLatestValues(data) {
this.latestDatum = data[data.length - 1];
this.updateView();
requestHistoricalData() {
let bounds = this.openmct.time.bounds();
let options = {
start: bounds.start,
end: bounds.end,
size: 1,
strategy: 'latest'
};
this.openmct.telemetry.request(this.domainObject, options)
.then(data => {
if (data.length > 0) {
this.latestDatum = data[data.length - 1];
this.updateView();
}
});
},
subscribeToObject() {
this.subscription = this.openmct.telemetry.subscribe(this.domainObject, function (datum) {
const key = this.openmct.time.timeSystem().key;
const datumTimeStamp = datum[key];
if (this.openmct.time.clock() !== undefined
|| (datumTimeStamp
&& (this.openmct.time.bounds().end >= datumTimeStamp))
) {
this.latestDatum = datum;
this.updateView();
}
}.bind(this));
},
updateView() {
if (!this.updatingView) {
@@ -266,10 +291,17 @@ export default {
});
}
},
removeSubscription() {
if (this.subscription) {
this.subscription();
this.subscription = undefined;
}
},
refreshData(bounds, isTick) {
if (!isTick) {
this.latestDatum = undefined;
this.updateView();
this.requestHistoricalData(this.domainObject);
}
},
setObject(domainObject) {
@@ -283,13 +315,8 @@ export default {
const valueMetadata = this.metadata.value(this.item.value);
this.customStringformatter = this.openmct.telemetry.customStringFormatter(valueMetadata, this.item.format);
this.telemetryCollection = this.openmct.telemetry.requestCollection(this.domainObject, {
size: 1,
strategy: 'latest'
});
this.telemetryCollection.on('add', this.setLatestValues);
this.telemetryCollection.on('clear', this.refreshData);
this.telemetryCollection.load();
this.requestHistoricalData();
this.subscribeToObject();
this.currentObjectPath = this.objectPath.slice();
this.currentObjectPath.unshift(this.domainObject);

View File

@@ -20,7 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
import { v4 as uuid } from 'uuid';
import uuid from 'uuid';
/**
* This class encapsulates the process of duplicating/copying a domain object

View File

@@ -22,7 +22,7 @@
import JSONExporter from '/src/exporters/JSONExporter.js';
import _ from 'lodash';
import { v4 as uuid } from 'uuid';
import uuid from "uuid";
export default class ExportAsJSONAction {
constructor(openmct) {

View File

@@ -57,7 +57,7 @@
/>
<drop-hint
:key="'hint-' + i"
:key="i"
class="c-fl-frame__drop-hint"
:index="i"
:allow-drop="allowDrop"
@@ -66,7 +66,7 @@
<resize-handle
v-if="(i !== frames.length - 1)"
:key="'handle-' + i"
:key="i"
:index="i"
:orientation="rowsLayout ? 'horizontal' : 'vertical'"
:is-editing="isEditing"

View File

@@ -1,4 +1,4 @@
import { v4 as uuid } from 'uuid';
import uuid from 'uuid';
class Container {
constructor(size) {

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